Repository: deadlyjack/code-editor Branch: main Commit: 0006340d32f6 Files: 510 Total size: 4.6 MB Directory structure: gitextract_3l2wh458/ ├── .babelrc ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── 0_bug_report.yml │ │ ├── 1_feature_request.yml │ │ └── config.yml │ ├── dependabot.yml │ ├── labeler.yml │ └── workflows/ │ ├── add-pr-labels.yml │ ├── ci.yml │ ├── close-inactive-issues.yml │ ├── community-release-notifier.yml │ ├── nightly-build.yml │ └── on-demand-preview-releases-PR.yml ├── .gitignore ├── .hintrc ├── .prettierrc ├── .vscode/ │ ├── launch.json │ ├── plugins.json │ ├── settings.json │ └── typings/ │ └── cordova/ │ └── cordova.d.ts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── _typos.toml ├── biome.json ├── build-extras.gradle ├── config.xml ├── fastlane/ │ └── metadata/ │ └── android/ │ └── en-US/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── hooks/ │ ├── README.md │ ├── modify-java-files.js │ ├── move-files.js │ ├── post-process.js │ └── restore-cordova-resources.js ├── jsconfig.json ├── license.txt ├── package.json ├── postcss.config.js ├── readme.md ├── res/ │ └── android/ │ ├── drawable/ │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ └── themes.xml │ └── xml/ │ └── network_security_config.xml ├── rspack.config.js ├── src/ │ ├── cm/ │ │ ├── baseExtensions.ts │ │ ├── colorView.ts │ │ ├── commandRegistry.js │ │ ├── editorUtils.ts │ │ ├── indentGuides.ts │ │ ├── lsp/ │ │ │ ├── api.ts │ │ │ ├── clientManager.ts │ │ │ ├── codeActions.ts │ │ │ ├── diagnostics.ts │ │ │ ├── documentSymbols.ts │ │ │ ├── formatter.ts │ │ │ ├── formattingSupport.ts │ │ │ ├── index.ts │ │ │ ├── inlayHints.ts │ │ │ ├── installRuntime.ts │ │ │ ├── installerUtils.ts │ │ │ ├── providerUtils.ts │ │ │ ├── references.ts │ │ │ ├── rename.ts │ │ │ ├── serverCatalog.ts │ │ │ ├── serverLauncher.ts │ │ │ ├── serverRegistry.ts │ │ │ ├── servers/ │ │ │ │ ├── index.ts │ │ │ │ ├── javascript.ts │ │ │ │ ├── luau.ts │ │ │ │ ├── python.ts │ │ │ │ ├── shared.ts │ │ │ │ ├── systems.ts │ │ │ │ └── web.ts │ │ │ ├── tooltipExtensions.ts │ │ │ ├── transport.ts │ │ │ ├── types.ts │ │ │ └── workspace.ts │ │ ├── mainEditorExtensions.ts │ │ ├── modelist.ts │ │ ├── modes/ │ │ │ └── luau/ │ │ │ └── index.ts │ │ ├── rainbowBrackets.ts │ │ ├── supportedModes.ts │ │ ├── themes/ │ │ │ ├── aura.js │ │ │ ├── dracula.js │ │ │ ├── githubDark.js │ │ │ ├── githubLight.js │ │ │ ├── index.js │ │ │ ├── monokai.js │ │ │ ├── noctisLilac.js │ │ │ ├── solarizedDark.js │ │ │ ├── solarizedLight.js │ │ │ ├── tokyoNight.js │ │ │ ├── tokyoNightDay.js │ │ │ ├── tomorrowNight.js │ │ │ ├── tomorrowNightBright.js │ │ │ └── vscodeDark.js │ │ └── touchSelectionMenu.js │ ├── components/ │ │ ├── WebComponents/ │ │ │ ├── index.js │ │ │ └── wcPage.js │ │ ├── audioPlayer/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── checkbox/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── collapsableList.js │ │ ├── contextmenu/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── fileTree/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── inputhints/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── logo/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── lspInfoDialog/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── lspStatusBar/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── page.js │ │ ├── palette/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── quickTools/ │ │ │ ├── footer.js │ │ │ ├── index.js │ │ │ ├── items.js │ │ │ └── style.scss │ │ ├── referencesPanel/ │ │ │ ├── index.js │ │ │ ├── referencesTab.js │ │ │ ├── styles.scss │ │ │ └── utils.js │ │ ├── scrollbar/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── searchbar/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── settingsPage.js │ │ ├── settingsPage.scss │ │ ├── sideButton/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── sidebar/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── symbolsPanel/ │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── tabView.js │ │ ├── terminal/ │ │ │ ├── index.js │ │ │ ├── ligatures.js │ │ │ ├── terminal.js │ │ │ ├── terminalDefaults.js │ │ │ ├── terminalManager.js │ │ │ ├── terminalThemeManager.js │ │ │ ├── terminalTouchSelection.css │ │ │ └── terminalTouchSelection.js │ │ ├── tile/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── toast/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── tutorial.js │ │ └── virtualList/ │ │ ├── index.js │ │ └── style.scss │ ├── dialogs/ │ │ ├── alert.js │ │ ├── box.js │ │ ├── color.js │ │ ├── confirm.js │ │ ├── loader.js │ │ ├── multiPrompt.js │ │ ├── prompt.js │ │ ├── rateBox.js │ │ ├── select.js │ │ └── style.scss │ ├── fileSystem/ │ │ ├── externalFs.js │ │ ├── ftp.js │ │ ├── index.js │ │ ├── internalFs.js │ │ └── sftp.js │ ├── handlers/ │ │ ├── editorFileTab.js │ │ ├── intent.js │ │ ├── keyboard.js │ │ ├── purchase.js │ │ ├── quickTools.js │ │ ├── quickToolsInit.js │ │ └── windowResize.js │ ├── index.d.ts │ ├── lang/ │ │ ├── ar-ye.json │ │ ├── be-by.json │ │ ├── bn-bd.json │ │ ├── cs-cz.json │ │ ├── de-de.json │ │ ├── en-us.json │ │ ├── es-sv.json │ │ ├── fr-fr.json │ │ ├── he-il.json │ │ ├── hi-in.json │ │ ├── hu-hu.json │ │ ├── id-id.json │ │ ├── ir-fa.json │ │ ├── it-it.json │ │ ├── ja-jp.json │ │ ├── ko-kr.json │ │ ├── ml-in.json │ │ ├── mm-unicode.json │ │ ├── mm-zawgyi.json │ │ ├── pl-pl.json │ │ ├── pt-br.json │ │ ├── pu-in.json │ │ ├── ru-ru.json │ │ ├── tl-ph.json │ │ ├── tr-tr.json │ │ ├── uk-ua.json │ │ ├── uz-uz.json │ │ ├── vi-vn.json │ │ ├── zh-cn.json │ │ ├── zh-hant.json │ │ └── zh-tw.json │ ├── lib/ │ │ ├── acode.js │ │ ├── actionStack.js │ │ ├── adRewards.js │ │ ├── applySettings.js │ │ ├── auth.js │ │ ├── checkFiles.js │ │ ├── checkPluginsUpdate.js │ │ ├── commands.js │ │ ├── console.js │ │ ├── constants.js │ │ ├── devTools.js │ │ ├── editorFile.js │ │ ├── editorManager.js │ │ ├── fileList.js │ │ ├── fileTypeHandler.js │ │ ├── fonts.js │ │ ├── installPlugin.js │ │ ├── installState.js │ │ ├── keyBindings.js │ │ ├── lang.js │ │ ├── loadPlugin.js │ │ ├── loadPlugins.js │ │ ├── logger.js │ │ ├── notificationManager.js │ │ ├── openFile.js │ │ ├── openFolder.js │ │ ├── polyfill.js │ │ ├── prettierFormatter.js │ │ ├── projects.js │ │ ├── recents.js │ │ ├── remoteStorage.js │ │ ├── removeAds.js │ │ ├── restoreFiles.js │ │ ├── restoreTheme.js │ │ ├── run.js │ │ ├── saveFile.js │ │ ├── saveState.js │ │ ├── searchHistory.js │ │ ├── secureAdRewardState.js │ │ ├── selectionMenu.js │ │ ├── settings.js │ │ ├── showFileInfo.js │ │ ├── startAd.js │ │ └── systemConfiguration.js │ ├── main.js │ ├── main.scss │ ├── pages/ │ │ ├── about/ │ │ │ ├── about.js │ │ │ ├── about.scss │ │ │ └── index.js │ │ ├── adRewards/ │ │ │ ├── adRewards.scss │ │ │ └── index.js │ │ ├── changelog/ │ │ │ ├── changelog.js │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── customTheme/ │ │ │ ├── customTheme.js │ │ │ ├── customTheme.scss │ │ │ └── index.js │ │ ├── fileBrowser/ │ │ │ ├── add-menu-home.hbs │ │ │ ├── add-menu.hbs │ │ │ ├── fileBrowser.hbs │ │ │ ├── fileBrowser.js │ │ │ ├── fileBrowser.scss │ │ │ ├── index.js │ │ │ ├── list.hbs │ │ │ └── util.js │ │ ├── fontManager/ │ │ │ ├── fontManager.js │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── markdownPreview/ │ │ │ ├── index.js │ │ │ ├── renderer.js │ │ │ └── style.scss │ │ ├── plugin/ │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ ├── plugin.scss │ │ │ └── plugin.view.js │ │ ├── plugins/ │ │ │ ├── index.js │ │ │ ├── item.js │ │ │ ├── plugins.js │ │ │ └── plugins.scss │ │ ├── problems/ │ │ │ ├── index.js │ │ │ ├── problems.js │ │ │ └── style.scss │ │ ├── quickTools/ │ │ │ ├── index.js │ │ │ ├── quickTools.js │ │ │ └── style.scss │ │ ├── sponsor/ │ │ │ ├── index.js │ │ │ ├── sponsor.js │ │ │ └── style.scss │ │ ├── sponsors/ │ │ │ ├── index.js │ │ │ ├── sponsors.js │ │ │ └── style.scss │ │ ├── themeSetting/ │ │ │ ├── index.js │ │ │ ├── themeSetting.js │ │ │ └── themeSetting.scss │ │ └── welcome/ │ │ ├── index.js │ │ ├── welcome.js │ │ └── welcome.scss │ ├── palettes/ │ │ ├── changeEditorTheme/ │ │ │ └── index.js │ │ ├── changeEncoding/ │ │ │ └── index.js │ │ ├── changeMode/ │ │ │ └── index.js │ │ ├── changeTheme/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── commandPalette/ │ │ │ └── index.js │ │ └── findFile/ │ │ └── index.js │ ├── plugins/ │ │ ├── auth/ │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ └── src/ │ │ │ └── android/ │ │ │ ├── Authenticator.java │ │ │ └── EncryptedPreferenceManager.java │ │ ├── browser/ │ │ │ ├── android/ │ │ │ │ └── com/ │ │ │ │ └── foxdebug/ │ │ │ │ └── browser/ │ │ │ │ ├── Browser.java │ │ │ │ ├── BrowserActivity.java │ │ │ │ ├── Emulator.java │ │ │ │ ├── Menu.java │ │ │ │ └── Plugin.java │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── res/ │ │ │ │ └── android/ │ │ │ │ ├── menu_enter.xml │ │ │ │ ├── menu_exit.xml │ │ │ │ └── styles.xml │ │ │ └── utils/ │ │ │ └── updatePackage.js │ │ ├── cordova-plugin-buildinfo/ │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── scripts/ │ │ │ │ ├── after_install.js │ │ │ │ ├── before_uninstall.js │ │ │ │ └── browser_after_prepare.js │ │ │ ├── src/ │ │ │ │ └── android/ │ │ │ │ ├── BuildInfo.gradle │ │ │ │ └── BuildInfo.java │ │ │ └── www/ │ │ │ └── buildinfo.js │ │ ├── custom-tabs/ │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── src/ │ │ │ │ └── CustomTabsPlugin.java │ │ │ └── www/ │ │ │ └── customtabs.js │ │ ├── ftp/ │ │ │ ├── LICENSE.md │ │ │ ├── README.md │ │ │ ├── index.d.ts │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── src/ │ │ │ │ └── android/ │ │ │ │ └── com/ │ │ │ │ └── foxdebug/ │ │ │ │ └── ftp/ │ │ │ │ └── Ftp.java │ │ │ └── www/ │ │ │ └── ftp.js │ │ ├── iap/ │ │ │ ├── index.d.ts │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── src/ │ │ │ │ └── com/ │ │ │ │ └── foxdebug/ │ │ │ │ └── iap/ │ │ │ │ └── Iap.java │ │ │ └── www/ │ │ │ └── plugin.js │ │ ├── pluginContext/ │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── src/ │ │ │ │ └── android/ │ │ │ │ └── Tee.java │ │ │ └── www/ │ │ │ └── PluginContext.js │ │ ├── proot/ │ │ │ ├── package.json │ │ │ └── plugin.xml │ │ ├── sdcard/ │ │ │ ├── index.d.ts │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── readme.md │ │ │ ├── src/ │ │ │ │ └── android/ │ │ │ │ └── SDcard.java │ │ │ └── www/ │ │ │ └── plugin.js │ │ ├── server/ │ │ │ ├── index.d.ts │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── src/ │ │ │ │ └── android/ │ │ │ │ └── com/ │ │ │ │ └── foxdebug/ │ │ │ │ └── server/ │ │ │ │ ├── NanoHTTPDWebserver.java │ │ │ │ └── Server.java │ │ │ └── www/ │ │ │ └── server.js │ │ ├── sftp/ │ │ │ ├── LICENSE.md │ │ │ ├── README.md │ │ │ ├── index.d.ts │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── src/ │ │ │ │ └── com/ │ │ │ │ └── foxdebug/ │ │ │ │ └── sftp/ │ │ │ │ └── Sftp.java │ │ │ └── www/ │ │ │ └── sftp.js │ │ ├── system/ │ │ │ ├── android/ │ │ │ │ └── com/ │ │ │ │ └── foxdebug/ │ │ │ │ └── system/ │ │ │ │ ├── RewardPassManager.java │ │ │ │ ├── SoftInputAssist.java │ │ │ │ ├── System.java │ │ │ │ └── Ui.java │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── readme.md │ │ │ ├── res/ │ │ │ │ └── android/ │ │ │ │ └── file_provider.xml │ │ │ ├── system.d.ts │ │ │ ├── utils/ │ │ │ │ ├── changeProvider.js │ │ │ │ ├── fixProvider.js │ │ │ │ └── resetProvider.js │ │ │ └── www/ │ │ │ └── plugin.js │ │ ├── terminal/ │ │ │ ├── package.json │ │ │ ├── plugin.xml │ │ │ ├── scripts/ │ │ │ │ ├── init-alpine.sh │ │ │ │ ├── init-sandbox.sh │ │ │ │ └── rm-wrapper.sh │ │ │ ├── src/ │ │ │ │ └── android/ │ │ │ │ ├── AlpineDocumentProvider.java │ │ │ │ ├── BackgroundExecutor.java │ │ │ │ ├── Executor.java │ │ │ │ ├── ProcessManager.java │ │ │ │ ├── ProcessServer.java │ │ │ │ ├── ProcessUtils.java │ │ │ │ ├── StreamHandler.java │ │ │ │ └── TerminalService.java │ │ │ └── www/ │ │ │ ├── Executor.js │ │ │ └── Terminal.js │ │ └── websocket/ │ │ ├── README.md │ │ ├── package.json │ │ ├── plugin.xml │ │ ├── src/ │ │ │ └── android/ │ │ │ ├── WebSocketInstance.java │ │ │ └── WebSocketPlugin.java │ │ └── www/ │ │ └── websocket.js │ ├── res/ │ │ ├── file-icons/ │ │ │ └── style.css │ │ └── icons/ │ │ └── style.css │ ├── settings/ │ │ ├── appSettings.js │ │ ├── backupRestore.js │ │ ├── editorSettings.js │ │ ├── filesSettings.js │ │ ├── formatterSettings.js │ │ ├── helpSettings.js │ │ ├── lspConfigUtils.js │ │ ├── lspServerDetail.js │ │ ├── lspSettings.js │ │ ├── mainSettings.js │ │ ├── previewSettings.js │ │ ├── scrollSettings.js │ │ ├── searchSettings.js │ │ └── terminalSettings.js │ ├── sidebarApps/ │ │ ├── extensions/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── files/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── index.js │ │ ├── notification/ │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── searchInFiles/ │ │ │ ├── cmResultView.js │ │ │ ├── index.js │ │ │ ├── styles.scss │ │ │ └── worker.js │ │ └── sidebarApp.js │ ├── styles/ │ │ ├── codemirror.scss │ │ ├── console.m.scss │ │ ├── fileInfo.scss │ │ ├── fonts.scss │ │ ├── keyframes.scss │ │ ├── list.scss │ │ ├── markdown.scss │ │ ├── mixins.scss │ │ ├── overrideAceStyle.scss │ │ ├── page.scss │ │ └── wideScreen.scss │ ├── test/ │ │ ├── ace.test.js │ │ ├── editor.tests.js │ │ ├── exec.tests.js │ │ ├── sanity.tests.js │ │ ├── tester.js │ │ └── url.tests.js │ ├── theme/ │ │ ├── builder.js │ │ ├── list.js │ │ └── preInstalled.js │ ├── utils/ │ │ ├── Path.js │ │ ├── Uri.js │ │ ├── Url.js │ │ ├── codeHighlight.js │ │ ├── color/ │ │ │ ├── hex.js │ │ │ ├── hsl.js │ │ │ ├── index.js │ │ │ ├── regex.js │ │ │ └── rgb.js │ │ ├── encodings.js │ │ ├── helpers.js │ │ ├── keyboardEvent.js │ │ ├── polyfill.js │ │ └── taskManager.js │ └── views/ │ ├── console.hbs │ ├── file-info.hbs │ ├── file-menu.hbs │ ├── markdown.hbs │ ├── menu.hbs │ └── rating.hbs ├── tsconfig.json ├── utils/ │ ├── code-editor-icon.icomoon.json │ ├── config.js │ ├── custom-loaders/ │ │ └── html-tag-jsx-loader.js │ ├── lang.js │ ├── loadStyles.js │ ├── scripts/ │ │ ├── build.sh │ │ ├── clean.sh │ │ ├── generate-release-notes.js │ │ ├── plugin.sh │ │ ├── setup.sh │ │ └── start.sh │ ├── setup.js │ ├── storage_manager.mjs │ └── updateAce.js ├── webpack.config.js └── www/ └── index.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ [ "@babel/preset-env", { "useBuiltIns": "entry", "corejs": "3.22" } ], "@babel/preset-typescript" ], "plugins": [ "html-tag-js/jsx/jsx-to-tag.js", "html-tag-js/jsx/syntax-parser.js", "@babel/plugin-transform-runtime", "@babel/plugin-transform-block-scoping" ], "compact": false, "sourceMaps": "inline" } ================================================ FILE: .devcontainer/Dockerfile ================================================ # Acode Development Container - Standalone Docker Build # # This Dockerfile is for MANUAL Docker builds (docker build/run). # Usage: # docker build -t acode-dev .devcontainer/ # docker run -it -v $(pwd):/workspaces/acode acode-dev FROM mcr.microsoft.com/devcontainers/java:1-21-bullseye ARG ANDROID_PLATFORM=35 ARG ANDROID_BUILD_TOOLS=35.0.0 ARG CMDLINE_TOOLS_VERSION=11076708 ARG NODE_VERSION=22 ARG GRADLE_VERSION=8.11 ENV ANDROID_HOME=/opt/android-sdk ENV ANDROID_SDK_ROOT=/opt/android-sdk ENV GRADLE_HOME=/opt/gradle ENV PATH="${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${GRADLE_HOME}/bin" RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ unzip \ && rm -rf /var/lib/apt/lists/* # Install Gradle RUN wget -q "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -O /tmp/gradle.zip \ && unzip -q /tmp/gradle.zip -d /opt \ && rm /tmp/gradle.zip \ && ln -s /opt/gradle-${GRADLE_VERSION} ${GRADLE_HOME} # Install fnm and Node.js ENV FNM_DIR=/usr/local/fnm ENV PATH="${FNM_DIR}:${PATH}" RUN curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir "${FNM_DIR}" --skip-shell \ && eval "$(${FNM_DIR}/fnm env)" \ && fnm install ${NODE_VERSION} \ && fnm default ${NODE_VERSION} \ && npm install -g pnpm ENV PATH="${FNM_DIR}/aliases/default/bin:${PATH}" # Install Android SDK RUN mkdir -p ${ANDROID_HOME}/cmdline-tools \ && cd ${ANDROID_HOME}/cmdline-tools \ && wget -q "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip" -O cmdline-tools.zip \ && unzip -q cmdline-tools.zip \ && rm cmdline-tools.zip \ && mv cmdline-tools latest \ && yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --licenses 2>/dev/null || true \ && ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --update \ && ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager \ "platform-tools" \ "platforms;android-${ANDROID_PLATFORM}" \ "build-tools;${ANDROID_BUILD_TOOLS}" WORKDIR /workspaces/acode LABEL maintainer="Acode Foundation" LABEL description="Development environment for building Acode - Code Editor for Android" ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Acode Development", "image": "mcr.microsoft.com/devcontainers/java:1-21-bullseye", "features": { "ghcr.io/devcontainers/features/java:1": { "installGradle": true, "installGroovy": false, "installMaven": false, "installAnt": false, "version": "21", "jdkDistro": "ms", "gradleVersion": "latest" }, "ghcr.io/nordcominc/devcontainer-features/android-sdk:1": { "platform": "35", "build_tools": "35.0.0" }, "ghcr.io/devcontainers/features/node:1": { "nodeGypDependencies": false, "installYarnUsingApt": false, "version": "lts", "pnpmVersion": "latest", "nvmVersion": "latest" } }, "postCreateCommand": "pnpm run setup", "customizations": { "vscode": { "extensions": ["biomejs.biome", "redhat.java"], "settings": { "editor.formatOnSave": true, "editor.defaultFormatter": "biomejs.biome", "[java]": { "editor.defaultFormatter": "redhat.java" } } } } } ================================================ FILE: .dockerignore ================================================ # Dependencies node_modules/ .pnpm-store/ # Build outputs platforms/ plugins/ www/css/build/ www/js/build/ # IDE .vscode/ .idea/ # Git .git/ .gitignore # Misc *.log *.apk *.aab .DS_Store Thumbs.db ================================================ FILE: .gitattributes ================================================ *.sh text eol=lf ================================================ FILE: .github/CODEOWNERS ================================================ .github/CODEOWNERS @Acode-Foundation/acode # workflows .github/workflows/nightly-build.yml @unschooledgamer .github/workflows/on-demand-preview-releases-PR.yml @unschooledgamer .github/workflows/community-release-notifier.yml @unschooledgamer ================================================ FILE: .github/ISSUE_TEMPLATE/0_bug_report.yml ================================================ name: Bug Report description: | Use this template for bug reports. labels: [] body: - type: checkboxes attributes: label: Check for existing issues description: Check the backlog of issues to reduce duplicates; if an issue already exists, place a 👍 on it. options: - label: Completed required: true - type: textarea attributes: label: Describe the bug / provide steps to reproduce it description: A clear and concise description of what the bug is. If possible, then include a clip validations: required: true - type: textarea id: environment attributes: label: Environment description: Provide your app version, Android version, webview version, etc (Search "Copy Device Info" in command palette to get all these info and share it) validations: required: true - type: textarea attributes: label: If applicable, add mockups / screenshots regarding your vision description: Drag issues into the text input below validations: required: false - type: textarea attributes: label: If applicable, attach your Acode.log file to this issue. description: | Search for `Open Log File` in Acode command palette, it will open Acode.log file then share it or Check this location for log file: `/storage/emulated/0/Android/com.foxdebug.acode{in case of free version there will be bit more}/files/Acode.log` value: |
Acode.log
        

        
validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/1_feature_request.yml ================================================ name: Feature Request description: "Suggest a new feature for Acode" labels: ["enhancement"] body: - type: checkboxes attributes: label: Check for existing issues description: Check the backlog of issues to reduce duplicates; if an issue already exists, place a `+1` (👍) on it. options: - label: Completed required: true - type: textarea attributes: label: Describe the feature description: A clear and concise description of what you want to happen. validations: required: true - type: textarea attributes: label: | If applicable, add mockups / screenshots to help present your vision of the feature description: Drag images into the text input below validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask Questions url: https://github.com/Acode-Foundation/Acode/discussions about: Ask any questions regarding Acode and its ecosystem - name: Telegram Group url: https://t.me/foxdebug_acode about: Friendly Acode Community for helps regarding Acode, development and more - name: Discord Server url: https://discord.gg/vVxVWYUAWD about: Acode discord server ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for more information: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates # https://containers.dev/guide/dependabot version: 2 updates: - package-ecosystem: "devcontainers" directory: "/" schedule: interval: weekly - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly ================================================ FILE: .github/labeler.yml ================================================ translations: - any: - changed-files: - any-glob-to-any-file: 'src/lang/*.json' docs: - any: - changed-files: - any-glob-to-any-file: '**/*.md' enhancement: - any: - head-branch: ['^feature', 'feature', '^feat', '^add'] ================================================ FILE: .github/workflows/add-pr-labels.yml ================================================ name: Add Pull Requests Labels on: pull_request_target: types: [opened, closed, ready_for_review] jobs: add-labels: timeout-minutes: 5 runs-on: ubuntu-latest if: github.event.action == 'opened' permissions: contents: read pull-requests: write steps: - uses: actions/labeler@v6 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: pull_request: jobs: spell-check: timeout-minutes: 5 name: Check spelling runs-on: ubuntu-latest steps: - name: Checkout Actions Repository uses: actions/checkout@v6 - name: Check spelling uses: crate-ci/typos@master with: config: ./_typos.toml quality: timeout-minutes: 5 name: Linting and formatting runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Setup Biome uses: biomejs/setup-biome@v2 - name: Run Biome run: biome ci . translation-check: name: Translation Check (On PR Only) timeout-minutes: 5 runs-on: ubuntu-latest permissions: contents: read pull-requests: read if: github.event_name == 'pull_request' steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Use Node.js uses: actions/setup-node@v6 with: cache: npm cache-dependency-path: '**/package-lock.json' - name: Detect Changed Files uses: dorny/paths-filter@v4 id: file-changes with: list-files: shell filters: | translation: - 'src/lang/*.json' token: ${{ secrets.GITHUB_TOKEN }} - name: Translation Files Check (if changed) if: steps.file-changes.outputs.translation == 'true' run: | npm ci --no-audit --no-fund npm run lang check ================================================ FILE: .github/workflows/close-inactive-issues.yml ================================================ name: Close inactive issues on: schedule: - cron: "30 1 * * *" jobs: close-issues: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v10 with: days-before-issue-stale: 60 days-before-issue-close: 14 stale-issue-label: "stale" stale-issue-message: > Hi there! 👋 We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Acode, please let us know by commenting on this issue(i.e Bump!), and we will keep it open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, we'll close it in 14 days. Thanks for your help! close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." exempt-all-milestones: true days-before-pr-stale: -1 days-before-pr-close: -1 exempt-issue-labels: "new plugin idea, todo, enhancement, bug, Low priority, documentation" operations-per-run: 100 repo-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/community-release-notifier.yml ================================================ name: community-release-notifier on: release: types: [ released ] workflow_call: inputs: tag_name: required: true description: "Release tag_name" type: 'string' url: required: true description: "release URL" type: 'string' body: required: true description: "Release Body" type: 'string' default: '' secrets: DISCORD_WEBHOOK_RELEASE_NOTES: description: 'Discord Webhook for Notifying Releases to Discord' required: true TELEGRAM_BOT_TOKEN: description: 'Telegram Bot Token' required: true TELEGRAM_CHAT_ID: description: 'Telegram Chat ID (group/channel/supergroup)' required: true TELEGRAM_MESSAGE_THREAD_ID: description: 'Topic / message_thread_id for Telegram forum/topic' required: true jobs: notify: if: github.repository_owner == 'Acode-Foundation' runs-on: ubuntu-latest steps: - name: Prepare release variables id: vars env: INPUT_TAG: ${{ github.event.release.tag_name || inputs.tag_name }} INPUT_URL: ${{ github.event.release.url || inputs.url }} INPUT_BODY: ${{ github.event.release.body || inputs.body }} run: | TAG="$INPUT_TAG" URL="$INPUT_URL" # Generate a random delimiter (hex string, safe and collision-resistant) DELIMITER=$(openssl rand -hex 16 || head -c 16 /dev/urandom | xxd -p -c 16) # Escape problematic characters for MarkdownV2 (very conservative escaping) # We escape: _ * [ ] ( ) ~ ` > # + - = | { } . ! \ BODY_SAFE=$(printf '%s' "$INPUT_BODY" | \ sed 's/[_*[\]()~`>#+=|{}.!\\-]/\\&/g') TAG_SAFE=$(printf '%s' "$TAG" | sed 's/[_*[\]()~`>#+=|{}.!\\-]/\\&/g') if [[ "$TAG" == *"-nightly"* ]]; then SUFFIX=" \(Nightly Release\)" SUFFIXPLAIN=" (Nightly Release)" else SUFFIX="" SUFFIXPLAIN="" fi # Announcement line — also escape for safety ANNOUNCE_SAFE="📢 Acode [$TAG_SAFE]($URL) was just Released 🎉${SUFFIX}\\!" echo "announce=$ANNOUNCE_SAFE" >> $GITHUB_OUTPUT { echo "body_safe<<$DELIMITER" printf '%s\n' "$BODY_SAFE" echo "$DELIMITER" } >> $GITHUB_OUTPUT # Plain (MD) Announcement for Discord ANNOUNCE_PLAIN="📢 Acode [$TAG](<$URL>) was just Released 🎉${SUFFIXPLAIN}!" echo "announce_plain=$ANNOUNCE_PLAIN" >> $GITHUB_OUTPUT { echo "body_plain<<$DELIMITER" printf '%s\n' "$INPUT_BODY" echo "$DELIMITER" } >> $GITHUB_OUTPUT # ──────────────────────────────────────────────── # Truncate for Discord # ──────────────────────────────────────────────── - name: Truncate message for Discord id: truncate-discord uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1 env: # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true with: maxLength: 2000 stringToTruncate: | ${{ steps.vars.outputs.announce_plain }} ${{ steps.vars.outputs.body_plain }} # ──────────────────────────────────────────────── # Discord notification # ──────────────────────────────────────────────── - name: Discord Webhook (Publishing) uses: tsickert/discord-webhook@b217a69502f52803de774ded2b1ab7c282e99645 # v7.0.0 env: # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }} content: ${{ steps.truncate-discord.outputs.string }} flags: 4 # 1 << 2 - SUPPRESS_EMBEDS! # ──────────────────────────────────────────────── # Telegram notification — MarkdownV2 + no link preview # ──────────────────────────────────────────────── - name: Send to Telegram #if: ${{ secrets.TELEGRAM_BOT_TOKEN != '' && secrets.TELEGRAM_CHAT_ID != '' && secrets.TELEGRAM_MESSAGE_THREAD_ID != '' }} uses: Salmansha08/telegram-github-action@17c9ce6b4210d2659dca29d34028b02fa29d70ad # or newer tag if available with: to: ${{ secrets.TELEGRAM_CHAT_ID }} token: ${{ secrets.TELEGRAM_BOT_TOKEN }} message: | ${{ steps.vars.outputs.announce }} ${{ steps.vars.outputs.body_safe }} format: markdown disable_web_page_preview: true # Only needed for topic-enabled supergroups/channels message_thread_id: ${{ secrets.TELEGRAM_MESSAGE_THREAD_ID }} continue-on-error: true ================================================ FILE: .github/workflows/nightly-build.yml ================================================ # Implement to use environment secrets someday, At the moment GH Actions doesn't support those in reusable workflows (ref: https://github.com/actions/runner/issues/1490) # at least that's what I found. name: Nightly Build on: schedule: # Fire every day at 7:00am UTC (Roughly before EU workday and after US workday) - cron: "0 7 * * *" push: tags: - nightly workflow_call: inputs: is_PR: default: false type: boolean description: If a Pull Request has triggered it. PR_NUMBER: required: true type: number description: The Pull Request that triggered this workflow skip_tagging_and_releases: required: false default: true type: boolean description: Skips Tagging & releases, since workflow_call isn't available for github.event_name, default is true outputs: job_result: description: "Build job result" value: ${{ jobs.build.result }} workflow_dispatch: inputs: skip_tagging_and_releases: required: false default: true type: boolean description: Skips Tagging & releases, since workflow_call isn't available for github.event_name, default is true concurrency: # Allow only one workflow per any non-`main` branch. group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}-${{ inputs.is_PR && 'is_PR' || 'not_PR'}} cancel-in-progress: true permissions: contents: read env: STORE_FILE_PATH: /tmp/app-debug.keystore BUILD_JSON_PATH: build.json VERSION_LABEL: ${{ (inputs.is_PR && 'pr') || 'nightly' }} DISCORD_RELEASE_NOTIFIER_ENABLED: "true" jobs: build: timeout-minutes: 60 runs-on: ubuntu-latest if: github.repository_owner == 'Acode-Foundation' permissions: # contents write is needed to create Nightly Releases. contents: write # issues: write pull-requests: write outputs: release_output_url: ${{ steps.release.outputs.url }} updated_version: ${{ steps.update-version.outputs.UPDATED_VERSION}} RELEASE_NOTES: ${{ env.RELEASE_NOTES }} steps: - name: Fast Fail if secrets are missing if: ${{ env.KEYSTORE_CONTENT == '' || env.BUILD_JSON_CONTENT == '' }} env: KEYSTORE_CONTENT: ${{ secrets.KEYSTORE_CONTENT }} BUILD_JSON_CONTENT: ${{ secrets.BUILD_JSON_CONTENT }} run: | echo "::error title=Missing Secrets::KEYSTORE_CONTENT or BUILD_JSON_CONTENT secrets are missing! Aborting workflow." exit 1 - name: Logging & summaries run: | echo "::group::Logging" echo "🎯 github trigger event name: ${{ github.event_name }}" echo "is_PR: ${{ inputs.is_PR }} " echo "PR_NUMBER: ${{ inputs.PR_NUMBER }}" echo "env: STORE_FILE_PATH: ${{ env.STORE_FILE_PATH }}" echo "env: BUILD_JSON_PATH: ${{ env.BUILD_JSON_PATH }}" echo "env: VERSION_LABEL: ${{ env. VERSION_LABEL }}" echo "github sha: ${{ github.sha }}" echo "should not skip tags, releases: ${{ ! inputs.skip_tagging_and_releases }} " echo "🤐 env: NORMAL_APK_PATH: ${{ env.NORMAL_APK_PATH }}" echo "🤐 env: FDROID_APK_PATH: ${{ env.FDROID_APK_PATH }}" echo "::endgroup::" echo "## 🚀 Build Type: ${{ env.VERSION_LABEL }}" >> $GITHUB_STEP_SUMMARY echo "is_PR: ${{ inputs.is_PR || 'NOT a PR' }}" >> $GITHUB_STEP_SUMMARY echo "PR_NUMBER: ${{ inputs.PR_NUMBER || 'not a PR' }}" >> $GITHUB_STEP_SUMMARY echo "should not skip tags, releases: ${{ ! inputs.skip_tagging_and_releases }}" >> $GITHUB_STEP_SUMMARY - name: Checkout Repository uses: actions/checkout@v6 with: fetch-depth: 0 # Required for tags # persists credentials locally if tagging and releases are not skipped. persist-credentials: ${{ ! inputs.skip_tagging_and_releases }} ref: ${{ (inputs.is_PR && inputs.PR_NUMBER) && github.event.pull_request.head.sha || '' }} - name: Set up Java 21 uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' cache: ${{ (!(inputs.is_PR && inputs.PR_NUMBER) && github.ref == 'refs/heads/main' && 'gradle') || '' }} - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: 'lts/*' # or '18.x' for latest stable cache: ${{ (!(inputs.is_PR && inputs.PR_NUMBER) && github.ref == 'refs/heads/main' && 'npm') || '' }} - name: Add keystore and build.json from secrets run: | echo "${{ secrets.KEYSTORE_CONTENT }}" | base64 -d > ${{ env.STORE_FILE_PATH }} echo "${{ secrets.BUILD_JSON_CONTENT }}" | base64 -d > ${{ env.BUILD_JSON_PATH }} echo "Keystore and build.json added successfully." - name: Export Commit Hash & prev tag run: | echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV echo "PREV_TAG=$(git describe --tags --abbrev=0 || git rev-list --max-parents=0 HEAD)" >> $GITHUB_ENV - name: Extract versionCode and version from config.xml id: extract_version run: | if [ ! -f config.xml ]; then echo "config.xml not found!" exit 1 fi VERSION_CODE=$(grep 'versionCode=' config.xml | sed -E 's/.*versionCode="([0-9]+)".*/\1/') VERSION=$(grep -oP 'version="\K[0-9.]+' config.xml) echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_OUTPUT echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "Extracted Version Code: $VERSION_CODE" echo "Extracted Version: $VERSION" - name: Append Commit Hash to Version and Update config.xml id: update-version run: | SHORT_COMMIT_HASH=$(echo "${{ env.GIT_COMMIT }}" | cut -c 1-7) if ${{ inputs.is_PR || false }}; then PR_NUMBER=${{ inputs.PR_NUMBER }} else PR_NUMBER= fi UPDATED_VERSION="${{ steps.extract_version.outputs.VERSION }}-${{ env.VERSION_LABEL }}.${PR_NUMBER:-$SHORT_COMMIT_HASH}" echo "Updated Version: $UPDATED_VERSION" # Update config.xml with the new version sed -i "s/version=\"${{ steps.extract_version.outputs.VERSION }}\"/version=\"$UPDATED_VERSION\"/g" config.xml echo "Updated version in config.xml" # Output the updated version echo "UPDATED_VERSION=$UPDATED_VERSION" >> $GITHUB_ENV echo "UPDATED_VERSION=$UPDATED_VERSION" >> $GITHUB_OUTPUT echo "NORMAL_APK_PATH=/tmp/acode-debug-normal-${UPDATED_VERSION}.apk" >> $GITHUB_ENV echo "FDROID_APK_PATH=/tmp/acode-debug-fdroid-${UPDATED_VERSION}.apk" >> $GITHUB_ENV - name: Install Node.js Packages run: npm install - name: Install Cordova CLI run: npm install -g cordova - name: Run npm setup run: npm run setup - name: Run npm build paid dev apk run: | node utils/storage_manager.mjs y npm run build paid dev apk mv platforms/android/app/build/outputs/apk/debug/app-debug.apk ${{ env.NORMAL_APK_PATH }} echo "VERSION: $UPDATED_VERSION" >> $GITHUB_STEP_SUMMARY - name: Upload APK Artifact uses: actions/upload-artifact@v7 with: name: app-debug-${{ env.GIT_COMMIT }} path: ${{ env.NORMAL_APK_PATH }} - name: Run npm build paid dev apk fdroid (for F-Droid) if: ${{ !inputs.is_PR }} run: | node utils/storage_manager.mjs y npm run build paid dev apk fdroid mv platforms/android/app/build/outputs/apk/debug/app-debug.apk ${{ env.FDROID_APK_PATH }} - name: Upload APK Artifact uses: actions/upload-artifact@v7 if: ${{ !inputs.is_PR }} with: name: app-debug-fdroid-${{ env.GIT_COMMIT }} path: ${{ env.FDROID_APK_PATH }} - name: remove keystore and build.json run: | rm $STORE_FILE_PATH $BUILD_JSON_PATH echo "Keystore and build.json removed successfully." - name: Check Nightly Tag and Force Update #if: github.event_name == 'push' && contains(github.event.ref, 'tags/nightly') == false if: ${{ ! inputs.skip_tagging_and_releases }} id: check-nightly-tag-force-update run: | # Check if the nightly tag exists and get the commit it points to if git show-ref --quiet refs/tags/nightly; then TAG_COMMIT=$(git rev-parse nightly) else TAG_COMMIT="" fi # If the current commit is the same as the tag, skip updating the tag if [ "$TAG_COMMIT" != "${{ env.GIT_COMMIT }}" ]; then echo "releaseRequired=true" >> $GITHUB_ENV # export tag commit before updating for change logs comparing. echo "TAG_COMMIT=$TAG_COMMIT" >> $GITHUB_ENV git config --global user.name 'GitHub Actions' git config --global user.email 'github-actions@github.com' git tag -f nightly git push origin nightly --force else echo "Nightly tag already points to this commit. Skipping update." fi # 🚨⚠️ WARNING: the GITHUB_TOKEN under this step, has access to write & read access to Contents, Pull Requests # Which is why, it uses a fine-granted token with Read-Only Access to Public Repos Only. - name: Generate Release Notes (Experimental) if: ${{ success() && env.releaseRequired == 'true' }} id: gen-release-notes continue-on-error: true run: | RELEASE_NOTES=$(node utils/scripts/generate-release-notes.js ${{ github.repository_owner }} Acode ${{ github.sha }} --format md --from-tag ${{ env.TAG_COMMIT }} --important-only --quiet --changelog-only) { echo "RELEASE_NOTES<> $GITHUB_ENV env: GITHUB_TOKEN: ${{ secrets.NIGHTLY_RELEASE_NOTES_GH_TOKEN }} - name: Release Nightly Version # Only run this step, if not called from another workflow. And a previous step is successful with releasedRequired=true id: release if: ${{ ! inputs.skip_tagging_and_releases && steps.check-nightly-tag-force-update.outcome == 'success' && env.releaseRequired == 'true' && !inputs.is_PR }} uses: softprops/action-gh-release@v2 env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true with: prerelease: true name: ${{ env.UPDATED_VERSION }} tag_name: ${{ env.UPDATED_VERSION }} files: | ${{ env.NORMAL_APK_PATH }} ${{ env.FDROID_APK_PATH }} body: | Automated Nightly (pre-release) Releases for Today [Compare Changes](https://github.com/${{ github.repository }}/compare/${{ env.TAG_COMMIT }}...${{ github.sha }}) ${{ env.RELEASE_NOTES }} - name: Update Last Comment by bot (If ran in PR) if: inputs.is_PR uses: marocchino/sticky-pull-request-comment@v3 with: hide_and_recreate: true hide_classify: "OUTDATED" header: on-demand-build-status message: | Preview Release for this, has been built. [Click here to view that github actions build](https://github.com/${{ github.repository}}/actions/runs/${{ github.run_id }}) community-release-notifier: needs: build # Run only if push tags, or triggered by a schedule if: ${{ github.repository_owner == 'Acode-Foundation' && (contains(fromJSON('["push", "schedule"]'), github.event_name) || ! inputs.skip_tagging_and_releases) && needs.build.outputs.release_output_url }} uses: Acode-Foundation/acode/.github/workflows/community-release-notifier.yml@main with: tag_name: ${{ needs.build.outputs.updated_version }} url: ${{ needs.build.outputs.release_output_url }} body: ${{ needs.build.outputs.RELEASE_NOTES }} secrets: DISCORD_WEBHOOK_RELEASE_NOTES: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} TELEGRAM_MESSAGE_THREAD_ID: ${{ secrets.TELEGRAM_MESSAGE_THREAD_ID }} ================================================ FILE: .github/workflows/on-demand-preview-releases-PR.yml ================================================ name: On Demand Preview Releases for PR # avoids paths like .md, and anything in .github folder on: pull_request_target: paths-ignore: - '!*.md' # - '.github/**' types: [labeled, synchronize] # defined at workflow-level as the workflow, Requires these permissions to function. permissions: contents: write pull-requests: write # All Pull Requests are issues, but not all issues are Pull Requests (like GitHub says 🙃) issues: write concurrency: # Allow only one workflow per any non-`main` branch. group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} cancel-in-progress: true jobs: job_trigger: name: Trigger Preview Release (if conditions met) if: | github.event.pull_request.draft == false && (github.repository_owner == 'Acode-Foundation' && (!contains(github.event.pull_request.labels.*.name, 'DO NOT PREVIEW RELEASE') && (contains(github.event.pull_request.labels.*.name, 'Bypass check - PREVIEW RELEASE') || contains(github.event.pull_request.labels.*.name, 'CI: RUN ON-DEMAND PREVIEW RELEASES'))) ) runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: clean: false fetch-depth: 0 persist-credentials: false # Checkout pull request HEAD commit instead of merge commit ref: ${{ github.event.pull_request.head.sha }} - name: Remove Manually added PR Label if: | contains(github.event.pull_request.labels.*.name, 'CI: RUN ON-DEMAND PREVIEW RELEASES') run: gh pr edit $PR --remove-label "$labels" env: PR: ${{ github.event.pull_request.number }} labels: 'CI: RUN ON-DEMAND PREVIEW RELEASES' GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Add comment to PR uses: marocchino/sticky-pull-request-comment@v3 with: hide_and_recreate: true hide_classify: "OUTDATED" header: on-demand-build-status message: | ⚒️ Starting a workflow to build, Your On-Demand Preview Release/build for ${{ github.event.pull_request.html_url || github.event.pull_request.url }}. status: **\`🟡 in_progress\`** Kindly wait, this message may be updated with success or failure status. For Owners: Please [Click here to view that github actions](https://github.com/${{ github.repository}}/actions/runs/${{ github.run_id }})/ env: PR: ${{ github.event.pull_request.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} trigger_builder: needs: job_trigger secrets: inherit uses: Acode-Foundation/acode/.github/workflows/nightly-build.yml@main with: is_PR: true PR_NUMBER: ${{ github.event.pull_request.number }} skip_tagging_and_releases: true update_Last_Comment: needs: [job_trigger,trigger_builder] runs-on: ubuntu-latest if: ${{ github.repository_owner == 'Acode-Foundation' && always() && contains(fromJSON('["failure","cancelled"]'), needs.trigger_builder.result) }} steps: # - name: Checkout code # uses: actions/checkout@v4 # with: # clean: false # fetch-depth: 0 - name: Update Last Comment by bot (if Workflow Triggering failed) uses: marocchino/sticky-pull-request-comment@v3 with: hide_and_recreate: true hide_classify: "OUTDATED" header: on-demand-build-status message: | 🔴 (Workflow Trigger stopped), Your On-Demand Preview Release/build for ${{ github.event.pull_request.html_url || github.event.pull_request.url }}. status: **${{ needs.trigger_builder.result || 'failure'}}** --- For Owners: Please [Click here to view that github actions](https://github.com/${{ github.repository}}/actions/runs/${{ github.run_id }}) ================================================ FILE: .gitignore ================================================ /node_modules /build.json /www/build /plugins /platforms /keystore.jks /platforms/android/debug-signing.properties /platforms/android/release-signing.properties **/*/.DS_Store .DS_Store pnpm-lock.yaml .zed .idea ace-builds fdroid.bool ================================================ FILE: .hintrc ================================================ { "extends": [ "development" ], "hints": { "compat-api/css": "off", "css-prefix-order": "off", "meta-viewport": "off", "detect-css-reflows/composite": "off", "detect-css-reflows/paint": "off" } } ================================================ FILE: .prettierrc ================================================ {} ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Run Android", "skipFiles": [ "/**" ], "program": "${workspaceFolder}\\utils\\run-android.js", } ] } ================================================ FILE: .vscode/plugins.json ================================================ {"plugins":["cordova-clipboard","cordova-plugin-buildinfo","cordova-plugin-device","cordova-plugin-file","cordova-plugin-sftp","cordova-plugin-server","cordova-plugin-ftp","cordova-plugin-sdcard","cordova-plugin-browser","cordova-plugin-iap","cordova-plugin-system","cordova-plugin-advanced-http"]} ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "autoimport.doubleQuotes": false, "java.configuration.updateBuildConfiguration": "disabled", "prettier.requireConfig": true, "javascript.format.enable": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "prettier.tabWidth": 2, "prettier.useTabs": false, "javascript.format.semicolons": "insert", "[scss]": { "editor.defaultFormatter": "vscode.css-language-features" }, "emmet.syntaxProfiles": { "xml": { "attr_quotes": "single" }, "html": { "attr_quotes": "single" }, "javascript": { "attr_quotes": "single", "attr_quotes_style": "single", "self_closing_tag": true }, "jsx": { "attr_quotes": "single" } }, "cSpell.words": [ "abap", "Acode", "acodefree", "actionscript", "addedfolder", "addedfolders", "addstorage", "admob", "adoc", "Ajit", "alda", "ANDROIDX", "anglebracket", "angularjs", "antlr", "anyscript", "applescript", "ARGB", "Astro", "Asuming", "audiotrack", "autocorrect", "autohotkey", "autoit", "autosize", "azurepipelines", "babelrcpath", "backbutton", "bazel", "biml", "bitbucketpipeline", "bithound", "browserslist", "buckbuild", "buildinfo", "cakefile", "cakephp", "canrun", "canrw", "Cascadia", "changemode", "cheader", "cirru", "clearclose", "clojurescript", "cloudfoundry", "codacy", "codeclimate", "codecov", "codekit", "coffeelint", "colorpicker", "commitlintignore", "commitlintrc", "configpath", "copyline", "copylinesdown", "copylinesup", "cordova", "corejs", "cppheader", "crossorigin", "Csound", "csscomb", "csslint", "cssmap", "cuda", "curlybracket", "customise", "cython", "darcs", "dartlang", "deadlyjack", "Debuggable", "deviceready", "dlang", "dockertest", "docpad", "docz", "dotjs", "downloadget", "doxygen", "DPAD", "dustjs", "easylogic", "Edifact", "editmenu", "endregion", "ensime", "ERUDA", "filebrowser", "filesize", "Fira", "firebasehosting", "firestore", "flac", "Flix", "floobits", "Foxdebug", "freemarker", "gamemaker", "gcode", "getfilename", "gitmodules", "glsl", "Gobstones", "googleplay", "gotoline", "graphviz", "greenkeeper", "gridsome", "guardfile", "haml", "HARDKEYBOARDHIDDEN", "haxe", "haxecheckstyle", "haxedevelop", "hideconsole", "historyrestore", "hjson", "hlsl", "homeassistant", "htgroups", "htmlhint", "htmlpath", "htpasswd", "idris", "idrisbin", "idrispkg", "imba", "INAPP", "infopath", "informix", "innosetup", "inputhints", "inputmethod", "intoview", "Invisibles", "irid", "jbuilder", "JEXL", "jsbeautify", "jsbeautifyrc", "jsmap", "jsonld", "jsonnet", "JSSM", "KEYBOARDHIDDEN", "KHTML", "kitchenci", "kivy", "Kumar", "Laravel", "lastfile", "lenspalette", "lintstagedrc", "livescript", "loaderror", "Localname", "Logi", "logopath", "Logtalk", "lolcode", "Lucene", "lync", "markdownlint", "markojs", "mathml", "maxscript", "menubutton", "MIXAL", "mjml", "mlang", "modeassoc", "modelist", "mojolicious", "moleculer", "moveline", "movelinesdown", "movelinesup", "mson", "NAVIGATIONHIDDEN", "nestjs", "njsproj", "nodemon", "NOKEYS", "nord", "Noto", "NOTOUCH", "NSIS", "nsri", "nunjs", "nunjucks", "nuxt", "objectivecpp", "OLED", "oncanrun", "onchangemode", "onchangesession", "ondisconnect", "onenote", "onexpand", "onfold", "onhide", "onloaderror", "onpurchase", "onref", "onremove", "onrename", "onrun", "onsave", "onscrollend", "onscrollleft", "onscrolltop", "onsearch", "onwilldisconnect", "onwriteend", "opencl", "openfolder", "opengl", "orangered", "ovpn", "paket", "partiql", "PERSISTABLE", "pgsql", "photoshop", "phpcsfixer", "phps", "phpt", "phpunit", "phraseapp", "plantuml", "platformio", "Plore", "plsql", "poedit", "Portugues", "postcss", "postcssconfig", "postcssrc", "powersave", "Praat", "praatscript", "precommit", "prefs", "processinglang", "procfile", "Proggy", "protobuf", "protobug", "PRQL", "purescript", "pyret", "pyup", "qlikview", "qmldir", "qsharp", "QUICKTOOLS", "rakefile", "Raku", "rakumod", "rakutest", "raml", "rateapp", "reactjs", "reacttemplate", "reactts", "recents", "rehype", "remotefile", "removeads", "retext", "rhtml", "rnfr", "rnto", "robotframework", "Roboto", "rollup", "rproj", "rubocop", "ruleset", "saltstack", "scilab", "sconsole", "SCROLLBARS", "scrollleft", "scrolltop", "sdcard", "sdlang", "selectall", "sequelize", "settitle", "shft", "showconsole", "silverstripe", "smali", "snapcraft", "snyk", "SPARQL", "sqlserver", "squarebracket", "stargrade", "stata", "styl", "stylable", "stylelint", "symfony", "systemverilog", "Tahoma", "tera", "Termux", "terragrunt", "testjs", "testts", "tfvars", "themelist", "ttcn", "typescriptdef", "ungap", "unibeautify", "unwatch", "vala", "vapi", "vash", "vbhtml", "Verdana", "verilog", "vhdl", "Visualforce", "vsix", "vsixmanifest", "warningreport", "watchmanconfig", "webp", "wercker", "Wollok", "wpgm", "wpml", "wtest", "wxml", "wxss", "xquery", "Zeek" ], "[javascript]": { "editor.defaultFormatter": "biomejs.biome" } } ================================================ FILE: .vscode/typings/cordova/cordova.d.ts ================================================ // Type definitions for Apache Cordova // Project: http://cordova.apache.org // Definitions by: Microsoft Open Technologies Inc. // Definitions: https://github.com/borisyankov/DefinitelyTyped // // Copyright (c) Microsoft Open Technologies, Inc. // Licensed under the MIT license. interface Cordova { /** Invokes native functionality by specifying corresponding service name, action and optional parameters. * @param success A success callback function. * @param fail An error callback function. * @param service The service name to call on the native side (corresponds to a native class). * @param action The action name to call on the native side (generally corresponds to the native class method). * @param args An array of arguments to pass into the native environment. */ exec(success: () => any, fail: () => any, service: string, action: string, args?: string[]): void; /** Gets the operating system name. */ platformId: string; /** Gets Cordova framework version */ version: string; /** Defines custom logic as a Cordova module. Other modules can later access it using module name provided. */ define(moduleName: string, factory: (require: any, exports: any, module: any) => any): void; /** Access a Cordova module by name. */ require(moduleName: string): any; /** Namespace for Cordova plugin functionality */ plugins:CordovaPlugins; } interface CordovaPlugins {} interface Document { addEventListener(type: "deviceready", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "pause", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "resume", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "backbutton", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "menubutton", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "searchbutton", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "startcallbutton", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "endcallbutton", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "volumedownbutton", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: "volumeupbutton", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "deviceready", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "pause", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "resume", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "backbutton", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "menubutton", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "searchbutton", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "startcallbutton", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "endcallbutton", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "volumedownbutton", listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: "volumeupbutton", listener: (ev: Event) => any, useCapture?: boolean): void; addEventListener(type: string, listener: (ev: Event) => any, useCapture?: boolean): void; removeEventListener(type: string, listener: (ev: Event) => any, useCapture?: boolean): void; } interface Window { cordova:Cordova; } // cordova/argscheck module interface ArgsCheck { checkArgs(argsSpec: string, functionName: string, args: any[], callee?: any): void; getValue(value?: any, defaultValue?: any): any; enableChecks: boolean; } // cordova/urlutil module interface UrlUtil { makeAbsolute(url: string): string } /** Apache Cordova instance */ declare var cordova: Cordova; ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## v1.11.8 (967) ### Features * feat: add acode cli by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1768 * feat: Add FileTree component with virtual scrolling by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1774 * feat(welcome): add welcome tab for first-time users by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1783 * feat: Add visibility toggle for quicktools toggler on terminal input by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1766 * feat: Add smart path shortening to terminal prompt by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1767 * feat(settings): add option to preserve original item order by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1772 * feat: add "Open in Terminal" option to folder context menu by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1809 * feat: add proper fallback and saf uri handling for ischeck by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1814 * feat: extensionless URL resolution in preview server by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1824 * feat: improve DevContainer and Docker support for easier dev setup by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1823 * feat: add eruda devtools for debugging by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1831 * feat: Refactor li-icon set and add new icon by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1759 * feat: Enhance `addIcon` to support svg currentColor by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1760 * feat: Offload file change detection to background thread by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1802 * feat: move service to background by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1754 * feat: add quiet hours for ads by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1779 * feat(auth): move authentication to native Android plugin with encrypted storage by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1794 * feat: do not close milestone issues by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1799 * feat: close search dialog with ctrl+f by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1807 * feat: added executor tests by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1816 * Added tests by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1813 * CustomTabs web api for plugins by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1785 * Implement plugin loading callbacks and tracking for installed plugins by @7HR4IZ3 in https://github.com/Acode-Foundation/Acode/pull/1558 * Added Documents viewer plugin id to load pdf/excel/csv etc. files by @hackesofice in https://github.com/Acode-Foundation/Acode/pull/1833 * Refactor quicktools settings by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1761 ### Fixes * fix: use correct timezone by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1716 * fix: sanitize plugin readme before rendering by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1731 * fix: make rm wrapper silent by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1727 * fix: remove duplicate rm command by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1732 * fix: sidebar app icon size and scrolling by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1733 * fix: terminal bug by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1741 * fix: sidebar apps active and removal things with fallback by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1749 * fix: improve the backup and restore by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1744 * fix: console by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1763 * fix(terminal): cleanup broken tabs and show alerts on terminal errors by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1780 * fix(plugin): sanitize markdown and prevent horizontal overflow by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1782 * fix: run current File not respecting `preview mode` option. by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1805 * fix: memory leak when toggling search panel by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1808 * fix: prevent mixed content in SFTP/FTP cached files by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1815 * Fix: Remove trimming of matched content in Executor.js by @dikidjatar in https://github.com/Acode-Foundation/Acode/pull/1798 * revert: relative http paths by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1743 ### Chore & CI * Add F-droid Build in GH nightly releases by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1719 * fix(CI): normal nightly & fdroid flavor build paths. by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1722 * Update devcontainer.json and add Dependabot for devcontainer's version updates by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1742 * enhance nightly build workflow by adding a step to generate release notes by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1747 * chore: update nightly build workflow to use a fine-grained GITHUB_TOKEN for generating nightly release notes by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1751 * chore: message type detection in release notes script by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1786 * update(sftp): dependencies for SFTP plugin by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1792 * chore(deps): bump tmp and cordova by @dependabot[bot] in https://github.com/Acode-Foundation/Acode/pull/1720 * chore(deps): bump tough-cookie and cordova by @dependabot[bot] in https://github.com/Acode-Foundation/Acode/pull/1721 * chore(deps): bump systeminformation from 5.27.11 to 5.27.14 by @dependabot[bot] in https://github.com/Acode-Foundation/Acode/pull/1750 * chore(deps): bump tar from 7.5.2 to 7.5.3 by @dependabot[bot] in https://github.com/Acode-Foundation/Acode/pull/1829 ### Translations & i18n * `pl-pl.json` by @andrewczm in https://github.com/Acode-Foundation/Acode/pull/1709 * `de-de.json` by @Mr-Update in https://github.com/Acode-Foundation/Acode/pull/1714, https://github.com/Acode-Foundation/Acode/pull/1764 * `cs-cz.json` by @Amereyeu in https://github.com/Acode-Foundation/Acode/pull/1752, https://github.com/Acode-Foundation/Acode/pull/1828 * `hu-hu.json` by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1755, https://github.com/Acode-Foundation/Acode/pull/1762, https://github.com/Acode-Foundation/Acode/pull/1787, https://github.com/Acode-Foundation/Acode/pull/1810, https://github.com/Acode-Foundation/Acode/pull/1834 * `zh-cn.json`, `zh-hant.json` by @LaunchLee in https://github.com/Acode-Foundation/Acode/pull/1769 * `pt-br.json` by @sebastianjnuwu in https://github.com/Acode-Foundation/Acode/pull/1775 * `uk-ua.json` by @PavloPogonets in https://github.com/Acode-Foundation/Acode/pull/1818, https://github.com/Acode-Foundation/Acode/pull/1821 ## v1.11.7 (966) * revert: sidebar/style.scss changes to fix collapse folders by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1572 * Terminal Service by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1570 * fix(i18n): fix typo in ./src/lang/id-id.json by @hyperz111 in https://github.com/Acode-Foundation/Acode/pull/1577 * fix: browser download by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1587 * Terminal initrc support by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1590 * chore(i18n): update de-de.json by @Mr-Update in https://github.com/Acode-Foundation/Acode/pull/1600 * Updated & Added Missing ar-ye.json (Arabic) translations by @Hussain96o in https://github.com/Acode-Foundation/Acode/pull/1601 * Restore terminal tabs by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1592 - fix: terminal font issue - Breaking change: Changed default terminal configs * feat: service on/off by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1602 * fix: service stop on app exit by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1603 * fix: ED25519 SSH keys not working by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1595 * CI: Nightly Builds by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1612 * style(terminal): Some touch selection handle enhancements by @peasneovoyager2banana2 in https://github.com/Acode-Foundation/Acode/pull/1611 * fix: pip by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1617 * feat: Enable all file access in nightly builds by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1618 * Add support for renaming documents in provider by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1619 * fix: support for Acode terminal SAF URIs by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1621 * fix: rm command by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1623 * feat: add 'check for app updates' setting to toggle the automatic behaviour by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1624 * chore(i18n): update hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1626 * Enforce Unix lf endings for shell scripts by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1637 * AutoSuggest install command in terminal by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1638 * feat: add plugin filtering by author and keywords by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1625 - Refactored filtering logic to support multi-page results and improved UI feedback for filter actions. * Translation: Update hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1640 * fix: init-alpine by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1641 * fix: ANSI escape sequence in init-alpine by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1643 * chore(i18n): update de-de.json by @Mr-Update in https://github.com/Acode-Foundation/Acode/pull/1648 * fix: update proot binaries to support 16kb page size by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1649 * chore(i18n): update id-id.json with new strings by @hyperz111 in https://github.com/Acode-Foundation/Acode/pull/1650 * Translation: Update hu-Hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1653 * Add confirmation prompt before closing terminal tabs by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1655 * fix: compatibility for old android versions by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1656 * fix: improve file sharing and URI handling by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1662 - Improved file sharing and fixed permission issue (also in case of open with , edit with) * fix: do not restore terminals if axs is dead by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1664 * fix: .capitalize() removed because it changes the translations (also English) by @Mr-Update in https://github.com/Acode-Foundation/Acode/pull/1665 * fix: `switchFile` api to respect custom subtitle by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1672 * Update zh-cn.json and zh-hant.json by @LaunchLee in https://github.com/Acode-Foundation/Acode/pull/1674 * fix: Translation corrected in terminal settings by @Mr-Update in https://github.com/Acode-Foundation/Acode/pull/1676 * fix: Added missing translation for info window in file browser and app settings. by @Mr-Update in https://github.com/Acode-Foundation/Acode/pull/1677 * Translation: Update hungarian hu-HU.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1680 * Update ads plugin and fix some issues of free version by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1683 - fix: system them on free version * fix: restore folds when formatting if available by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1682 * fix: Added missing translation for info window in terminal settings by @Mr-Update in https://github.com/Acode-Foundation/Acode/pull/1681 * Translation: Update hungarian hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1687 * feat: Add clean install state functionality to app settings by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1690 * Translation: Update hungarian hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1693 ## v1.11.6 (965) * fix: Terminal in F-Droid flavour by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1478 * feat: update target SDK to API 35 and fix edge-to-edge compatibility by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1486 * improve German translation by @brian200508 in https://github.com/Acode-Foundation/Acode/pull/1487 * Translation: Update hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1489 * chore(update IAP library to lateset version) by @deadlyjack in https://github.com/Acode-Foundation/Acode/pull/1405 * feat: font manager ui for managing custom fonts by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1491 * feat: add package updated time display on plugin page by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1494 * chore: update id-id.json by @hyperz111 in https://github.com/Acode-Foundation/Acode/pull/1495 * fix: document provider by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1497 * feat(sponsor page) by @deadlyjack in https://github.com/Acode-Foundation/Acode/pull/1496 * fix: load custom fonts on restart by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1499 * Update hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1498 * fix: resolve GitHub URI preview issues in server by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1501 * fix: plugins page scrolling by @deadlyjack in https://github.com/Acode-Foundation/Acode/pull/1505 * [chore] bump workflow dependencies to latest versions by @Jvr2022 in https://github.com/Acode-Foundation/Acode/pull/1506 * fix: load base app stylesheet in custom editor tab by @deadlyjack in https://github.com/Acode-Foundation/Acode/pull/1507 * update src/lang/pt-br.json by @sebastianjnuwu in https://github.com/Acode-Foundation/Acode/pull/1510 * Add code formatter keybind by @UnschooledGamer in https://github.com/Acode-Foundation/Acode/pull/1511 * Added hyphen in quicktools and more strings for i18n by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1516 * chore(i18n): update id-id.json with new strings by @hyperz111 in https://github.com/Acode-Foundation/Acode/pull/1521 * chore(i18n): update hu-hu.json with new strings by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1520 * Add/Update Bengali (bn-BD) Translation File by @thebadhonbiswas in https://github.com/Acode-Foundation/Acode/pull/1522 * feat: take confirmation before uninstalling terminal by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1528 * fix ui issue by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1527 * fix: disable changing editor theme when system is selected by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1529 * fix: svg render issue by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1530 * fix: encoding detection by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1533 * fix: big screen ui and global search worker issue by @deadlyjack in https://github.com/Acode-Foundation/Acode/pull/1534 * fix: improved terminal mounts by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1537 * chore(i18n): refine Russian (ru-ru.json) localization with updated and new strings by @Nein-Ich-wurde-Gewinnen in https://github.com/Acode-Foundation/Acode/pull/1541 * Fixed Plugin installation issues(in some cases) by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1546 * feat: add option to auto detect encoding by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1547 * fix: ftp infinite loading by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1552 * fix: tab active state when switching tab with api by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1557 * fix: resize of terminal and some small patches by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1562 * fix: fdroid builds by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1565 ## v1.11.5 (963) * Alpine Linux Backend by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1401 * fix: html escaping for console page by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1411 * Create plugin for native libs by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1413 * feat: terminal frontend by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1415 * Update hu-hu.json translation by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1423, https://github.com/Acode-Foundation/Acode/pull/1425, https://github.com/Acode-Foundation/Acode/pull/1440 * chore: update id-id.json translation by @hyperz111 in https://github.com/Acode-Foundation/Acode/pull/1424, https://github.com/Acode-Foundation/Acode/pull/1447 * All file access permission for terminal by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1426 * refactor: plugins page by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1428 * Fixed infinite scrolling in case of filtering on both plugin page and sidebar * Ui improvements on plugin page * Revamped few themes(fixes visibility issue) and Added few new ones by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1430 * New theme: Neon, Sunset, Glass, Glass Dark * Revamped themes: System (Dark & Light), Oled, Light * System theme option is available on free too. * fix: scaling issue by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1431 * fix: quicktools toggler responsiveness(scaling) and migrate biome by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1442 * chore: update ace to v1.43.2 by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1443 * feat: add uninstall option in terminal setting and tab subtitle fixes by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1445 * feat: alpine document provider(exposes terminal public dir to saf picker) by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1444 * feat: improved logging + fix terminal installation failure by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1446 * some terminal related quality improvements by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1449 * fix: dont start selection in case of back gesture * remove useless border-radius from terminal container * fix: improve terminal cursor positioning for mobile keyboard events * fix: update terminal container background when theme changes * exclude alpine/public while backup by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1450 * Custom port support in Terminal manager by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1459 * 🌐 i18n(locale): update vietnamese translations by @Nekitori17 in https://github.com/Acode-Foundation/Acode/pull/1461 * fix: infinite loading when previewing by @RohitKushvaha01 in https://github.com/Acode-Foundation/Acode/pull/1460 * Feat/some improvements to terminal and its api by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1463 * Fixes/sidebar extension state by clearing and active autocompletion box background by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1466 * feat: add terminal like search and replace history by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1467 * fix: theme issue in terminal context menu * fix: prevent folder paste loops and improve SAF handling(termux) in cut operation by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1474 - Add validation to prevent pasting folders into themselves or subdirectories - Implement special Termux SAF handling for cut operations with recursive file/folder moving - Fix createFileStructure to handle nested paths (foo/bar) for file:// URIs ## v1.11.4 (962) * Fix renaming current Termux URI file whilst viewing/editing it by @peasneovoyager2banana2 * fix: html preview on unsaved files by @RohitKushvaha01 * Add markdown-it-footnote and task lists support by @UnschooledGamer * Fix quickTools height fallback by @bajrangCoder * Comment out column adjustment in touchHandler.js for weird selection by @bajrangCoder * Update Indonesian Translation by @hyperz111 * feat: :sparkles: Native Websocket Plugin (uses okhttp) by @UnschooledGamer * fix: unintentional scrolling of tab container while dragging a tab by @peasneovoyager2banana2 * feat. alert user when changing editor theme by @RohitKushvaha01 * feat: Improve handling of intents before files are restored on startup by @bajrangCoder * fix: don't save state of SAFMode="single" files by @bajrangCoder * fix: plugin passing undefined to plugin item on plugin page by @bajrangCoder * Integrated cordova-plugin-buildinfo as a local plugin by @RohitKushvaha01 * fix: crash when running unsaved file by @RohitKushvaha01 * feat: show open source plugin url on plugin page by @bajrangCoder * fix: Make "None" option clickable in select dialog on formatter settings by @bajrangCoder * fix: show confirm when copy/cut in sidebar instead of alert so user can proceed if they want by @bajrangCoder * feat: plugin enable/disable functionality and update translations by @UnschooledGamer * Update hu-hu.json by @summoner001 * Advanced execution interface by @RohitKushvaha01 ## v1.11.3 (959) * feat: added system theme by @RohitKushvaha01 * add: close-inactive-issues.yml by @UnschooledGamer * Added Executor by @RohitKushvaha01 * fix: Executor Plugin's js module/interface path by @UnschooledGamer * fix: teardrop goes out of viewport when there is no gutter by @bajrangCoder * fix: infinite loading screen due to executor by @RohitKushvaha01 * fix: infinite loading when previewing html files by @RohitKushvaha01 * fix: apk related issues ## v1.11.2 (958) * fix: cors related issues when installing plugins from remote by @bajrangCoder * fix: Acode ignoring main, readme and icon fields in plugin manifest by @alMukaafih * feat: inapp acode account login by @bajrangCoder * fix: launchApp function by @RohitKushvaha01 * Update plugin documentation url by @RohitKushvaha01 * Backup restore fixes by @bajrangCoder * file: re-emit switch and load file events after plugin load by @bajrangCoder * fix: File copy-paste retains paste option after file is copied. by @RohitKushvaha01 * Fix sftp sidebar UI issue by @bajrangCoder * fix: http relative path by @RohitKushvaha01 * Download support by @RohitKushvaha01 * Add Custom File Type Handler API by @bajrangCoder * fix: applying folds on reopening the app by @bajrangCoder * feat(tabs): Implement Shadow DOM isolation for non-editor tabs by @bajrangCoder * remove invalid font by @bajrangCoder * Improve Indonesian Translation by @hyperz111 * feat: improve changelogs page by @bajrangCoder * Refactor, feat: Select dialog by @overskul * Fix: Translate "Donation Message" and some words by @hyperz111 * Fix: "File Not Found" error when previewing HTML files from a Termux directory by @RohitKushvaha01 * Update zh-cn.json and zh-hant.json by @LaunchLee * fix: console not showing on unsaved html file by @RohitKushvaha01 * File Menu & QuickTools Visibility for editor tabs by @bajrangCoder * fix: handle edge case for `hideQuickTools` property by @bajrangCoder * feat: add more new keys(Home,End, PageUp, PageDown, Delete) and symbols(`~` `Backtick`,`#` ,`$` ,`%` ,`^`) for quicktools by @bajrangCoder * Fixed select box issue and improved it by @bajrangCoder * Added `x` to delete recent files/folder from dialog * Update Hungarian translation by @summoner001 * chore(i18n): update vi-vn.json by @Nekitori17 * fix: file not found error by @RohitKushvaha01 * feat: revamped file tree by @bajrangCoder * fix: pagedown key issue in editor by @bajrangCoder * feat: open files with arbitrary extension names by @RohitKushvaha01 * add Hebrew language by @elid34 * feat: add support for compound file paths like `.blade.php` by @bajrangCoder * support string content in tabs by @overskul * update Tagalog/Filipino language by @ychwah * fix: keyboard shortcuts leaks into ace editor by @RohitKushvaha01 * Resizeable activity by @RohitKushvaha01 * update ace v1.41.0 by @bajrangCoder * Fix html content access by @RohitKushvaha01 * fix: path overlap issue in html viewer by @RohitKushvaha01 * fix: only initiate iap stuff in case of paid plugin on sidebar by @bajrangCoder ## v1.11.1 (957) ### Features - **Syntax Highlighting**: Added syntax highlighting for code blocks on the plugin page using Ace Editor. - **Theme Settings Fallback**: Implemented a fallback mechanism on the theme settings page when a custom tab is opened. - **QuickTools Scroll Wheel Support**: Added support for scroll wheel events in QuickTools when in click mode. - **SFTP Improvements**: - Enhanced logging for better debugging. - Enabled Bouncy Castle for SFTP, resolving connection issues with certain key types. - **App Update Mechanism**: - Now checks for updates after app startup. - Uses a native approach instead of traditional fetch. ### Fixes & Improvements - **SD Card Plugin**: Handled a few edge cases to prevent crashes. - **GitHub Plugin Palette Fix**: Resolved an issue where the color palette was breaking the GitHub plugin. - **Plugin Installation Fixes**: - Updated the install button in the sidebar to prevent multiple installations. - Fixed the installation mechanism in both the sidebar and the `installPlugin` API. - **Quality of Life Enhancements**: - Various small improvements when installing/uninstalling plugins from the sidebar. #### ⚠️ Experimental Changes ⚠️ - **Improved Plugin Loading**: - Only **theme plugins** load on Acode startup for faster performance. - All other plugins load **after** Acode startup. - **Important for Theme Developers**: - Ensure your **plugin ID includes the word "theme"** to be correctly recognized as a theme plugin. - No changes are needed for existing theme plugins. - **Potential Issues**: Since this is an experimental change, some features may break. Please report any issues encountered. ## v1.11.0 (956) ### Fixes * Fixed a typo: "vission" → "vision" by @ByteJoseph in #1125 * Fixed heading and image alignment issues with `alignment="center"` on the plugin page by @bajrangCoder in #1132 * Fixed file listing in SFTP by @bajrangCoder in #1133 * Fixed fallback to `*/*` when the `accept` attribute is absent in `` in the in-app browser by @bajrangCoder in #1154 * Fixed console not reappearing after page reload in the in-app browser by @bajrangCoder in #1155 * Fixed infinite scroll on the plugin page to remove duplicates by @bajrangCoder in #1171 * Fixed logger to limit its size in #1167 * Fixed an issue where the info dialog wouldn't appear for non-editor tabs in #1167 * Fixed incorrect file attributes in FTP by @bajrangCoder in #1194 * Fixed the palette not opening when triggered from an existing palette by @bajrangCoder in #1197 * Fixed triggering of infinite scroll on plugin page while searching by @bajrangCoder in #1200 ### Features * Improved tab view gesture handling to distinguish between scroll and swipe on the plugin page by @bajrangCoder in #1131 * Added color preview for SVG files by @bajrangCoder in #1135 * Implemented custom editor tab support by @bajrangCoder in #1136 * Now supports image, video, and audio previews directly in the editor instead of pop-ups * Exposed API for plugin developers to add content in editor tabs via `EditorFile` * Redesigned the About page by @bajrangCoder in #1153 * Added a plugin rebuild option for local plugins by @bajrangCoder in #1150 * Plugins can use `cordova.plugin.http` instead of `fetch` to avoid CORS issues when making network requests * Redesigned the Plugin page by @bajrangCoder in #1152 * Added new metadata fields in `plugin.json`: `license`, `changelog`, `keywords`, and `contributors` * Added a new option to open files in an external app from the file browser in #1163 * Added minor sidebar UI tweaks and improved input element styling in #1164 * Used theme colors in the extra cutout area in landscape mode instead of the default color in #1165 * Improved file info dialog design in #1170 * Added Eruda console support for external sites in Acode's built-in browser in #1172 * Added a new editor setting: **"Fade fold widgets"** by @bajrangCoder in #1195 * Added a command to change the editor and app theme via the command palette by @bajrangCoder in #1197 * Added option to install plugins directly from sidebar extensions app by @bajrangCoder in #1200 ### Improvements * Improved paste operation with proper error handling in #1162 * Rewritten SFTP implementation(#1167): * Better performance and stability * Improved symlink support with better visual distinction * Proper error handling * Enhanced file reading (downloading) and writing * Uses native APIs for file system operations * Better buffer handling—now even 30-40 minute videos load within seconds (faster than internal storage) * Emoji support added * Improved the FTP client (#1193) by @bajrangCoder * Supports emoji-named files * Improved symlink handling * Better handling of videos and images as binary files * Displays file names instead of full paths * Reworked the plugin page UI—now displays essential info such as author, license, price, etc., in the list view by @bajrangCoder in #1196 * Tweaked breadcrumbs in the file browser to follow the app theme in #1167 * Updated the SSH library to `v3.1.2` in #1167 * Removed deprecated APIs in #1167 * Added experimental support for saving native logs in Acode Logger in #1167 * Tweaked the donation page #1188 ### Other Changes * **Chore**: Updated Ace Editor to `v1.39.0` * Added CSV & TSV mode * Improved search support for multi-line patterns (`\n`, `\t`) * And more—see the Ace Changelog * Many translation updates for `hu-hu` by @summoner ## v1.10.7 (955) * Update hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1092 * build(deps): bump path-to-regexp and express by @dependabot in https://github.com/Acode-Foundation/Acode/pull/1093 * Update hu-hu.json by @summoner001 in https://github.com/Acode-Foundation/Acode/pull/1094 * Exclude python `venv` and php `vendor` folders to speedup startup by @Jobians in https://github.com/Acode-Foundation/Acode/pull/1096 * Update de-de.json to 1.10.6.954 by @Micha-he in https://github.com/Acode-Foundation/Acode/pull/1097 * Update pl-pl.json by @andrewczm in https://github.com/Acode-Foundation/Acode/pull/1099 * minor fixes and tweaks by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1103 * Added new command(file explorer) to open file manager * Added copy button to code blocks on plugin page * some error logging for debugging * few markdown tweaks * Updating Brazilian Portuguese language by @sebastianjnuwu in https://github.com/Acode-Foundation/Acode/pull/1102 * add className by @NezitX in https://github.com/Acode-Foundation/Acode/pull/1104 * Add monochrome icon by @Npepperlinux in https://github.com/Acode-Foundation/Acode/pull/1108 * fix: issue while making request to any ipv4, etc from app by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1107 * Update zh-cn.json and zh-hant.json by @LaunchLee in https://github.com/Acode-Foundation/Acode/pull/1109 * fix: show "No matches found" when palette has no matching items by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1110 * build(deps): bump systeminformation from 5.21.8 to 5.23.14 by @dependabot in https://github.com/Acode-Foundation/Acode/pull/1111 * Follow up for #1110 by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1112 * fix: cleanup plugin install state on failed installations by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1113 * feat: implement infinite scroll for plugin list by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1114 * feat: add quick install button for plugins in sidebar by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1115 * Plugin Dependencies by @alMukaafih in https://github.com/Acode-Foundation/Acode/pull/1030 * updated install state to reflect fs behaviour of android by @alMukaafih in https://github.com/Acode-Foundation/Acode/pull/1118 * fix: clean broken or half installed plugins on start by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1121 * few improvements by @bajrangCoder in https://github.com/Acode-Foundation/Acode/pull/1122 ## v1.10.6 (954) ### New Features - **Install State for Plugins**: Added an install state to improve plugin updates (#1026) by @alMukaafih, further enhanced by @bajrangCoder. - **Selection Mode in File Browser**: Introduced a selection mode in the file browser by @bajrangCoder. - **Persistent Notification System**: Added a persistent notification system with toast notifications by @bajrangCoder. - **In-App Browser Command**: Added a command to open an in-app browser with a given URL by @angeloyana. - **Font Size Shortcut Keys**: Added new key shortcuts for changing font size by @bajrangCoder: - Increase Font Size: `Ctrl - +` or `Ctrl - =` - Decrease Font Size: `Ctrl + -` or `Ctrl - _` - **Command Palette Enhancements**: - "Open Plugin Page" command for quick access to plugin pages, especially for keyboard users. - "Copy Device Info" command to share device information for troubleshooting. - **GitHub Alert Support**: Added GitHub alert support in plugin descriptions by @bajrangCoder. - **File Tab Drop Behavior**: Dropping a file tab into any input or editor now inserts its path by @bajrangCoder. - **File Browser Context Menu**: Added a "Copy URI" option in the file browser context menu by @bajrangCoder. - **Project Import as ZIP**: Added the option to import projects as ZIP files by @bajrangCoder. - **Backup Plugins**: You can now back up plugins, whether they are paid or free by @bajrangCoder. - **Task List Markdown Support**: Added support for task lists (`- [x]`) in the plugin page markdown by @bajrangCoder. - **Install as Plugin for ZIP Files**: Added the "Install as Plugin" option in the sidebar files section for ZIP files containing a `plugin.json` in the root directory by @bajrangCoder. - **`acode.installPlugin` API**: Introduced an API for plugins to install other plugins with user consent by @bajrangCoder(available from versionCode: `954`). - **View Changelogs in Settings**: Added an option in the settings page to view changelogs directly inside the app by @bajrangCoder. - **App Update Checker**: Implemented an app update checker that runs on startup by @bajrangCoder. - feat: copy/cut current line when selection is empty with same key shortcut by @angeloyana - feat: add symlinks support in SFTP by @bajrangCoder ### Fixes - **Plugin Loading Failures**: Improved handling of plugin loading failures by @bajrangCoder: - Prevents the app from crashing when plugins fail to load. - Shows user feedback for each failed plugin while continuing to load others. - **Internal URL Navigation**: Replaced browser navigation with scroll behavior for internal links in plugin descriptions. Links to the same markdown now scroll instead of opening a browser by @bajrangCoder. - **Pro Version Ads Issue**: Fixed an issue where plugin context menu actions displayed ads even if the Pro version was purchased by @UnschooledGamer. - **Termux Private URI Operations**: Resolved issues with deleting folders/files and renaming files in Termux private URI file systems. - **Logger Enhancements**: Improved the logger to automatically detect failures and use `console.error` in Acode. - **Preview and Server Port Issue**: Fixed an issue where the browser did not open on the given port when the preview port and server port differed. - **Font Persistence**: Resolved an issue where fonts did not persist after restarting the app. - **Markdown Linking**: Fixed issues with linking to headings within the same page in markdown. - **Search Bar in File Browser**: Fixed a bug where the search bar in the file browser would get stuck and become unclosable. - **Theme Page Issues**: Addressed issues with theme plugins, including preview rendering and checkbox state changes. - **Formatter Mode Selection**: Fixed the formatter ignoring the selected mode for files by @alMukaafih. - fix: fetch plugin updates even if any fails by @UnschooledGamer - fix: plugin update page by removing unwanted option icon @bajrangCoder ### Others - **Plugin Refactor**: Migrated the old plugin update icon to a new toast-like notification widget. ### Translators - Updated translations for specific languages contributed by: - **@Micha-he**: `de-de.json` - **@LaunchLee**: `zh-cn.json` and `zh-hant.json` - **@andrewczm**: `pl-pl.json` - **@Nekitori17**: `vi-vn` - **@s4ntiksu**: `ru-ru.json` - **@summoner001**: `hu-hu.json` - **@antikruk**: `be-by.json` --- ## [1.10.5] (953) - New - Plugin Filtering System in #1011 - feat: Add more action menu in sidebar plugin in #1017 - Implement Logger system in #1020 - Feat: Use Current File for preview (Toggle option) in #1006 - updated ace editor to v1.36.2 in #1025 - Fixes - Update de-de.json in #1039 - fixed sidebar plugin search list scrolling in #1002 - Improve zh-TW traditional Chinese translation in #1004 - fix: Fixed save all changes option in #1010 - chore(i18n): vi-vn in #1016 - removed auto paste of url on plugin page in #1023 - fixed weird spacing issue on header #925 in #1024 - Update zh-cn.json and zh-hant.json in #1031 - Refactor Iap.java: Use WeakReference for Context and Activity to prevent memory leaks in #1040 - Updated Polish translation in #1043 - ru-ru improved in #1041 - Update pl-pl.json in #1044 & #1045 - fix: termux related fs operations failure in #1046 ## [1.10.4] (952) - New - Nested Files/Folder Creation - Updated ace to latest version - Improved displaying of Download count on Plugins page as well as on Sidebar - Enhanced search functionality to allow searching across all available plugins from the "all" section of the plugin page. - Added a new option on Help page for submitting bug reports. - Fixes - Fixed issue with the search bar on the plugin page - Fixed issue with the search bar closing accidentally when clicking elsewhere on the screen ## [1.10.2] - New - [x] **Updated Ace editor** | 947 - Updated Ace editor to version 1.33.1. ## [1.10.1] - New - [x] **Updated Ace editor** | 946 - Updated Ace editor to version 1.32.9 - Fixes - [x] **Scrolling** | 946 - Fixed scrollbars not showing up properly. ## [1.10.0] - New - [x] **Ace editor** | 937 - Updated Ace editor to version 1.32.7 - Fixes - [x] **UI** | 944 - Fixed status and navigation text color not visible in light theme. - [x] **Plugin** | 944 - Fixed updates for plugin not showing up. - [x] **Markdown** | 945 - Fixed markdown preview not working properly. - [x] **Text selection** | 937 - Fixed context menu not showing up when selecting all text from context menu after click and hold. ## [1.9.0] - New - [x] **New in app browser** | 324 - New in app browser with new UI. - You can emulate multiple devices. - [x] **New mode** | 933 - Zig mode - Astro mode - Fixes - [x] **File** | 931 - Fixed not able share, edit or open with other apps. - Fixed file rename not working properly. | 934 - [x] **Preview** | 934 - Fixed preview where it opens browser two times. ## [1.8.8] - New - [x] **Natural Scrolling** | 331 - Acode now uses natural scrolling. - Fixes - [x] **Selecting text** | 331 - Fixed an issue where selecting text using long press and adding text to selection behaves unexpectedly. - [x] **Open folders** | 331 - Fixed an issue where removing folder from sidebar doesn't remove the selected folder. ## [1.8.7] - New - [x] **Updated Ace editor** | 318 - Updated Ace editor to version 1.28.0 - [x] **New problems page** | 318 - You can now see all the problems in one place. - You can also see the problems in opened file. - [x] **New settings** | 318 - New settings to toggle side buttons. - [x] **New Plugin API** | 318 - `SideButton` is a new component that can be used to add side buttons. - [x] **New theme color** | 318 - New `danger-color` and `danger-text-color` theme color. - [x] **New key binding** | 318 - Use `Ctrl+Shift+X` key binding to open problems page. - [x] **Plugin** | 320 - Install plugin directly from browser. - [x] **Intent** | 323 - Plugin has now API to handle intent with uri acode://module/action/value. - Fixes - [x] **Plugin page** | 318 - Improved plugin page UI. - Shows plugin quickly when opened and loads new information in background. - [x] **Unsaved changes** | 318 - Fixed unsaved changes not showing up in file when app restarted. - [x] **Quicktools** | 319 - Fixed quicktools slides back when touch moved slightly. - [x] **Settings** | 321 - Fixed settings not saving properly. - [x] **Internal storage** | 322 - Fixed renaming file. - [x] **Side buttons** | 323 - Fixed side buttons not shown properly. - [x] **Open folders** | 330 - Fixed move file/folder not working properly. - [x] **Editor** | 330 - Improved scrolling. - [x] **Quicktools** | 330 - Improved quicktools. ## [1.8.6] - Build 313 - New - [x] **Search in settings** - You can now search for any settings in settings page. - Updates - [x] **Language** - Updated language pack for Russian, Spanish, Portuguese and Deutsche. - [x] **Updated Ace editor** - Updated Ace editor to version 1.5.0 - Fixes - [x] **Sidebar search** - Fixed sidebar search not rendering words with special characters. - [x] **Not Loading** - Fixed app not loading on older devices. - [x] **Scrolling** - Fixed scrolling not working properly on some devices. - [x] **Eruda console** - Fixed eruda console not working properly. - [x] **Scrollbar** - Fixed scrollbar not working properly. - [x] **Custom theme** - Fixed custom theme not working properly. ## [1.8.5] - Build 295 - New - [x] **Scroll speed** - New 'Fast x2' scroll speed. - [x] **Touch handling** - Prevent accidental touch when tapping tear drop. - [x] **Color Preview** - You can now see color preview in css, scss, less, stylus and sass codes. - No need to select the whole color. - Enable/disable this feature from editor settings. - [x] **Smaller app size** - Reduced app size. - [x] **Updated icon pack** - Updated icon pack (mono color). - [x] **Default file encoding** - You can set default file encoding from settings. - [x] **File encoding** - Remember file encoding for each file. - [x] **Sidebar apps** - File list and extension list now remembers scroll position. - [x] **File tab bar** - When repositioning file tab bar, tab container will scroll when current tab is at the edge. - Fixes - [x] **Touch handling** - Fixed teardrop and text menu not updated when switching tabs. - [x] **File encoding** - Fixed file encoding not working properly. - [x] **File icon** - Fixed inconsistent file icon. - [x] **JavaScript console** - Fixed JavaScript console not opening. - [x] **Ads** - Fixed ads taking screen when keyboard is open. - [x] **Insert file** - Fixed 'insert file' in opened folder not working properly. ## [1.8.4] - Build 283 - New - [x] **Updated Ace editor** - Updated Ace editor to version 1.22.0 - [x] **Open files position** - **Bottom** `beta`: This is new option for open files position. You can change this from settings. This will open files in bottom of the screen. - [x] **Search in All Files** `beta` - This feature can be used to search and replace in all files in the opened projects. - To use this feature, open side bar and click on the search icon. - Note: This feature is in beta, so it may not work as expected. - [x] **Fast File Listing in Find Files (Ctrl + P)** - Loads files at startup and caches them for faster loading. - Watches file being created or modified from sidebar and updates the list accordingly. - [x] **Ctrl Key Functionality** - Keyboard shortcuts: - Ctrl+S: Save - Ctrl+Shift+P: Open the command palette. (Your shortcut may be different depending on what is saved in .keybindings.json file.) - [x] **Plugin API** - `contextMenu` is a component that can be used to show context menu in your plugin page. - [x] **Customisable quick tools** - You can now customise quick tools from settings. - Fixes - [x] **Scrolling Issue** - Resolved an issue causing automatic scrolling from the cursor's position when the back button is pressed with the soft keyboard up. - Fixed a bug where scrollbar gets visible even when the file is not scrollable. - [x] **Active files in sidebar** - Fixed active files taking whole height of sidebar. - [x] **File opened using intent** - Fixed file opened using intent is not set as active file. - [x] **App doesn't load** - Fixed an issue where the app wouldn't load when an error occurred. - [x] **File tab bar** - Changing file tab bar position will not make editor lose focus. - [x] **Sidebar** - Fixed sidebar resized unknowingly when dragged to open or close it. - [x] **Close all tabs** - Fixed "close all" action opens up multiple confirm dialog for every unsaved file. - Now it will ask what to do with unsaved files. - [x] **File changed alert** - Fixed file changed alert showing up even when file is not changed. - [x] **Explore** - Fixed file not opening when opened from Explore. - [x] **Extensions app in sidebar** - Fixed extensions app's explore not rendering properly. - [x] **Minor bugs** - Fixed many minor bugs. - Improved stability. ## [1.8.3] - Build 278 - [x] Bugs fixes ## [1.8.2] - Build 276 - [x] Updated ace editor to version v1.18.0 - [x] Bugs fixes ## [1.8.1] - Build 274 - [x] Clicking on gutter will go to that line - [x] Plugin Stability improvement - [x] Updated ace editor - [x] Bugs fixes ## [1.8.0] - Build 272 - [x] Plugins can support themes, fonts and sidebar items - [x] Redesigned sidebar and quicktools - [x] UI improvements - [x] Bugs fixes ## [1.7.2] - Build 268 - [x] Added new command to toggle quick tools - [x] Added back search in quick tools - [x] Palette will close on ESC key - [x] Plugin settings UI - [x] Bugs fixes ## [1.7.1] - Build 266 - [x] Swipe to change tab in plugins page - [x] Fixed update paid plugin - [x] Bugs fixes ## [1.7.0] - Build 262 - [x] New medium size teardrop - [x] Fixed some indic languages rendering - [x] Fixed selection bug - [x] Support for paid plugins - [x] Quick tools improvement - [x] Settings will have info button, you can see more info about settings - [x] Updated plugin system - [x] You can see plugin review and rating - [x] GitHub is removed from app but, you can still use it from plugin - [x] Bugs fixes ## [1.7.0] - Build 260 - [x] Quick tools improvement - [x] Settings will have info button, you can see more info about settings - [x] Updated plugin system - [x] You can see plugin review and rating - [x] GitHub is removed from app but, you can still use it from plugin - [x] Bugs fixes ## [1.6.1] - Build 245 - [x] Updated plugin CDN ## [1.6.0] - Build 244 - [x] Updated plugin APIs - [x] Updated text menu - [x] Install plugin form device - [x] Updated ace editor ## [1.6.0] - Build 239 - [x] Fixed horizontal scroll bug - [x] Updated ace editor ## [1.6.0] - Build 235 - [x] Retry FTP/SFTP connection when fail - [x] Supports plugin - [x] Updated custom theme settings - [x] Set custom port for preview in app settings - [x] New option find file - [x] Stability improvement - [x] UI improvement - [x] Fixed keyboard issues - [x] Fixed tap hold to select text - [x] Fixed loading files using FTP/SFTP - [x] Fixed File checking - [x] Fixed settings - [x] Various fixes ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at Acode.CODE_OF_CONDUCT.md All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Corrections Issues **Community Impact**: Must Not Use of inappropriate coding language or other hacker behavior deemed Any professional or manipulative findings like ease dropping hackers, malware bots, or faulty A.I. that breaks code for attacks against citizens cut-off for those programs and users are rendered fictitious businesses spammers using meta-trans-users causing confusion and corruption problems recirculating systems. Without help fixing these misconducts are responsible with talk back in any encryptions Acode intelligence blocking them from entry to another account. Issues and comments are welcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Guidance **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent view **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent free service for bandwidth/LTE technology, for opening arguments within discussions hearing any testing that class forms, and any sort of arbitration claims processed are settled. Giving back to private sectors,to public interaction within the community, state, country, and never excludes global conglomerates due to consumer reports, feedbacks, sharing viable information that interests any disadvantages against ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement reversal legislation relativity](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Acode Thank you for your interest in contributing to Acode! This guide will help you get started with development. ## Quick Start Options ### Option 1: DevContainer (Recommended) 1. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) in VS Code or other editors that support DevContainers. 2. Clone and open the repository: ```bash git clone https://github.com/Acode-Foundation/Acode.git code Acode ``` 3. When VS Code prompts "Reopen in Container", click it - Or use Command Palette (Cmd/Ctrl+Shift+P) → "Dev Containers: Reopen in Container" 4. Wait for the container to build (~5-10 minutes first time, subsequent opens are instant) 5. Once ready, build the APK: ```bash pnpm run build paid dev apk ``` > Use any package manager (pnpm, bun, npm, yarn, etc.) ### Option 2: Docker CLI (For Any Editor) If your editor doesn't support DevContainers, you can use Docker directly: ```bash # Clone the repository git clone https://github.com/Acode-Foundation/Acode.git cd Acode # Build the Docker image from our Dockerfile docker build -t acode-dev .devcontainer/ # Run the container with your code mounted docker run -it --rm \ -v "$(pwd):/workspaces/acode" \ -w /workspaces/acode \ acode-dev \ bash # Inside the container, run setup and build # bun run setup && bun run build paid dev apk pnpm run setup pnpm run build paid dev apk # or pnpm run build p d ``` **Keep container running for repeated use:** ```bash # Start container in background docker run -d --name acode-dev \ -v "$(pwd):/workspaces/acode" \ -w /workspaces/acode \ acode-dev \ sleep infinity # Execute commands in the running container docker exec -it acode-dev bash -c "pnpm run setup" docker exec -it acode-dev pnpm run build paid dev apk # Stop and remove when done docker stop acode-dev && docker rm acode-dev ``` --- ## 🛠️ Manual Setup (Without Docker) If you prefer not to use Docker at all: ### Prerequisites | Requirement | Version | |------------|---------| | **Node.js** | 18+ (22 recommended) | | **pnpm** or **bun** | Latest | | **Java JDK** | 17+ (21 recommended) | | **Android SDK** | API 35 | | **Gradle** | 8.x | ### Environment Setup Add these to your shell profile (`~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`): **macOS:** ```bash export ANDROID_HOME="$HOME/Library/Android/sdk" export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin" ``` **Linux:** ```bash export ANDROID_HOME="$HOME/Android/Sdk" export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin" ``` Some more environment variables, check [cordova docs](https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html). ### Build Steps ```bash # Clone the repository git clone https://github.com/Acode-Foundation/Acode.git cd Acode # Install dependencies and set up Cordova pnpm run setup # Build the APK pnpm run build paid dev apk # or pnpm run build p d ``` The APK will be at: `platforms/android/app/build/outputs/apk/debug/app-debug.apk` ## 📝 Contribution Guidelines ### Before Submitting a PR 1. **Fork** the repository and create a branch from `main` 2. **Make changes** - keep commits focused and atomic 3. **Check code quality:** ```bash pnpm run check ``` 4. **Test** on a device or emulator if possible ### Pull Request Checklist - [ ] Clear description of changes - [ ] Reference to related issue (if applicable) - [ ] Screenshots/GIFs for UI changes - [ ] Passing CI checks ### Code Style We use [Biome](https://biomejs.dev/) for linting and formatting: - Run `pnpm run check` before committing - Install the Biome VS Code extension for auto-formatting ### Commit Messages Use clear, descriptive messages: ``` feat: add dark mode toggle to settings fix: resolve crash when opening large files docs: update build instructions refactor: simplify file loading logic ``` ## 🌍 Adding Translations 1. Create a JSON file in `src/lang/` (e.g., `fr-fr.json` for French) 2. Add it to `src/lib/lang.js` 3. Use the translation utilities: ```bash pnpm run lang add # Add new string pnpm run lang remove # Remove string pnpm run lang search # Search strings pnpm run lang update # Update translations ``` ## ℹ️ Adding New Icons (to the existing font family) > [!NOTE] > Acode uses SVG and converts them into a font family, to be used inside the editor and generally for plugin devs. > > **Plugin-specific icons SHOULD NOT be added into the editor. Only generally helpful icons SHOULD BE added** Many font editing software and web-based tools exist for this purpose. Some of them are listed below. | Name | Platform | |------|----------| | https://icomoon.io/ | Free (Web-Based, PWA-supported, Offline-supported) | | https://fontforge.org/ | Open-Source (Linux, Mac, Windows) | ### Steps in Icomoon to add new Icons 1. Download the `code-editor-icon.icomoon.json` file from https://github.com/Acode-Foundation/Acode/tree/main/utils 2. Go to https://icomoon.io/ > Import 3. Import the `code-editor-icon.icomoon.json` downloaded (in step 1) 4. All icons will be displayed after importing. 5. Import the SVG icon created/downloaded to be added to the Font Family. 6. On the right side, press **enable Show Characters** & **Show Names** to view the Unicode character & Name for that icon. 7. Provided the newly added SVG icon with a name (in the name box). 8. Repeat Step 5 and Step 7 until all needed new icons are added. 9. Press the export icon from the top left-hand side. 10. Press the download button, and a zip file will be downloaded. 11. Go to the Projects section of [icomoon](https://icomoon.io/new-app), uncollapse/expand the Project named `code-editor-icon` and press the **save** button (this downloads the project file named: `code-editor-icon.icomoon.json`) ### Updating Project files for Icon Contribution 1. Extract the downloaded zip file; navigate to the `fonts` folder inside it. 2. Rename `code-editor-icon.ttf` to `icons.ttf`. 3. Copy & paste the renamed `icons.ttf` into https://github.com/Acode-Foundation/Acode/tree/main/src/res/icons 4. Copy and paste the `code-editor-icon.icomoon.json` file (downloaded in the adding icons steps) onto https://github.com/Acode-Foundation/Acode/tree/main/utils (yes, replace it with the newer one; we downloaded!) 4. Commit the changes **ON A NEW branch** (by following: [Commit Messages guide](#commit-messages)) ## 🔌 Plugin Development To create plugins for Acode: - [Plugin Starter Repository](https://github.com/Acode-Foundation/acode-plugin) - [Plugin Documentation](https://docs.acode.app/) ================================================ FILE: _typos.toml ================================================ [default] check-filename = true [files] extend-exclude = [ "node_modules", "www", "*.yaml", ".vscode", "fastlane", "hooks", "*.gradle", "plugins", "platforms", "src/lang/ar-ye.json", "src/lang/be-by.json", "src/lang/bn-bd.json", "src/lang/cs-cz.json", "src/lang/de-de.json", "src/lang/es-sv.json", "src/lang/fr-fr.json", "src/lang/hi-in.json", "src/lang/hu-hu.json", "src/lang/id-id.json", "src/lang/ir-fa.json", "src/lang/it-it.json", "src/lang/ja-jp.json", "src/lang/ko-kr.json", "src/lang/ml-in.json", "src/lang/mm-unicode.json", "src/lang/mm-zawgyi.json", "src/lang/pl-pl.json", "src/lang/pt-br.json", "src/lang/pu-in.json", "src/lang/ru-ru.json", "src/lang/tl-ph.json", "src/lang/tr-tr.json", "src/lang/uk-ua.json", "src/lang/uz-uz.json", "src/lang/vi-vn.json", "src/lang/zh-cn.json", "src/lang/zh-hant.json", "src/lang/zh-tw.json", ] [default.extend-words] # temporary thing to staisfy the linter strech = "strech" contaienr = "contaienr" formate = "formate" collapsable = "collapsable" styl = "styl" IZ = "IZ" shft = "shft" multline = "multline" ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.4.11/schema.json", "formatter": { "enabled": true, "indentStyle": "tab" }, "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, "rules": { "recommended": false, "complexity": { "noForEach": "off", "noStaticOnlyClass": "error", "noUselessSwitchCase": "error", "useFlatMap": "error" }, "style": { "noNegationElse": "off", "useForOf": "error", "useNodejsImportProtocol": "error", "useNumberNamespace": "error" }, "suspicious": { "noDoubleEquals": "error", "noThenProperty": "error", "useIsArray": "error" } } }, "javascript": { "globals": ["Global1"] }, "files": { "includes": [ "**/src/**/*", "**/utils/**/*.js", "!**/www/build/**/*", "**/www/res/**/*.css", "**/src/plugins/terminal/**", "!**/ace-builds", "!**/src/plugins/**/*", "!**/plugins/**/*", "!**/hooks/**/*", "!**/fastlane/**/*", "!**/res/**/*", "!**/platforms/**/*" ] } } ================================================ FILE: build-extras.gradle ================================================ android { packagingOptions { pickFirst 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' } } configurations { all { exclude module: 'commons-logging' exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on' exclude group: 'org.bouncycastle', module: 'bcpkix-jdk18on' exclude group: 'org.bouncycastle', module: 'bcprov-jdk18on' } } ================================================ FILE: config.xml ================================================ Acode Light weight code editor and web IDE for android. Foxdebug ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================ Welcome to Acode! A powerful, lightweight code editor, and web IDE for Android. Now enhanced with cutting-edge features and updates to transform your coding experience. What's New? Step into the future of coding with our innovative Plugin System. This brand new feature supports a wide range of plugins, boosting the functionality of Acode to meet all your development needs. With over 30 plugins already available in the Plugin Store, the possibilities are endless. Latest Updates Include: - Enhanced Ace Editor: Now updated to version 1.22.0 for more efficient editing. - Search in All Files: Our beta feature lets you search and replace text in all files within your opened projects. - Customizable Quick Tools: Personalize your quick tools to enhance your workflow. - Fast File Listing in Find Files (Ctrl + P): Acode now loads and caches files at startup, leading to faster file listing. - Ctrl Key Functionality: Take advantage of keyboard shortcuts for actions such as save (Ctrl+S) and open command palette (Ctrl+Shift+P). Why Choose Acode? Acode lets you build and run websites directly within your browser, debug with ease using the integrated console, and edit a wide range of source files - from Python and CSS to Java, JavaScript, Dart, and more. Key Features: - Universal File Editor: Edit any file directly from your device. - GitHub Integration: Seamlessly sync your projects with GitHub. - FTP/SFTP Support: Manage your files efficiently with FTP/SFTP. - Extensive Syntax Highlighting: Supports over 100 programming languages. - Personalized Themes: Choose from dozens of unique themes to match your style. - User-Friendly Interface: Navigate with ease through our intuitive design. - In-App Preview: Instantly view your HTML/MarkDown files within the app. - Interactive JavaScript Console: Debug JavaScript code right from the console. - In-App File Browser: Access your files directly within Acode. - Open Source: Benefit from our transparent and community-driven project. - High Performance: Supports files with over 50,000 lines, ensuring smooth workflow. - Multi-File Support: Work on multiple files simultaneously for productive multitasking. - Customizable Interface: Adapt Acode to your personal coding style. - Keyboard Shortcuts: Speed up your coding with handy shortcuts. - File Recovery: Never lose your work with our reliable file recovery feature. - File Management: Keep your projects organized with effective file management. Start your streamlined coding journey with Acode today. Join our ever-growing community of developers and experience the difference for yourself! ================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ A lightweight but powerful text/code editor. ================================================ FILE: fastlane/metadata/android/en-US/title.txt ================================================ Acode editor - Android code editor ================================================ FILE: hooks/README.md ================================================ # Cordova Hooks Cordova Hooks represent special scripts which could be added by application and plugin developers or even by your own build system to customize cordova commands. See Hooks Guide for more details: http://cordova.apache.org/docs/en/edge/guide_appdev_hooks_index.md.html#Hooks%20Guide. ================================================ FILE: hooks/modify-java-files.js ================================================ const path = require('path'); const fs = require('fs'); const prettier = require('prettier'); main(); async function main() { const patchVersion = '2'; const flagFile = path.resolve(__dirname, '../platforms/android/.flag_done'); if (fs.existsSync(flagFile)) { const appliedVersion = fs.readFileSync(flagFile, 'utf8').trim(); if (appliedVersion === patchVersion) { return; } } const base = path.resolve(__dirname, `../platforms/android/CordovaLib/src/org/apache/cordova`); const files = { 'SystemWebView.java': `${base}/engine/SystemWebView.java`, 'SystemWebViewEngine.java': `${base}/engine/SystemWebViewEngine.java`, 'CordovaWebViewEngine.java': `${base}/CordovaWebViewEngine.java`, 'CordovaWebView.java': `${base}/CordovaWebView.java`, 'CordovaWebViewImpl.java': `${base}/CordovaWebViewImpl.java`, }; const interfaceMethod = { name: 'setInputType', modifier: 'public', returnType: 'void', params: [ { type: 'int', name: 'type', } ], }; const nativeContextMenuInterfaceMethod = { name: 'setNativeContextMenuDisabled', modifier: 'public', returnType: 'void', params: [ { type: 'boolean', name: 'disabled', } ], }; const setInputTypeMethod = { name: 'setInputType', modifier: 'public', returnType: 'void', params: [ { type: 'int', name: 'type', } ], body: ['webView.setInputType(type);'], }; const setNativeContextMenuDisabledMethod = { name: 'setNativeContextMenuDisabled', modifier: 'public', returnType: 'void', params: [ { type: 'boolean', name: 'disabled', } ], body: ['webView.setNativeContextMenuDisabled(disabled);'], }; const contentToAdd = { 'SystemWebView.java': { 'import': [ 'android.graphics.Rect', 'android.os.Build', 'android.text.InputType', 'android.view.ActionMode', 'android.view.inputmethod.InputConnection', 'android.view.inputmethod.EditorInfo', 'android.view.Menu', 'android.view.MenuItem', 'android.view.View', ], 'fields': [ { type: 'int', name: 'type', modifier: 'private', value: '-1', }, { type: 'int', name: 'NO_SUGGESTIONS', modifier: 'private', value: '0', }, { type: 'int', name: 'NO_SUGGESTIONS_AGGRESSIVE', modifier: 'private', value: '1', }, { type: 'boolean', name: 'nativeContextMenuDisabled', modifier: 'private', value: 'false', }, ], methods: [ { ...setInputTypeMethod, body: [`this.type = type;`] }, { name: 'setNativeContextMenuDisabled', modifier: 'public', returnType: 'void', params: [ { type: 'boolean', name: 'disabled', } ], body: [`this.nativeContextMenuDisabled = disabled;`], }, { name: 'onCreateInputConnection', modifier: 'public', returnType: 'InputConnection', params: [ { type: 'EditorInfo', name: 'outAttrs', } ], body: [ `InputConnection ic = super.onCreateInputConnection(outAttrs); if (type == NO_SUGGESTIONS) { outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; } else if (type == NO_SUGGESTIONS_AGGRESSIVE) { outAttrs.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; } else { outAttrs.inputType |= InputType.TYPE_NULL; } return ic;`, ], notation: '@Override', }, { name: 'startActionMode', modifier: 'public', returnType: 'ActionMode', params: [ { type: 'ActionMode.Callback', name: 'callback', } ], body: [ `return suppressActionMode(super.startActionMode(wrapActionModeCallback(callback)));`, ], notation: '@Override', }, { name: 'startActionMode', modifier: 'public', returnType: 'ActionMode', params: [ { type: 'ActionMode.Callback', name: 'callback', }, { type: 'int', name: 'type', } ], body: [ `return suppressActionMode(super.startActionMode(wrapActionModeCallback(callback), type));`, ], notation: '@Override', }, { name: 'startActionModeForChild', modifier: 'public', returnType: 'ActionMode', params: [ { type: 'View', name: 'originalView', }, { type: 'ActionMode.Callback', name: 'callback', } ], body: [ `return suppressActionMode(super.startActionModeForChild(originalView, wrapActionModeCallback(callback)));`, ], notation: '@Override', }, { name: 'startActionModeForChild', modifier: 'public', returnType: 'ActionMode', params: [ { type: 'View', name: 'originalView', }, { type: 'ActionMode.Callback', name: 'callback', }, { type: 'int', name: 'type', } ], body: [ `return suppressActionMode(super.startActionModeForChild(originalView, wrapActionModeCallback(callback), type));`, ], notation: '@Override', }, { name: 'wrapActionModeCallback', modifier: 'private', returnType: 'ActionMode.Callback', params: [ { type: 'ActionMode.Callback', name: 'callback', } ], body: [ `if (!nativeContextMenuDisabled || callback == null) { return callback; } return new ActionMode.Callback2() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { boolean created = callback.onCreateActionMode(mode, menu); if (created) { suppressActionModeUi(mode, menu); } return created; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { boolean prepared = callback.onPrepareActionMode(mode, menu); suppressActionModeUi(mode, menu); return prepared; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return callback.onActionItemClicked(mode, item); } @Override public void onDestroyActionMode(ActionMode mode) { callback.onDestroyActionMode(mode); } @Override public void onGetContentRect(ActionMode mode, View view, Rect outRect) { if (callback instanceof ActionMode.Callback2) { ((ActionMode.Callback2) callback).onGetContentRect(mode, view, outRect); return; } super.onGetContentRect(mode, view, outRect); } };`, ], }, { name: 'suppressActionMode', modifier: 'private', returnType: 'ActionMode', params: [ { type: 'ActionMode', name: 'mode', } ], body: [ `if (mode == null || !nativeContextMenuDisabled) { return mode; } suppressActionModeUi(mode, mode.getMenu()); return mode;`, ], }, { name: 'suppressActionModeUi', modifier: 'private', returnType: 'void', params: [ { type: 'ActionMode', name: 'mode', }, { type: 'Menu', name: 'menu', } ], body: [ `if (mode == null || !nativeContextMenuDisabled || menu == null) { return; } menu.clear(); mode.setTitle(null); mode.setSubtitle(null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { post(() -> { if (!nativeContextMenuDisabled) { return; } try { mode.hide(0); } catch (Throwable ignored) { } }); }`, ], }, ] }, 'SystemWebViewEngine.java': { methods: [ setInputTypeMethod, setNativeContextMenuDisabledMethod, ] }, 'CordovaWebViewEngine.java': { methods: [ interfaceMethod, nativeContextMenuInterfaceMethod, ] }, 'CordovaWebView.java': { methods: [ interfaceMethod, nativeContextMenuInterfaceMethod, ] }, 'CordovaWebViewImpl.java': { methods: [ { ...setInputTypeMethod, body: [`engine.setInputType(type);`] }, { ...setNativeContextMenuDisabledMethod, body: [`engine.setNativeContextMenuDisabled(disabled);`] } ] } }; const fileContent = {}; for (let file in files) { fileContent[file] = fs.readFileSync(files[file], 'utf8'); } for (let file in contentToAdd) { const content = fileContent[file]; const contentToAddTo = contentToAdd[file]; const text = removeComments(content); let newContent = await format(text); if (contentToAddTo.import) { const imports = contentToAddTo.import.map(importStr => { return `import ${importStr};`; }).join('\n'); newContent = newContent.replace( /^(\s*)(import.*;)/m, `$1${imports}\n$2` ); } if (contentToAddTo.fields) { const fields = contentToAddTo.fields.map(field => { return getFieldString(field); }).join('\n'); newContent = newContent.replace( /^(\s*)(\w+\s+\w+\s*;)/m, `$1${fields}\n$2` ); } if (contentToAddTo.methods) { const methods = contentToAddTo.methods.map(method => { return getMethodString(method); }).join('\n'); if (isInterface(file, content)) { const regex = getInterfaceDeclarationRegex(file); newContent = newContent.replace( regex, `$1${methods}\n$2` ); } else { let regex = getConstructorRegex(file); if (regex.test(newContent)) { newContent = newContent.replace( regex, `$1${methods}\n$2` ); } else { regex = getClassDeclarationRegex(file); newContent = newContent.replace( regex, `$1${methods}\n$2` ); } } } newContent = await format(newContent); fs.writeFile(files[file], newContent, err => { if (err) { console.log(err); process.exit(1); } console.log(`${files[file]} updated`); }); } fs.writeFile(flagFile, patchVersion, err => { if (err) { console.log(err); process.exit(1); } console.log(`${flagFile} updated`); }); async function format(content) { return prettier.format(content, { plugins: ['prettier-plugin-java'], parser: 'java', tabWidth: 2, printWidth: Infinity }); } function getMethodString(method) { const params = method.params.map(param => { return `${param.type} ${param.name}`; }).join(', '); let str = `${method.modifier} ${method.returnType} ${method.name}(${params})`; if (method.notation) { str = `\n${method.notation}\n${str}`; } if (method.body) { return str + `{${method.body.join('')}}`; } return str + ';'; } function getFieldString(field) { return `${field.modifier} ${field.type} ${field.name}${field.value ? ` = ${field.value}` : ''};`; } function isInterface(filename, content) { return content.indexOf(`interface ${filename.split('.')[0]}`) > -1; } function getConstructorRegex(filename) { return new RegExp(`([^]*${filename.split('.')[0]}\\s*\\(.*\\)\\s*{[^}]*})([^]*)`, 'm'); } function getInterfaceDeclarationRegex(filename) { return new RegExp(`([^]*interface\\s+${filename.split('.')[0]}[\\s\\w]*{)([^]*})`, 'm'); } function getClassDeclarationRegex(filename) { return new RegExp(`([^]*class\\s+${filename.split('.')[0]}[\\s\\w]*{)([^]*})`, 'm'); } function removeComments(content) { return content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, ''); } } ================================================ FILE: hooks/move-files.js ================================================ const path = require('path'); const fs = require('fs'); const gradleFilePath = path.resolve(__dirname, '../build-extras.gradle'); const newGradleFilePath = path.resolve( __dirname, '../platforms/android/app/build-extras.gradle' ); const buildFilePath = path.resolve(__dirname, '../build.json'); const newBuildFilePath = path.resolve( __dirname, '../platforms/android/build.json' ); const repeatChar = (char, times) => { let res = ''; while (--times >= 0) res += char; return res; }; let msg; if (!fs.existsSync(newBuildFilePath) && fs.existsSync(buildFilePath)) { msg = '== Moved build.json =='; console.log(repeatChar('=', msg.length)); console.log(msg); console.log(repeatChar('=', msg.length)); fs.copyFileSync(buildFilePath, newBuildFilePath); } if (!fs.existsSync(newGradleFilePath) && fs.existsSync(gradleFilePath)) { msg = '== Moved build-extras.gradle =='; console.log(repeatChar('=', msg.length)); console.log(msg); console.log(repeatChar('=', msg.length)); fs.copyFileSync(gradleFilePath, newGradleFilePath); } ================================================ FILE: hooks/post-process.js ================================================ /* eslint-disable no-console */ const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const buildFilePath = path.resolve(__dirname, '../build.json'); const copyToPath = path.resolve(__dirname, '../platforms/android/build.json'); const gradleFilePath = path.resolve(__dirname, '../build-extras.gradle'); const androidGradleFilePath = path.resolve( __dirname, '../platforms/android/app/build-extras.gradle' ); const resPath = path.resolve(__dirname, '../platforms/android/app/src/main/res/'); const localResPath = path.resolve(__dirname, '../res/android/'); if ( !fs.existsSync(copyToPath) && fs.existsSync(buildFilePath) ) fs.copyFileSync(buildFilePath, copyToPath); if (fs.existsSync(androidGradleFilePath)) fs.unlinkSync(androidGradleFilePath); fs.copyFileSync(gradleFilePath, androidGradleFilePath); // Cordova Android 15 generates `cdv_*` resources and version-qualified value // directories that are required later in the build. Keep the generated tree and // only overlay this project's custom resources on top of it. copyDirRecursively(localResPath, resPath); enableLegacyJni(); enableStaticContext(); patchTargetSdkVersion(); enableKeyboardWorkaround(); function getTmpDir() { const tmpdirEnv = process.env.TMPDIR; if (tmpdirEnv) { try { fs.accessSync(tmpdirEnv, fs.constants.R_OK | fs.constants.W_OK); return tmpdirEnv; } catch { // TMPDIR exists but not accessible } } try { fs.accessSync("/tmp", fs.constants.R_OK | fs.constants.W_OK); return "/tmp"; } catch { console.log("Error: No usable temporary directory found (TMPDIR or /tmp not accessible)."); return null; // process.exit(1); } } function patchTargetSdkVersion() { const prefix = execSync('npm prefix').toString().trim(); const gradleFile = path.join(prefix, 'platforms/android/app/build.gradle'); if (!fs.existsSync(gradleFile)) { console.warn('[Cordova Hook] ⚠️ build.gradle not found'); return; } let content = fs.readFileSync(gradleFile, 'utf-8'); const sdkRegex = /targetSdkVersion\s+(cordovaConfig\.SDK_VERSION|\d+)/; if (sdkRegex.test(content)) { let api = "36"; const tmp = getTmpDir(); if (tmp == null) { console.warn("---------------------------------------------------------------------------------\n\n\n\n"); console.warn(`⚠️ fdroid.bool not found`); console.warn("⚠️ Fdroid flavour will be built"); api = "28"; console.warn("\n\n\n\n---------------------------------------------------------------------------------"); } else { const froidFlag = path.join(getTmpDir(), 'fdroid.bool'); if (fs.existsSync(froidFlag)) { const fdroid = fs.readFileSync(froidFlag, 'utf-8').trim(); if (fdroid == "true") { api = "28"; } } else { console.warn("---------------------------------------------------------------------------------\n\n\n\n"); console.warn(`⚠️ fdroid.bool not found`); console.warn("⚠️ Fdroid flavour will be built"); api = "28"; console.warn("\n\n\n\n---------------------------------------------------------------------------------"); //process.exit(1); } } content = content.replace(sdkRegex, 'targetSdkVersion ' + api); fs.writeFileSync(gradleFile, content, 'utf-8'); console.log('[Cordova Hook] ✅ Patched targetSdkVersion to ' + api); } else { console.warn('[Cordova Hook] ⚠️ targetSdkVersion not found'); } } function enableLegacyJni() { const prefix = execSync('npm prefix').toString().trim(); const gradleFile = path.join(prefix, 'platforms/android/app/build.gradle'); if (!fs.existsSync(gradleFile)) return; let content = fs.readFileSync(gradleFile, 'utf-8'); // Check for correct block to avoid duplicate insertion if (content.includes('useLegacyPackaging = true')) return; // Inject under android block with correct Groovy syntax content = content.replace(/android\s*{/, match => { return ( match + ` packagingOptions { jniLibs { useLegacyPackaging = true } }` ); }); fs.writeFileSync(gradleFile, content, 'utf-8'); console.log('[Cordova Hook] ✅ Enabled legacy JNI packaging'); } function enableStaticContext() { try { const prefix = execSync('npm prefix').toString().trim(); const mainActivityPath = path.join( prefix, 'platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java' ); if (!fs.existsSync(mainActivityPath)) { return; } let content = fs.readFileSync(mainActivityPath, 'utf-8'); // Skip if fully patched if ( content.includes('WeakReference') && content.includes('public static Context getContext()') && content.includes('weakContext = new WeakReference<>(this);') ) { return; } // Add missing imports if (!content.includes('import java.lang.ref.WeakReference;')) { content = content.replace( /import org\.apache\.cordova\.\*;/, match => match + '\nimport android.content.Context;\nimport java.lang.ref.WeakReference;' ); } // Inject static field and method into class body content = content.replace( /public class MainActivity extends CordovaActivity\s*\{/, match => match + `\n\n private static WeakReference weakContext;\n\n` + ` public static Context getContext() {\n` + ` return weakContext != null ? weakContext.get() : null;\n` + ` }\n` ); // Insert weakContext assignment inside onCreate content = content.replace( /super\.onCreate\(savedInstanceState\);/, `super.onCreate(savedInstanceState);\n weakContext = new WeakReference<>(this);` ); fs.writeFileSync(mainActivityPath, content, 'utf-8'); } catch (err) { console.error('[Cordova Hook] ❌ Failed to patch MainActivity:', err.message); } } function enableKeyboardWorkaround() { try{ const prefix = execSync('npm prefix').toString().trim(); const mainActivityPath = path.join( prefix, 'platforms/android/app/src/main/java/com/foxdebug/acode/MainActivity.java' ); if (!fs.existsSync(mainActivityPath)) { return; } let content = fs.readFileSync(mainActivityPath, 'utf-8'); // Skip if already patched if (content.includes('SoftInputAssist')) { return; } // Add import if (!content.includes('import com.foxdebug.system.SoftInputAssist;')) { content = content.replace( /import java.lang.ref.WeakReference;|import org\.apache\.cordova\.\*;/, match => match + '\nimport com.foxdebug.system.SoftInputAssist;' ); } // Declare field if (!content.includes('private SoftInputAssist softInputAssist;')) { content = content.replace( /public class MainActivity extends CordovaActivity\s*\{/, match => match + `\n\n private SoftInputAssist softInputAssist;\n` ); } // Initialize in onCreate content = content.replace( /loadUrl\(launchUrl\);/, `loadUrl(launchUrl);\n\n softInputAssist = new SoftInputAssist(this);` ); fs.writeFileSync(mainActivityPath, content, 'utf-8'); console.log('[Cordova Hook] ✅ Enabled keyboard workaround'); } catch (err) { console.error('[Cordova Hook] ❌ Failed to enable keyboard workaround:', err.message); } } /** * Copy directory recursively * @param {string} src Source directory * @param {string} dest Destination directory * @param {string[]} skip Files to not copy */ function copyDirRecursively(src, dest, skip = [], currPath = '') { const exists = fs.existsSync(src); const stats = exists && fs.statSync(src); const isDirectory = exists && stats.isDirectory(); if (!exists) { console.log(`File ${src} does not exist`); return; } if (!fs.existsSync(dest) && isDirectory) { fs.mkdirSync(dest); } if (exists && isDirectory) { fs.mkdirSync(dest, { recursive: true }); fs.readdirSync(src).forEach((childItemName) => { const relativePath = path.join(currPath, childItemName); if (childItemName.startsWith('.')) return; if (skip.includes(childItemName) || skip.includes(relativePath)) return; copyDirRecursively( path.join(src, childItemName), path.join(dest, childItemName), skip, relativePath, ); }); } else { removeConflictingResourceFiles(src, dest); fs.copyFileSync(src, dest); // log const message = `copied: ${path.basename(src)}`; console.log('\x1b[32m%s\x1b[0m', message); // green } } function removeConflictingResourceFiles(src, dest) { const parentDir = path.dirname(dest); if (!fs.existsSync(parentDir)) { return; } const resourceDirName = path.basename(parentDir); if (!resourceDirName.startsWith('mipmap') && !resourceDirName.startsWith('drawable')) { return; } const srcExt = path.extname(src); const resourceName = path.basename(src, srcExt); for (const existingName of fs.readdirSync(parentDir)) { const existingPath = path.join(parentDir, existingName); if (existingPath === dest || !fs.statSync(existingPath).isFile()) { continue; } const existingExt = path.extname(existingName); const existingResourceName = path.basename(existingName, existingExt); if (existingResourceName !== resourceName || existingExt === srcExt) { continue; } fs.rmSync(existingPath); console.log('\x1b[31m%s\x1b[0m', `deleted conflicting resource: ${existingName}`); } } ================================================ FILE: hooks/restore-cordova-resources.js ================================================ const fs = require("fs"); const path = require("path"); const templateResPath = path.resolve( __dirname, "../node_modules/cordova-android/templates/project/res", ); const androidResPath = path.resolve( __dirname, "../platforms/android/app/src/main/res", ); if (!fs.existsSync(templateResPath) || !fs.existsSync(androidResPath)) { process.exit(0); } restoreCordovaResourceFiles(templateResPath); function restoreCordovaResourceFiles(currentPath) { for (const entry of fs.readdirSync(currentPath, { withFileTypes: true })) { const absolutePath = path.join(currentPath, entry.name); if (entry.isDirectory()) { restoreCordovaResourceFiles(absolutePath); continue; } if (!shouldRestore(absolutePath)) { continue; } const relativePath = path.relative(templateResPath, absolutePath); const destinationPath = path.join(androidResPath, relativePath); if (fs.existsSync(destinationPath)) { continue; } fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); fs.copyFileSync(absolutePath, destinationPath); console.log(`[Cordova Hook] Restored ${relativePath}`); } } function shouldRestore(filePath) { const fileName = path.basename(filePath); return fileName.startsWith("cdv_") || fileName === "ic_cdv_splashscreen.xml"; } ================================================ FILE: jsconfig.json ================================================ { "exclude": ["**/node_modules", "**/platforms", "**/www", "www/js/ace/**/*"], "compilerOptions": { "baseUrl": "./src", "paths": { "*": ["*"] } }, "include": ["src/**/*"], "typeAcquisition": { "enable": true } } ================================================ FILE: license.txt ================================================ Copyright 2020 Foxdebug(Ajit Kumar) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: package.json ================================================ { "name": "com.foxdebug.acode", "displayName": "Acode", "version": "1.11.8", "description": "Acode is a code editor for android", "scripts": { "lang": "node ./utils/lang.js", "build": "sh utils/scripts/build.sh", "start": "sh utils/scripts/start.sh", "clean": "sh utils/scripts/clean.sh android android", "plugin": "sh utils/scripts/plugin.sh", "setup": "node ./utils/setup.js", "lint": "biome lint --write", "format": "biome format --write", "check": "biome check --write", "typecheck": "tsc --noEmit", "updateAce": "node ./utils/updateAce.js" }, "keywords": [ "ecosystem:cordova" ], "author": "Foxdebug (Ajit Kumar)", "license": "MIT", "cordova": { "plugins": { "cordova-clipboard": {}, "cordova-plugin-device": {}, "cordova-plugin-file": {}, "cordova-plugin-server": {}, "cordova-plugin-ftp": {}, "cordova-plugin-sdcard": {}, "cordova-plugin-iap": {}, "cordova-plugin-advanced-http": { "ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1" }, "cordova-plugin-websocket": {}, "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": {}, "com.foxdebug.acode.rk.auth": {}, "cordova-plugin-system": {} }, "platforms": [ "android" ] }, "repository": { "type": "git", "url": "git+https://github.com/deadlyjack/acode.git" }, "bugs": { "url": "https://github.com/deadlyjack/acode/issues" }, "homepage": "https://github.com/deadlyjack/acode#readme", "devDependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-runtime": "^7.28.5", "@babel/preset-env": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@babel/runtime": "^7.28.4", "@babel/runtime-corejs3": "^7.28.4", "@biomejs/biome": "2.4.11", "@rspack/cli": "^1.7.0", "@rspack/core": "^1.7.0", "@types/ace": "^0.0.52", "@types/url-parse": "^1.4.11", "autoprefixer": "^10.4.22", "babel-loader": "^10.0.0", "com.foxdebug.acode.rk.auth": "file:src/plugins/auth", "com.foxdebug.acode.rk.customtabs": "file:src/plugins/custom-tabs", "com.foxdebug.acode.rk.exec.proot": "file:src/plugins/proot", "com.foxdebug.acode.rk.exec.terminal": "file:src/plugins/terminal", "com.foxdebug.acode.rk.plugin.plugincontext": "file:src/plugins/pluginContext", "cordova-android": "^15.0.0", "cordova-clipboard": "^1.3.0", "cordova-plugin-advanced-http": "^3.3.1", "cordova-plugin-browser": "file:src/plugins/browser", "cordova-plugin-buildinfo": "file:src/plugins/cordova-plugin-buildinfo", "cordova-plugin-device": "^2.1.0", "cordova-plugin-file": "^8.1.3", "cordova-plugin-ftp": "file:src/plugins/ftp", "cordova-plugin-iap": "file:src/plugins/iap", "cordova-plugin-sdcard": "file:src/plugins/sdcard", "cordova-plugin-server": "file:src/plugins/server", "cordova-plugin-sftp": "file:src/plugins/sftp", "cordova-plugin-system": "file:src/plugins/system", "cordova-plugin-websocket": "file:src/plugins/websocket", "css-loader": "^7.1.2", "mini-css-extract-plugin": "^2.9.4", "path-browserify": "^1.0.1", "postcss-loader": "^8.2.0", "prettier": "^3.7.4", "prettier-plugin-java": "^2.7.7", "raw-loader": "^4.0.2", "sass": "^1.94.2", "sass-loader": "^16.0.6", "style-loader": "^4.0.0", "terminal": "^0.1.4", "ts-loader": "^9.5.4", "typescript": "^5.9.3", "vscode-languageserver-types": "^3.17.5" }, "dependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", "@codemirror/lang-angular": "^0.1.4", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-java": "^6.0.2", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-less": "^6.0.2", "@codemirror/lang-liquid": "^6.3.2", "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", "@codemirror/lint": "^6.9.5", "@codemirror/lsp-client": "^6.2.2", "@codemirror/search": "^6.6.0", "@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", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-image": "^0.8.0", "@xterm/addon-search": "^0.15.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "acorn": "^8.15.0", "autosize": "^6.0.1", "codemirror": "^6.0.2", "cordova": "13.0.0", "core-js": "^3.47.0", "dayjs": "^1.11.19", "dompurify": "^3.3.2", "escape-string-regexp": "^5.0.0", "esprima": "^4.0.1", "filesize": "^11.0.13", "html-tag-js": "^2.4.16", "jszip": "^3.10.1", "katex": "^0.16.39", "markdown-it": "^14.1.1", "markdown-it-anchor": "^9.2.0", "markdown-it-emoji": "^3.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-github-alerts": "^1.0.0", "markdown-it-task-lists": "^2.1.1", "markdown-it-texmath": "^1.0.0", "mermaid": "^11.13.0", "mime-types": "^3.0.1", "mustache": "^4.2.0", "picomatch": "^4.0.4", "url-parse": "^1.5.10", "vanilla-picker": "^2.12.3", "yargs": "^18.0.0" }, "overrides": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.2", "@codemirror/lint": "^6.9.5", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.40.0" }, "browserslist": "cover 100%,not android < 5" } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: [ require('autoprefixer')({}) ] }; ================================================ FILE: readme.md ================================================ # Acode - Code Editor for Android

[![](https://img.shields.io/endpoint?logo=telegram&label=Acode&style=flat&url=https%3A%2F%2Facode.app%2Fapi%2Ftelegram-members-count)](https://t.me/foxdebug_acode) [![](https://dcbadge.vercel.app/api/server/vVxVWYUAWD?style=flat)](https://discord.gg/vVxVWYUAWD) ## • Overview Welcome to Acode Editor - a powerful and versatile code editing tool designed specifically for Android devices. Whether you're working on HTML, CSS, JavaScript, or other programming languages, Acode empowers you to code on-the-go with confidence. ## • Features - Edit and create websites, and instantly preview them in a browser. - Seamlessly modify source files for various languages like Python, Java, JavaScript, and more. - Built-in javascript console - Enjoy multi-language editing support with easy management tools. - Enjoy a large collections of community plugins to enhance your coding experience. ## • Installation You can get Acode Editor from popular platforms: [Get it on Google Play](https://play.google.com/store/apps/details?id=com.foxdebug.acodefree) [Get it on F-Droid](https://www.f-droid.org/packages/com.foxdebug.acode/) ## • Project Structure
Acode/
|
|- src/   - Core code and language files
|
|- www/   - Public documents, compiled files, and HTML templates
|
|- utils/ - CLI tools for building, string manipulation, and more
## • Multi-language Support Enhance Acode's capabilities by adding new languages easily. Just create a file with the language code (e.g., en-us for English) in [`src/lang/`](https://github.com/Acode-Foundation/Acode/tree/main/src/lang) and include it in [`src/lib/lang.js`](https://github.com/Acode-Foundation/Acode/blob/main/src/lib/lang.js). Manage strings across languages effortlessly using utility commands: ```shell pnpm run lang add pnpm run lang remove pnpm run lang search pnpm run lang update ``` ## • Contributing & Building the Application See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions. ## • Contributors ## • Developing a Plugin for Acode For comprehensive documentation on creating plugins for Acode Editor, visit the [repository](https://github.com/Acode-Foundation/acode-plugin). For plugin development information, refer to: [Acode Plugin Documentation](https://docs.acode.app/) ## Star History Star History Chart ================================================ FILE: res/android/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: res/android/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: res/android/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: res/android/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: res/android/values/colors.xml ================================================ #FFFFFF ================================================ FILE: res/android/values/ic_launcher_background.xml ================================================ #3a3e54 #3a3e54 ================================================ FILE: res/android/values/themes.xml ================================================ ================================================ FILE: res/android/xml/network_security_config.xml ================================================ ================================================ FILE: rspack.config.js ================================================ const path = require('path'); const { rspack } = require('@rspack/core'); module.exports = (env, options) => { const { mode = 'development' } = options; const prod = mode === 'production'; const rules = [ // TypeScript/TSX files - Custom JSX loader + SWC { test: /\.tsx?$/, exclude: /node_modules/, use: [ { loader: 'builtin:swc-loader', options: { jsc: { parser: { syntax: 'typescript', tsx: false, }, transform: { // react: { // pragma: 'tag', // pragmaFrag: 'Array', // throwIfNamespace: false, // development: false, // useBuiltins: false, // runtime: 'classic', // }, }, target: 'es2015', }, }, }, path.resolve(__dirname, 'utils/custom-loaders/html-tag-jsx-loader.js'), ], }, // JavaScript files { test: /\.m?js$/, oneOf: [ // Node modules - use builtin:swc-loader only { include: /node_modules/, use: [ { loader: 'builtin:swc-loader', options: { jsc: { parser: { syntax: 'ecmascript', }, target: 'es2015', }, }, }, ], }, // Source JS files - Custom JSX loader + SWC (JSX will be removed first) { use: [ { loader: 'builtin:swc-loader', options: { jsc: { parser: { syntax: 'ecmascript', jsx: false, }, target: 'es2015', }, }, }, path.resolve(__dirname, 'utils/custom-loaders/html-tag-jsx-loader.js'), ], }, ], }, // Handlebars and Markdown files { test: /\.(hbs|md)$/, type: 'asset/source', }, // Module CSS/SCSS (with .m prefix) { test: /\.m\.(sa|sc|c)ss$/, use: [ 'raw-loader', 'postcss-loader', 'sass-loader', ], type: 'javascript/auto', }, // Asset files { test: /\.(png|svg|jpg|jpeg|ico|ttf|webp|eot|woff|webm|mp4|wav)(\?.*)?$/, type: 'asset/resource', }, // Regular CSS/SCSS files { test: /(? { const rect = view.dom.getBoundingClientRect(); return { top: rect.top, left: rect.left, bottom: window.innerHeight, right: window.innerWidth, }; }, }), ]; } ================================================ FILE: src/cm/colorView.ts ================================================ import type { Range, Text } from "@codemirror/state"; import type { DecorationSet, ViewUpdate } from "@codemirror/view"; import { Decoration, EditorView, ViewPlugin, WidgetType, } from "@codemirror/view"; import pickColor from "dialogs/color"; import color from "utils/color"; import { colorRegex, HEX } from "utils/color/regex"; interface ColorWidgetState { from: number; to: number; colorType: string; alpha?: string; } interface ColorWidgetParams extends ColorWidgetState { color: string; colorRaw: string; } // WeakMap to carry state from widget DOM back into handler const colorState = new WeakMap(); const HEX_RE = new RegExp(HEX, "gi"); const RGBG = new RegExp(colorRegex.anyGlobal); const enumColorType = { hex: "hex", rgb: "rgb", hsl: "hsl", named: "named" }; const disallowedBoundaryBefore = new Set(["-", ".", "/", "#"]); const disallowedBoundaryAfter = new Set(["-", ".", "/"]); const ignoredLeadingWords = new Set(["url"]); function isWhitespace(char: string): boolean { return ( char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f" ); } function isAlpha(char: string): boolean { if (!char) return false; const code = char.charCodeAt(0); return ( (code >= 65 && code <= 90) || // A-Z (code >= 97 && code <= 122) ); } function charAt(doc: Text, index: number): string { if (index < 0 || index >= doc.length) return ""; return doc.sliceString(index, index + 1); } function findPrevNonWhitespace(doc: Text, index: number): number { for (let i = index - 1; i >= 0; i--) { if (!isWhitespace(charAt(doc, i))) return i; } return -1; } function findNextNonWhitespace(doc: Text, index: number): number { for (let i = index; i < doc.length; i++) { if (!isWhitespace(charAt(doc, i))) return i; } return doc.length; } function readWordBefore(doc: Text, index: number): string { let pos = index; while (pos >= 0 && isWhitespace(charAt(doc, pos))) pos--; if (pos < 0) return ""; if (charAt(doc, pos) === "(") { pos--; } while (pos >= 0 && isWhitespace(charAt(doc, pos))) pos--; const end = pos; while (pos >= 0 && isAlpha(charAt(doc, pos))) pos--; const start = pos + 1; if (end < start) return ""; return doc.sliceString(start, end + 1).toLowerCase(); } function shouldRenderColor(doc: Text, start: number, end: number): boolean { const immediatePrev = charAt(doc, start - 1); if (disallowedBoundaryBefore.has(immediatePrev)) return false; const immediateNext = charAt(doc, end); if (disallowedBoundaryAfter.has(immediateNext)) return false; const prevNonWhitespaceIndex = findPrevNonWhitespace(doc, start); if (prevNonWhitespaceIndex !== -1) { const prevNonWhitespaceChar = charAt(doc, prevNonWhitespaceIndex); if (disallowedBoundaryBefore.has(prevNonWhitespaceChar)) return false; const prevWord = readWordBefore(doc, prevNonWhitespaceIndex); if (ignoredLeadingWords.has(prevWord)) return false; } const nextNonWhitespaceIndex = findNextNonWhitespace(doc, end); if (nextNonWhitespaceIndex < doc.length) { const nextNonWhitespaceChar = charAt(doc, nextNonWhitespaceIndex); if (disallowedBoundaryAfter.has(nextNonWhitespaceChar)) return false; } return true; } class ColorWidget extends WidgetType { state: ColorWidgetState; color: string; colorRaw: string; constructor({ color, colorRaw, ...state }: ColorWidgetParams) { super(); this.state = state; // from, to, colorType, alpha this.color = color; // hex for input value this.colorRaw = colorRaw; // original css color string } eq(other: ColorWidget): boolean { return ( other.state.colorType === this.state.colorType && other.color === this.color && other.state.from === this.state.from && other.state.to === this.state.to && (other.state.alpha || "") === (this.state.alpha || "") ); } toDOM(): HTMLElement { const wrapper = document.createElement("span"); wrapper.className = "cm-color-chip"; wrapper.style.display = "inline-block"; wrapper.style.width = "0.9em"; wrapper.style.height = "0.9em"; wrapper.style.borderRadius = "2px"; wrapper.style.verticalAlign = "middle"; wrapper.style.margin = "0 2px"; wrapper.style.boxSizing = "border-box"; wrapper.style.border = "1px solid rgba(0,0,0,0.2)"; wrapper.style.backgroundColor = this.colorRaw; wrapper.dataset["color"] = this.color; wrapper.dataset["colorraw"] = this.colorRaw; wrapper.style.cursor = "pointer"; colorState.set(wrapper, this.state); return wrapper; } ignoreEvent(): boolean { return false; } } function colorDecorations(view: EditorView): DecorationSet { const deco: Range[] = []; const ranges = view.visibleRanges; const doc = view.state.doc; for (const { from, to } of ranges) { const text = doc.sliceString(from, to); // Any color using global matcher from utils (captures named/rgb/rgba/hsl/hsla/hex) RGBG.lastIndex = 0; for (let m: RegExpExecArray | null; (m = RGBG.exec(text)); ) { const raw = m[2]; const start = from + m.index + m[1].length; const end = start + raw.length; if (!shouldRenderColor(doc, start, end)) continue; const c = color(raw); const colorHex = c.hex.toString(false); deco.push( Decoration.widget({ widget: new ColorWidget({ from: start, to: end, color: colorHex, colorRaw: raw, colorType: enumColorType.named, }), side: -1, }).range(start), ); } } return Decoration.set(deco, true); } class ColorViewPlugin { decorations: DecorationSet; raf = 0; pendingView: EditorView | null = null; constructor(view: EditorView) { this.decorations = colorDecorations(view); } update(update: ViewUpdate): void { if (update.docChanged || update.viewportChanged) { this.scheduleDecorations(update.view); } const readOnly = update.view.contentDOM.ariaReadOnly === "true"; const editable = update.view.contentDOM.contentEditable === "true"; const canBeEdited = readOnly === false && editable; this.changePicker(update.view, canBeEdited); } scheduleDecorations(view: EditorView): void { this.pendingView = view; if (this.raf) return; // Color chips are decorative, so batch rapid viewport/doc changes into // one animation frame instead of rebuilding on every intermediate update. this.raf = requestAnimationFrame(() => { this.raf = 0; const pendingView = this.pendingView; this.pendingView = null; if (!pendingView) return; this.decorations = colorDecorations(pendingView); }); } changePicker(view: EditorView, canBeEdited: boolean): void { const doms = view.contentDOM.querySelectorAll("input[type=color]"); doms.forEach((inp) => { const input = inp as HTMLInputElement; if (canBeEdited) { input.removeAttribute("disabled"); } else { input.setAttribute("disabled", ""); } }); } destroy(): void { if (this.raf) { cancelAnimationFrame(this.raf); this.raf = 0; } this.pendingView = null; } } export const colorView = (showPicker = true) => ViewPlugin.fromClass(ColorViewPlugin, { decorations: (v) => v.decorations, eventHandlers: { click: (e: PointerEvent, view: EditorView): boolean => { const target = e.target as HTMLElement | null; const chip = target?.closest?.(".cm-color-chip") as HTMLElement | null; if (!chip) return false; // Respect read-only and setting toggle const readOnly = view.contentDOM.ariaReadOnly === "true"; const editable = view.contentDOM.contentEditable === "true"; const canBeEdited = !readOnly && editable; if (!canBeEdited) return true; const data = colorState.get(chip); if (!data) return false; pickColor(chip.dataset.colorraw || chip.dataset.color || "") .then((picked: string | null) => { if (!picked) return; view.dispatch({ changes: { from: data.from, to: data.to, insert: picked }, }); }) .catch(() => { /* ignore */ }); return true; }, }, }); export default colorView; ================================================ FILE: src/cm/commandRegistry.js ================================================ import fsOperation from "fileSystem"; import * as cmCommands from "@codemirror/commands"; import { copyLineDown, copyLineUp, cursorCharLeft, cursorCharRight, cursorDocEnd, cursorDocStart, cursorGroupLeft, cursorGroupRight, cursorLineDown, cursorLineEnd, cursorLineStart, cursorLineUp, cursorMatchingBracket, cursorPageDown, cursorPageUp, deleteCharBackward, deleteCharForward, deleteGroupBackward, deleteGroupForward, deleteLine, deleteLineBoundaryForward, deleteToLineEnd, deleteToLineStart, indentLess, indentMore, indentSelection, insertBlankLine, insertNewlineAndIndent, lineComment, lineUncomment, moveLineDown, moveLineUp, redo, selectAll, selectCharLeft, selectCharRight, selectDocEnd, selectDocStart, selectGroupLeft, selectGroupRight, selectLine, selectLineDown, selectLineEnd, selectLineStart, selectLineUp, selectMatchingBracket, selectPageDown, selectPageUp, simplifySelection, toggleBlockComment, undo, } from "@codemirror/commands"; import { indentUnit as indentUnitFacet } from "@codemirror/language"; import { closeLintPanel, forceLinting, nextDiagnostic, openLintPanel, previousDiagnostic, } from "@codemirror/lint"; import { LSPPlugin, closeReferencePanel as lspCloseReferencePanel, findReferences as lspFindReferences, formatDocument as lspFormatDocument, jumpToDeclaration as lspJumpToDeclaration, jumpToDefinition as lspJumpToDefinition, jumpToImplementation as lspJumpToImplementation, jumpToTypeDefinition as lspJumpToTypeDefinition, } from "@codemirror/lsp-client"; import { Compartment, EditorSelection } from "@codemirror/state"; import { keymap } from "@codemirror/view"; import { renameSymbol as acodeRenameSymbol, clearDiagnosticsEffect, clientManager, nextSignature as lspNextSignature, prevSignature as lspPrevSignature, showSignatureHelp as lspShowSignatureHelp, } from "cm/lsp"; import { closeReferencesPanel as acodeCloseReferencesPanel, findAllReferences as acodeFindAllReferences, findAllReferencesInTab as acodeFindAllReferencesInTab, } from "cm/lsp/references"; import { showDocumentSymbols } from "components/symbolsPanel"; import toast from "components/toast"; import prompt from "dialogs/prompt"; import actions from "handlers/quickTools"; import keyBindings from "lib/keyBindings"; import settings from "lib/settings"; import Url from "utils/Url"; const commandKeymapCompartment = new Compartment(); /** * @typedef {import("@codemirror/view").EditorView} EditorView */ /** * @typedef {{ * name: string; * description?: string; * readOnly?: boolean; * run: (view?: EditorView | null) => boolean | void; * requiresView?: boolean; * defaultDescription?: string; * defaultKey?: string | null; * key?: string | null; * }} CommandEntry */ /** @type {Map} */ const commandMap = new Map(); /** @type {Record} */ let resolvedKeyBindings = keyBindings; /** @type {Record} */ let cachedResolvedKeyBindings = {}; let resolvedKeyBindingsVersion = 0; /** @type {import("@codemirror/view").KeyBinding[]} */ let cachedKeymap = []; const ARROW_KEY_MAP = { left: "ArrowLeft", right: "ArrowRight", up: "ArrowUp", down: "ArrowDown", }; const SPECIAL_KEY_MAP = { esc: "Escape", escape: "Escape", return: "Enter", enter: "Enter", space: "Space", del: "Delete", delete: "Delete", backspace: "Backspace", tab: "Tab", home: "Home", end: "End", pageup: "PageUp", pagedown: "PageDown", insert: "Insert", }; const MODIFIER_MAP = { ctrl: "Mod", control: "Mod", cmd: "Mod", meta: "Mod", shift: "Shift", alt: "Alt", option: "Alt", }; const CODEMIRROR_COMMAND_ENTRIES = Object.entries(cmCommands).filter( ([, value]) => typeof value === "function", ); const CODEMIRROR_COMMAND_MAP = new Map( CODEMIRROR_COMMAND_ENTRIES.map(([name, fn]) => [name, fn]), ); registerCoreCommands(); registerLspCommands(); registerLintCommands(); registerCommandsFromKeyBindings(); rebuildKeymap(); function registerCoreCommands() { addCommand({ name: "focusEditor", description: "Focus editor", readOnly: true, requiresView: false, run(view) { const resolvedView = resolveView(view); resolvedView?.focus(); return true; }, }); addCommand({ name: "findFile", description: "Find file in workspace", readOnly: true, requiresView: false, run() { acode.exec("find-file"); return true; }, }); addCommand({ name: "closeCurrentTab", description: "Close current tab", readOnly: false, requiresView: false, run() { acode.exec("close-current-tab"); return true; }, }); addCommand({ name: "closeAllTabs", description: "Close all tabs", readOnly: false, requiresView: false, run() { acode.exec("close-all-tabs"); return true; }, }); addCommand({ name: "togglePinnedTab", description: "Pin or unpin current tab", readOnly: true, requiresView: false, run() { acode.exec("toggle-pin-tab"); return true; }, }); addCommand({ name: "newFile", description: "Create new file", readOnly: true, requiresView: false, run() { acode.exec("new-file"); return true; }, }); addCommand({ name: "openFile", description: "Open a file", readOnly: true, requiresView: false, run() { acode.exec("open-file"); return true; }, }); addCommand({ name: "openFolder", description: "Open a folder", readOnly: true, requiresView: false, run() { acode.exec("open-folder"); return true; }, }); addCommand({ name: "saveFile", description: "Save current file", readOnly: true, requiresView: false, run() { acode.exec("save"); return true; }, }); addCommand({ name: "saveFileAs", description: "Save as current file", readOnly: true, requiresView: false, run() { acode.exec("save-as"); return true; }, }); addCommand({ name: "saveAllChanges", description: "Save all changes", readOnly: true, requiresView: false, run() { acode.exec("save-all-changes"); return true; }, }); addCommand({ name: "nextFile", description: "Open next file tab", readOnly: true, requiresView: false, run() { acode.exec("next-file"); return true; }, }); addCommand({ name: "prevFile", description: "Open previous file tab", readOnly: true, requiresView: false, run() { acode.exec("prev-file"); return true; }, }); addCommand({ name: "showSettingsMenu", description: "Show settings menu", readOnly: true, requiresView: false, run() { acode.exec("open", "settings"); return true; }, }); addCommand({ name: "renameFile", description: "Rename active file", readOnly: true, requiresView: false, run() { acode.exec("rename"); return true; }, }); addCommand({ name: "run", description: "Preview HTML and MarkDown", readOnly: true, requiresView: false, run() { acode.exec("run"); return true; }, }); addCommand({ name: "openInAppBrowser", description: "Open In-App Browser", readOnly: true, requiresView: false, run: openInAppBrowserCommand, }); addCommand({ name: "toggleFullscreen", description: "Toggle full screen mode", readOnly: true, requiresView: false, run() { acode.exec("toggle-fullscreen"); return true; }, }); addCommand({ name: "toggleSidebar", description: "Toggle sidebar", readOnly: true, requiresView: false, run() { acode.exec("toggle-sidebar"); return true; }, }); addCommand({ name: "toggleMenu", description: "Toggle main menu", readOnly: true, requiresView: false, run() { acode.exec("toggle-menu"); return true; }, }); addCommand({ name: "toggleEditMenu", description: "Toggle edit menu", readOnly: true, requiresView: false, run() { acode.exec("toggle-editmenu"); return true; }, }); addCommand({ name: "selectall", description: "Select all", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return selectAll(resolvedView); }, }); addCommand({ name: "gotoline", description: "Go to line...", readOnly: true, requiresView: false, run() { acode.exec("goto"); return true; }, }); addCommand({ name: "find", description: "Find", readOnly: true, requiresView: false, run() { acode.exec("find"); return true; }, }); addCommand({ name: "copy", description: "Copy", readOnly: true, requiresView: true, run: copyCommand, }); addCommand({ name: "cut", description: "Cut", readOnly: false, requiresView: true, run: cutCommand, }); addCommand({ name: "paste", description: "Paste", readOnly: false, requiresView: true, run: pasteCommand, }); addCommand({ name: "problems", description: "Show errors and warnings", readOnly: true, requiresView: false, run() { acode.exec("open", "problems"); return true; }, }); addCommand({ name: "replace", description: "Replace", readOnly: true, requiresView: false, run() { acode.exec("replace"); return true; }, }); addCommand({ name: "openCommandPalette", description: "Open command palette", readOnly: true, requiresView: false, run() { acode.exec("command-palette"); return true; }, }); addCommand({ name: "modeSelect", description: "Change language mode...", readOnly: true, requiresView: false, run() { acode.exec("syntax"); return true; }, }); addCommand({ name: "toggleQuickTools", description: "Toggle quick tools", readOnly: true, requiresView: false, run() { actions("toggle"); return true; }, }); addCommand({ name: "selectWord", description: "Select current word", readOnly: false, requiresView: true, run: selectWordCommand, }); addCommand({ name: "openLogFile", description: "Open Log File", readOnly: true, requiresView: false, run() { acode.exec("open-log-file"); return true; }, }); addCommand({ name: "increaseFontSize", description: "Increase font size", readOnly: false, requiresView: false, run: () => adjustFontSize(1), }); addCommand({ name: "decreaseFontSize", description: "Decrease font size", readOnly: false, requiresView: false, run: () => adjustFontSize(-1), }); addCommand({ name: "openPluginsPage", description: "Open Plugins Page", readOnly: true, requiresView: false, run() { acode.exec("open", "plugins"); return true; }, }); addCommand({ name: "openFileExplorer", description: "File Explorer", readOnly: true, requiresView: false, run() { acode.exec("open", "file_browser"); return true; }, }); addCommand({ name: "copyDeviceInfo", description: "Copy Device info", readOnly: true, requiresView: false, run() { acode.exec("copy-device-info"); return true; }, }); addCommand({ name: "changeAppTheme", description: "Change App Theme", readOnly: true, requiresView: false, run() { acode.exec("change-app-theme"); return true; }, }); addCommand({ name: "changeEditorTheme", description: "Change Editor Theme", readOnly: true, requiresView: false, run() { acode.exec("change-editor-theme"); return true; }, }); addCommand({ name: "openTerminal", description: "Open Terminal", readOnly: true, requiresView: false, run() { acode.exec("new-terminal"); return true; }, }); addCommand({ name: "acode:showWelcome", description: "Show Welcome", readOnly: true, requiresView: false, run() { acode.exec("welcome"); return true; }, }); addCommand({ name: "run-tests", description: "Run Tests", key: "Ctrl-Shift-T", readOnly: true, requiresView: false, run() { acode.exec("run-tests"); return true; }, }); addCommand({ name: "dev:openInspector", description: "Open Inspector", run() { acode.exec("open-inspector"); return true; }, readOnly: true, requiresView: false, }); addCommand({ name: "dev:toggleDevTools", description: "Toggle Developer Tools", run() { acode.exec("toggle-inspector"); return true; }, readOnly: true, requiresView: false, key: "Ctrl-Shift-I", }); // Additional editor-centric helpers mapped to CodeMirror primitives that have existing key bindings in defaults. addCommand({ name: "duplicateSelection", description: "Duplicate selection", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return copyLineDown(resolvedView); }, }); addCommand({ name: "copylinesdown", description: "Copy lines down", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return copyLineDown(resolvedView); }, }); addCommand({ name: "copylinesup", description: "Copy lines up", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return copyLineUp(resolvedView); }, }); addCommand({ name: "movelinesdown", description: "Move lines down", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return moveLineDown(resolvedView); }, }); addCommand({ name: "movelinesup", description: "Move lines up", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return moveLineUp(resolvedView); }, }); addCommand({ name: "removeline", description: "Remove line", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return deleteLine(resolvedView); }, }); addCommand({ name: "insertlineafter", description: "Insert line after", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return insertBlankLine(resolvedView); }, }); addCommand({ name: "selectline", description: "Select line", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return selectLine(resolvedView); }, }); addCommand({ name: "selectlinesdown", description: "Select line down", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return selectLineDown(resolvedView); }, }); addCommand({ name: "selectlinesup", description: "Select line up", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return selectLineUp(resolvedView); }, }); addCommand({ name: "selectlinestart", description: "Select line start", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return selectLineStart(resolvedView); }, }); addCommand({ name: "selectlineend", description: "Select line end", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return selectLineEnd(resolvedView); }, }); addCommand({ name: "indent", description: "Indent", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; const { state } = resolvedView; const hasSelection = state.selection.ranges.some((range) => !range.empty); if (hasSelection) { return indentMore(resolvedView); } const indentString = state.facet(indentUnitFacet) || (settings?.value?.softTab ? " ".repeat(Math.max(1, Number(settings?.value?.tabSize) || 2)) : "\t"); const insert = indentString && indentString.length ? indentString : "\t"; resolvedView.dispatch( state.changeByRange((range) => ({ changes: { from: range.from, to: range.to, insert }, range: EditorSelection.cursor(range.from + insert.length), })), ); return true; }, }); addCommand({ name: "outdent", description: "Outdent", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return indentLess(resolvedView); }, }); addCommand({ name: "indentselection", description: "Indent selection", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return indentSelection(resolvedView); }, }); addCommand({ name: "newline", description: "Insert newline", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return insertNewlineAndIndent(resolvedView); }, }); addCommand({ name: "joinlines", description: "Join lines", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return deleteLineBoundaryForward(resolvedView); }, }); addCommand({ name: "deletetolinestart", description: "Delete to line start", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return deleteToLineStart(resolvedView); }, }); addCommand({ name: "deletetolineend", description: "Delete to line end", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return deleteToLineEnd(resolvedView); }, }); addCommand({ name: "togglecomment", description: "Toggle comment", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return lineComment(resolvedView); }, }); addCommand({ name: "comment", description: "Add line comment", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return lineComment(resolvedView); }, }); addCommand({ name: "uncomment", description: "Remove line comment", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return lineUncomment(resolvedView); }, }); addCommand({ name: "toggleBlockComment", description: "Toggle block comment", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return toggleBlockComment(resolvedView); }, }); addCommand({ name: "undo", description: "Undo", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return undo(resolvedView); }, }); addCommand({ name: "redo", description: "Redo", readOnly: false, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return redo(resolvedView); }, }); addCommand({ name: "simplifySelection", description: "Simplify selection", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return simplifySelection(resolvedView); }, }); } function registerLspCommands() { addCommand({ name: "formatDocument", description: "Format document (Language Server)", readOnly: false, requiresView: true, run: runLspCommand(lspFormatDocument), }); addCommand({ name: "renameSymbol", description: "Rename symbol (Language Server)", readOnly: false, requiresView: true, run: runLspCommand(acodeRenameSymbol), }); addCommand({ name: "showSignatureHelp", description: "Show signature help", readOnly: true, requiresView: true, run: runLspCommand(lspShowSignatureHelp), }); addCommand({ name: "nextSignature", description: "Next signature", readOnly: true, requiresView: true, run: runLspCommand(lspNextSignature, { silentOnMissing: true }), }); addCommand({ name: "prevSignature", description: "Previous signature", readOnly: true, requiresView: true, run: runLspCommand(lspPrevSignature, { silentOnMissing: true }), }); addCommand({ name: "jumpToDefinition", description: "Go to definition (Language Server)", readOnly: true, requiresView: true, run: runLspCommand(lspJumpToDefinition), }); addCommand({ name: "jumpToDeclaration", description: "Go to declaration (Language Server)", readOnly: true, requiresView: true, run: runLspCommand(lspJumpToDeclaration), }); addCommand({ name: "jumpToTypeDefinition", description: "Go to type definition (Language Server)", readOnly: true, requiresView: true, run: runLspCommand(lspJumpToTypeDefinition), }); addCommand({ name: "jumpToImplementation", description: "Go to implementation (Language Server)", readOnly: true, requiresView: true, run: runLspCommand(lspJumpToImplementation), }); addCommand({ name: "findReferences", description: "Find all references (Language Server)", readOnly: true, requiresView: true, async run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; const plugin = LSPPlugin.get(resolvedView); if (!plugin) { notifyLspUnavailable(); return false; } return acodeFindAllReferences(resolvedView); }, }); addCommand({ name: "closeReferencePanel", description: "Close references panel", readOnly: true, requiresView: false, run() { return acodeCloseReferencesPanel(); }, }); addCommand({ name: "findReferencesInTab", description: "Find all references in new tab (Language Server)", readOnly: true, requiresView: true, async run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; const plugin = LSPPlugin.get(resolvedView); if (!plugin) { notifyLspUnavailable(); return false; } return acodeFindAllReferencesInTab(resolvedView); }, }); addCommand({ name: "restartAllLspServers", description: "Restart all running LSP servers", readOnly: true, requiresView: false, async run() { const activeClients = clientManager.getActiveClients(); if (!activeClients.length) { toast("No LSP servers are currently running"); return true; } const count = activeClients.length; toast(`Restarting ${count} LSP server${count > 1 ? "s" : ""}...`); // Dispose all clients (also clears diagnostics) await clientManager.dispose(); // Trigger reconnect for active file editorManager?.restartLsp?.(); return true; }, }); addCommand({ name: "stopAllLspServers", description: "Stop all running LSP servers", readOnly: true, requiresView: false, async run() { const activeClients = clientManager.getActiveClients(); if (!activeClients.length) { toast("No LSP servers are currently running"); return true; } const count = activeClients.length; // Dispose all clients await clientManager.dispose(); toast(`Stopped ${count} LSP server${count > 1 ? "s" : ""}`); return true; }, }); addCommand({ name: "documentSymbols", description: "Go to Symbol in Document...", readOnly: true, requiresView: true, async run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return showDocumentSymbols(resolvedView); }, }); } function registerLintCommands() { addCommand({ name: "openLintPanel", description: "Open lint panel", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return openLintPanel(resolvedView); }, }); addCommand({ name: "closeLintPanel", description: "Close lint panel", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return closeLintPanel(resolvedView); }, }); addCommand({ name: "nextDiagnostic", description: "Go to next diagnostic", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return nextDiagnostic(resolvedView); }, }); addCommand({ name: "previousDiagnostic", description: "Go to previous diagnostic", readOnly: true, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return previousDiagnostic(resolvedView); }, }); } function registerCommandsFromKeyBindings() { Object.entries(keyBindings).forEach(([name, binding]) => { if (commandMap.has(name)) return; const description = binding?.description || humanizeCommandName(name); const readOnly = binding?.readOnly ?? false; const requiresView = !!binding?.editorOnly; const commandFn = CODEMIRROR_COMMAND_MAP.get(name); if (binding?.action) { addCommand({ name, description, readOnly, requiresView, run(view) { try { if (requiresView) { const resolvedView = resolveView(view); if (!resolvedView) return false; } acode.exec(binding.action); return true; } catch (error) { console.error(`Failed to execute action ${binding.action}`, error); return false; } }, }); return; } if (commandFn) { addCommand({ name, description, readOnly, requiresView: true, run(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; return commandFn(resolvedView); }, }); } }); } function addCommand(entry) { const command = { ...entry, defaultDescription: entry.description || entry.name, defaultKey: entry.key ?? null, key: entry.key ?? null, }; commandMap.set(entry.name, command); } function resolveView(view) { return view || editorManager?.editor || null; } function notifyLspUnavailable() { toast?.("Language server not available"); } function runLspCommand(commandFn, options = {}) { return (view) => { const resolvedView = resolveView(view); if (!resolvedView) return false; const plugin = LSPPlugin.get(resolvedView); if (!plugin) { if (!options?.silentOnMissing) { notifyLspUnavailable(); } return false; } const result = commandFn(resolvedView); return result !== false; }; } function humanizeCommandName(name) { return name .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/_/g, " ") .replace(/^./, (char) => char.toUpperCase()); } function copyCommand(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; const { state } = resolvedView; const texts = state.selection.ranges.map((range) => { if (range.empty) { const line = state.doc.lineAt(range.head); return state.doc.sliceString(line.from, line.to); } return state.doc.sliceString(range.from, range.to); }); const textToCopy = texts.join("\n"); cordova.plugins.clipboard.copy(textToCopy); toast?.(strings?.["copied to clipboard"] || "Copied to clipboard"); return true; } function cutCommand(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; const { state } = resolvedView; const ranges = state.selection.ranges; const segments = []; let changes = []; ranges.forEach((range) => { if (range.empty) { const line = state.doc.lineAt(range.head); segments.push(state.doc.sliceString(line.from, line.to)); changes.push({ from: line.from, to: line.to, insert: "" }); return; } segments.push(state.doc.sliceString(range.from, range.to)); changes.push({ from: range.from, to: range.to, insert: "" }); }); cordova.plugins.clipboard.copy(segments.join("\n")); resolvedView.dispatch({ changes, selection: EditorSelection.single( changes[0]?.from ?? state.selection.main.from, ), }); return true; } function pasteCommand(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; cordova.plugins.clipboard.paste((text = "") => { const insertText = String(text); resolvedView.dispatch( resolvedView.state.changeByRange((range) => ({ changes: { from: range.from, to: range.to, insert: insertText }, range: EditorSelection.cursor(range.from + insertText.length), })), ); }); return true; } function selectWordCommand(view) { const resolvedView = resolveView(view); if (!resolvedView) return false; const { state } = resolvedView; const ranges = state.selection.ranges.map((range) => { const word = state.wordAt(range.head); if (word) return EditorSelection.range(word.from, word.to); const line = state.doc.lineAt(range.head); return EditorSelection.range(line.from, line.to); }); resolvedView.dispatch({ selection: EditorSelection.create(ranges, state.selection.mainIndex), }); return true; } async function openInAppBrowserCommand() { const url = await prompt("Enter url", "", "url", { placeholder: "http://", match: /^https?:\/\/.+/, }); if (url) acode.exec("open-inapp-browser", url); return true; } function adjustFontSize(delta) { const current = settings?.value?.fontSize || "12px"; const numeric = Number.parseInt(current, 10) || 12; const next = Math.max(1, numeric + delta); settings.value.fontSize = `${next}px`; settings.update(false); return true; } function parseKeyString(keyString) { if (!keyString) return []; return String(keyString) .split("|") .map((combo) => combo.trim()) .filter(Boolean); } function hasOwnBindingOverride(name) { return Object.prototype.hasOwnProperty.call(resolvedKeyBindings ?? {}, name); } function resolveBindingInfo(name) { const baseBinding = keyBindings[name] ?? null; if (!hasOwnBindingOverride(name)) return baseBinding; const override = resolvedKeyBindings?.[name]; if (override === null) { return baseBinding ? { ...baseBinding, key: null } : { key: null }; } if (!override || typeof override !== "object") { return baseBinding; } return baseBinding ? { ...baseBinding, ...override } : override; } function buildResolvedKeyBindingsSnapshot() { const bindingNames = new Set([ ...Object.keys(keyBindings), ...Object.keys(resolvedKeyBindings ?? {}), ]); return Object.fromEntries( Array.from(bindingNames, (name) => [name, resolveBindingInfo(name)]).filter( ([, binding]) => binding, ), ); } function toCodeMirrorKey(combo) { if (!combo) return null; const parts = combo .split("-") .map((part) => part.trim()) .filter(Boolean); const modifiers = []; let key = null; parts.forEach((part, index) => { const lower = part.toLowerCase(); if (MODIFIER_MAP[lower]) { const mod = MODIFIER_MAP[lower]; if (!modifiers.includes(mod)) modifiers.push(mod); return; } if (ARROW_KEY_MAP[lower]) { key = ARROW_KEY_MAP[lower]; return; } if (SPECIAL_KEY_MAP[lower]) { key = SPECIAL_KEY_MAP[lower]; return; } if (part.length === 1 && /[a-z]/i.test(part)) { key = part.length === 1 ? part.toLowerCase() : part; return; } key = part; }); if (!key) return modifiers.join("-") || null; return modifiers.length ? `${modifiers.join("-")}-${key}` : key; } function rebuildKeymap() { const bindings = []; cachedResolvedKeyBindings = buildResolvedKeyBindingsSnapshot(); commandMap.forEach((command, name) => { const bindingInfo = resolveBindingInfo(name); command.description = bindingInfo?.description || command.defaultDescription; const keySource = bindingInfo && Object.prototype.hasOwnProperty.call(bindingInfo, "key") ? bindingInfo.key : (command.defaultKey ?? null); command.key = keySource; const combos = parseKeyString(keySource); combos.forEach((combo) => { const cmKey = toCodeMirrorKey(combo); if (!cmKey) return; bindings.push({ key: cmKey, run: (view) => executeCommand(name, view), preventDefault: true, }); }); }); cachedKeymap = bindings; resolvedKeyBindingsVersion += 1; return bindings; } function resolveCommand(name) { return commandMap.get(name) || null; } function commandRunsInReadOnly(command, view) { if (!view) return command.readOnly; return view.state?.readOnly ? !!command.readOnly : true; } export function executeCommand(name, view, args) { const command = resolveCommand(name); if (!command) return false; const targetView = command.requiresView ? resolveView(view) : resolveView(view) || null; if (command.requiresView && !targetView) return false; if (!commandRunsInReadOnly(command, targetView)) return false; try { const result = command.run(targetView, args); return result !== false; } catch (error) { console.error(`Failed to execute command ${name}`, error); return false; } } export function getRegisteredCommands() { return Array.from(commandMap.values()).map((command) => ({ name: command.name, description: command.description || command.defaultDescription, key: command.key || null, })); } export function getResolvedKeyBindings() { return cachedResolvedKeyBindings; } export function getResolvedKeyBindingsVersion() { return resolvedKeyBindingsVersion; } export function getCommandKeymapExtension() { return commandKeymapCompartment.of(keymap.of(cachedKeymap)); } export async function setKeyBindings(view) { await loadCustomKeyBindings(); const bindings = rebuildKeymap(); const resolvedView = resolveView(view); applyCommandKeymap(resolvedView, bindings); } async function loadCustomKeyBindings() { try { const bindingsFile = fsOperation(KEYBINDING_FILE); if (await bindingsFile.exists()) { const bindings = await bindingsFile.readFile("json"); if (bindings && typeof bindings === "object") { resolvedKeyBindings = bindings; } } else { throw new Error("Key binding file not found"); } } catch (error) { await resetKeyBindings(); resolvedKeyBindings = keyBindings; } } export async function resetKeyBindings() { try { const fs = fsOperation(KEYBINDING_FILE); const fileName = Url.basename(KEYBINDING_FILE); const content = JSON.stringify(keyBindings, undefined, 2); if (!(await fs.exists())) { await fsOperation(DATA_STORAGE).createFile(fileName, content); return; } await fs.writeFile(content); } catch (error) { window.log?.("error", "Reset Keybinding failed!"); window.log?.("error", error); } } export { commandKeymapCompartment }; export function registerExternalCommand(descriptor = {}) { const normalized = normalizeExternalCommand(descriptor); if (!normalized) return null; const { name } = normalized; if (commandMap.has(name)) { commandMap.delete(name); } addCommand(normalized); const stored = commandMap.get(name); if (stored) { stored.key = normalized.key ?? stored.key; } rebuildKeymap(); return stored; } export function removeExternalCommand(name) { if (!name) return false; const exists = commandMap.has(name); if (!exists) return false; commandMap.delete(name); rebuildKeymap(); return true; } export function refreshCommandKeymap(view) { const resolvedView = resolveView(view); applyCommandKeymap(resolvedView); } function normalizeExternalCommand(descriptor) { const name = typeof descriptor?.name === "string" ? descriptor.name.trim() : ""; if (!name) { console.warn("Command registration skipped: missing name", descriptor); return null; } const exec = typeof descriptor?.exec === "function" ? descriptor.exec : null; if (!exec) { console.warn( `Command registration skipped for "${name}": exec must be a function.`, ); return null; } const requiresView = descriptor?.requiresView ?? true; const key = normalizeExternalKey(descriptor?.bindKey); return { name, description: descriptor?.description || humanizeCommandName(name), readOnly: descriptor?.readOnly ?? true, requiresView, key, run(view, args) { try { const resolvedView = resolveView(view); if (requiresView && !resolvedView) return false; const result = exec(resolvedView || null, args); return result !== false; } catch (error) { console.error(`Command \"${name}\" failed`, error); return false; } }, }; } function normalizeExternalKey(bindKey) { if (!bindKey) return null; if (typeof bindKey === "string") return bindKey; const combos = []; if (typeof bindKey === "object") { const pushCombo = (combo) => { if (typeof combo === "string" && combo.trim()) combos.push(combo.trim()); }; pushCombo(bindKey.win); pushCombo(bindKey.linux); pushCombo(bindKey.mac); } return combos.length ? combos.join("|") : null; } function applyCommandKeymap(view, bindings = cachedKeymap) { if (!view) return; view.dispatch({ effects: commandKeymapCompartment.reconfigure( keymap.of(bindings ?? cachedKeymap), ), }); } ================================================ FILE: src/cm/editorUtils.ts ================================================ import { foldEffect, foldedRanges } from "@codemirror/language"; import type { EditorState, StateEffect } from "@codemirror/state"; import { EditorSelection } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; export interface FoldSpan { fromLine: number; fromCol: number; toLine: number; toCol: number; } export interface SelectionRange { from: number; to: number; } export interface SerializedSelection { ranges: SelectionRange[]; mainIndex: number; } export interface ScrollPosition { scrollTop: number; scrollLeft: number; } /** * Get all folded ranges from CodeMirror editor state */ export function getAllFolds(state: EditorState): FoldSpan[] { const doc = state.doc; const folds: FoldSpan[] = []; foldedRanges(state).between(0, doc.length, (from, to) => { const fromPos = doc.lineAt(from); const toPos = doc.lineAt(to); folds.push({ fromLine: fromPos.number, fromCol: from - fromPos.from, toLine: toPos.number, toCol: to - toPos.from, }); }); return folds; } /** * Get current selection from editor view */ export function getSelection(view: EditorView): SerializedSelection { const sel = view.state.selection; return { ranges: sel.ranges.map((r) => ({ from: r.from, to: r.to })), mainIndex: sel.mainIndex, }; } /** * Get scroll position from editor view */ export function getScrollPosition(view: EditorView): ScrollPosition { const { scrollTop, scrollLeft } = view.scrollDOM; return { scrollTop, scrollLeft }; } /** * Set scroll position in CodeMirror editor view */ export function setScrollPosition( view: EditorView, scrollTop?: number, scrollLeft?: number, ): void { const scroller = view.scrollDOM; if (typeof scrollTop === "number") { scroller.scrollTop = scrollTop; } if (typeof scrollLeft === "number") { scroller.scrollLeft = scrollLeft; } } /** * Restore selection to editor view */ export function restoreSelection( view: EditorView, sel: SerializedSelection | null | undefined, ): void { if (!sel || !sel.ranges || !sel.ranges.length) return; const len = view.state.doc.length; const ranges = sel.ranges .map((r) => { const from = Math.max(0, Math.min(len, r.from | 0)); const to = Math.max(0, Math.min(len, r.to | 0)); return EditorSelection.range(from, to); }) .filter(Boolean); if (!ranges.length) return; const mainIndex = sel.mainIndex >= 0 && sel.mainIndex < ranges.length ? sel.mainIndex : 0; view.dispatch({ selection: EditorSelection.create(ranges, mainIndex), scrollIntoView: true, }); } /** * Restore folds to CodeMirror editor */ export function restoreFolds( view: EditorView, folds: FoldSpan[] | null | undefined, ): void { if (!Array.isArray(folds) || folds.length === 0) return; function lineColToOffset( doc: EditorState["doc"], line: number, col: number, ): number { const ln = doc.line(line); return Math.min(ln.from + col, ln.to); } function loadFolds( state: EditorState, saved: FoldSpan[], ): StateEffect<{ from: number; to: number }>[] { const doc = state.doc; const effects: StateEffect<{ from: number; to: number }>[] = []; for (const f of saved) { // Validate line numbers if (f.fromLine < 1 || f.fromLine > doc.lines) continue; if (f.toLine < 1 || f.toLine > doc.lines) continue; const from = lineColToOffset(doc, f.fromLine, f.fromCol); const to = lineColToOffset(doc, f.toLine, f.toCol); if (to > from) { effects.push(foldEffect.of({ from, to })); } } return effects; } const restoreEffects = loadFolds(view.state, folds); if (restoreEffects.length) { view.dispatch({ effects: restoreEffects }); } } /** * Clear selection, keeping only cursor position */ export function clearSelection(view: EditorView): void { view.dispatch({ selection: EditorSelection.single(view.state.selection.main.head), scrollIntoView: true, }); // Also clear the global DOM selection to prevent native selection handles/menus persisting try { document.getSelection()?.removeAllRanges(); } catch (_) { // Ignore errors } } export default { getAllFolds, getSelection, getScrollPosition, setScrollPosition, restoreSelection, restoreFolds, clearSelection, }; ================================================ FILE: src/cm/indentGuides.ts ================================================ import { getIndentUnit, syntaxTree } from "@codemirror/language"; import type { Extension } from "@codemirror/state"; import { EditorState, RangeSetBuilder } from "@codemirror/state"; import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, } from "@codemirror/view"; import type { SyntaxNode } from "@lezer/common"; /** * Configuration options for indent guides */ export interface IndentGuidesConfig { /** Whether to highlight the guide at the cursor's indent level */ highlightActiveGuide?: boolean; /** Whether to hide guides on blank lines */ hideOnBlankLines?: boolean; } const defaultConfig: Required = { highlightActiveGuide: true, hideOnBlankLines: false, }; const GUIDE_MARK_CLASS = "cm-indent-guides"; /** * Get the tab size from editor state */ function getTabSize(state: EditorState): number { const tabSize = state.facet(EditorState.tabSize); return Number.isFinite(tabSize) && tabSize > 0 ? tabSize : 4; } /** * Resolve the indentation width used for guide spacing. */ function getIndentUnitColumns(state: EditorState): number { const width = getIndentUnit(state); if (Number.isFinite(width) && width > 0) return width; return getTabSize(state); } /** * Calculate the visual indentation of a line */ function getLineIndentation(line: string, tabSize: number): number { let columns = 0; for (const ch of line) { if (ch === " ") { columns++; } else if (ch === "\t") { columns += tabSize - (columns % tabSize); } else { break; } } return columns; } /** * Check if a line is blank */ function isBlankLine(line: string): boolean { return /^\s*$/.test(line); } /** * Count the leading indentation characters of a line. */ function getLeadingWhitespaceLength(line: string): number { let count = 0; for (const ch of line) { if (ch === " " || ch === "\t") { count++; continue; } break; } return count; } /** * Node types that represent scope blocks in various languages */ const SCOPE_NODE_TYPES = new Set([ "Block", "ObjectExpression", "ArrayExpression", "ArrowFunction", "FunctionDeclaration", "FunctionExpression", "ClassBody", "ClassDeclaration", "MethodDeclaration", "SwitchBody", "IfStatement", "WhileStatement", "ForStatement", "ForInStatement", "ForOfStatement", "TryStatement", "CatchClause", "Object", "Array", "Element", "SelfClosingTag", "RuleSet", "DeclarationList", "Body", "Suite", "Program", "Script", "Module", ]); /** * Information about the active scope for highlighting */ interface ActiveScope { level: number; startLine: number; endLine: number; } /** * Find the active scope using syntax tree analysis */ function getActiveScope( view: EditorView, indentUnit: number, ): ActiveScope | null { const { state } = view; const { main } = state.selection; const cursorPos = main.head; const tree = syntaxTree(state); if (!tree || tree.length === 0) { return getActiveScopeByIndentation(state, indentUnit); } let scopeNode: SyntaxNode | null = null; let node: SyntaxNode | null = tree.resolveInner(cursorPos, 0); while (node) { if (SCOPE_NODE_TYPES.has(node.name)) { scopeNode = node; break; } node = node.parent; } if (!scopeNode) { return null; } const startLine = state.doc.lineAt(scopeNode.from); const endLine = state.doc.lineAt(scopeNode.to); let contentStartLine = startLine.number; if (startLine.number < endLine.number) { contentStartLine = startLine.number + 1; } const tabSize = getTabSize(state); let level = 0; for (let ln = contentStartLine; ln <= endLine.number; ln++) { const line = state.doc.line(ln); if (!isBlankLine(line.text)) { const indent = getLineIndentation(line.text, tabSize); level = Math.floor(indent / indentUnit); break; } } if (level <= 0) { return null; } return { level, startLine: startLine.number, endLine: endLine.number, }; } /** * Fallback: Find active scope by indentation when no syntax tree is available */ function getActiveScopeByIndentation( state: EditorState, indentUnit: number, ): ActiveScope | null { const { main } = state.selection; const cursorLine = state.doc.lineAt(main.head); const tabSize = getTabSize(state); let cursorIndent = getLineIndentation(cursorLine.text, tabSize); if (isBlankLine(cursorLine.text)) { for (let lineNum = cursorLine.number - 1; lineNum >= 1; lineNum--) { const prevLine = state.doc.line(lineNum); if (!isBlankLine(prevLine.text)) { cursorIndent = getLineIndentation(prevLine.text, tabSize); break; } } } const cursorLevel = Math.floor(cursorIndent / indentUnit); if (cursorLevel <= 0) return null; let startLine = cursorLine.number; for (let lineNum = cursorLine.number - 1; lineNum >= 1; lineNum--) { const line = state.doc.line(lineNum); if (isBlankLine(line.text)) continue; const lineLevel = Math.floor( getLineIndentation(line.text, tabSize) / indentUnit, ); if (lineLevel < cursorLevel) break; startLine = lineNum; } let endLine = cursorLine.number; for ( let lineNum = cursorLine.number + 1; lineNum <= state.doc.lines; lineNum++ ) { const line = state.doc.line(lineNum); if (isBlankLine(line.text)) { endLine = lineNum; continue; } const lineLevel = Math.floor( getLineIndentation(line.text, tabSize) / indentUnit, ); if (lineLevel < cursorLevel) break; endLine = lineNum; } return { level: cursorLevel, startLine, endLine }; } function buildGuideStyle( levels: number, guideStepPx: number, activeGuideIndex: number, ): string { const images = []; const positions = []; const sizes = []; for (let i = 0; i < levels; i++) { const color = i === activeGuideIndex ? "var(--indent-guide-active-color)" : "var(--indent-guide-color)"; images.push(`linear-gradient(${color}, ${color})`); positions.push(`${i * guideStepPx}px 0`); sizes.push("1px 100%"); } return [ `background-image:${images.join(",")}`, "background-repeat:no-repeat", `background-position:${positions.join(",")}`, `background-size:${sizes.join(",")}`, ].join(";"); } /** * Build decorations for indent guides */ function buildDecorations( view: EditorView, config: Required, ): DecorationSet { const builder = new RangeSetBuilder(); const { state } = view; const tabSize = getTabSize(state); const indentUnit = getIndentUnitColumns(state); const guideStepPx = Math.max(view.defaultCharacterWidth * indentUnit, 1); const activeScope = config.highlightActiveGuide ? getActiveScope(view, indentUnit) : null; for (const { from: blockFrom, to: blockTo } of view.visibleRanges) { const startLine = state.doc.lineAt(blockFrom); const endLine = state.doc.lineAt(blockTo); for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) { const line = state.doc.line(lineNum); const lineText = line.text; if (config.hideOnBlankLines && isBlankLine(lineText)) { continue; } const indentColumns = getLineIndentation(lineText, tabSize); const levels = Math.floor(indentColumns / indentUnit); if (levels <= 0) continue; const leadingWhitespaceLength = getLeadingWhitespaceLength(lineText); if (leadingWhitespaceLength <= 0) continue; let activeGuideIndex = -1; if ( activeScope && lineNum >= activeScope.startLine && lineNum <= activeScope.endLine && levels >= activeScope.level ) { activeGuideIndex = activeScope.level - 1; } builder.add( line.from, line.from + leadingWhitespaceLength, Decoration.mark({ attributes: { class: GUIDE_MARK_CLASS, style: buildGuideStyle(levels, guideStepPx, activeGuideIndex), }, }), ); } } return builder.finish(); } /** * ViewPlugin for indent guides */ function createIndentGuidesPlugin( config: Required, ): ViewPlugin<{ decorations: DecorationSet; update(update: ViewUpdate): void; }> { return ViewPlugin.fromClass( class { decorations: DecorationSet; raf = 0; pendingView: EditorView | null = null; constructor(view: EditorView) { this.decorations = buildDecorations(view, config); } update(update: ViewUpdate): void { if ( !update.docChanged && !update.viewportChanged && !update.geometryChanged && !(config.highlightActiveGuide && update.selectionSet) ) { return; } this.scheduleBuild(update.view); } scheduleBuild(view: EditorView): void { this.pendingView = view; if (this.raf) return; // Guide rebuilding is cosmetic and can be expensive on large // viewports, so we intentionally collapse bursts into one frame. this.raf = requestAnimationFrame(() => { this.raf = 0; const pendingView = this.pendingView; this.pendingView = null; if (!pendingView) return; this.decorations = buildDecorations(pendingView, config); }); } destroy(): void { if (this.raf) { cancelAnimationFrame(this.raf); this.raf = 0; } this.pendingView = null; } }, { decorations: (v) => v.decorations, }, ); } /** * Theme for indent guides. * Uses a single span around leading indentation instead of per-guide widgets. */ const indentGuidesTheme = EditorView.baseTheme({ ".cm-indent-guides": { display: "inline-block", verticalAlign: "top", }, "&": { "--indent-guide-color": "rgba(128, 128, 128, 0.25)", "--indent-guide-active-color": "rgba(128, 128, 128, 0.7)", }, "&light": { "--indent-guide-color": "rgba(0, 0, 0, 0.1)", "--indent-guide-active-color": "rgba(0, 0, 0, 0.4)", }, "&dark": { "--indent-guide-color": "rgba(255, 255, 255, 0.1)", "--indent-guide-active-color": "rgba(255, 255, 255, 0.4)", }, }); export function indentGuides(config: IndentGuidesConfig = {}): Extension { const mergedConfig: Required = { ...defaultConfig, ...config, }; return [createIndentGuidesPlugin(mergedConfig), indentGuidesTheme]; } export function indentGuidesExtension( enabled: boolean, config: IndentGuidesConfig = {}, ): Extension { if (!enabled) return []; return indentGuides(config); } export default indentGuides; ================================================ FILE: src/cm/lsp/api.ts ================================================ import { defineBundle, defineServer, installers } from "./providerUtils"; import { getServerBundle, listServerBundles, registerServerBundle, unregisterServerBundle, } from "./serverCatalog"; import { getServer, getServersForLanguage, listServers, onRegistryChange, type RegisterServerOptions, registerServer, type ServerUpdater, unregisterServer, updateServer, } from "./serverRegistry"; import type { LspServerBundle, LspServerDefinition, LspServerManifest, } from "./types"; export { defineBundle, defineServer, installers }; export type LspRegistrationEntry = LspServerManifest | LspServerBundle; function isBundleEntry(entry: LspRegistrationEntry): entry is LspServerBundle { return typeof (entry as LspServerBundle)?.getServers === "function"; } export function register( entry: LspRegistrationEntry, options?: RegisterServerOptions & { replace?: boolean }, ): LspServerDefinition | LspServerBundle { if (isBundleEntry(entry)) { return registerServerBundle(entry, options); } return registerServer(entry, options); } export function upsert( entry: LspRegistrationEntry, ): LspServerDefinition | LspServerBundle { return register(entry, { replace: true }); } export const servers = { get(id: string): LspServerDefinition | null { return getServer(id); }, list(): LspServerDefinition[] { return listServers(); }, listForLanguage( languageId: string, options?: { includeDisabled?: boolean }, ): LspServerDefinition[] { return getServersForLanguage(languageId, options); }, update(id: string, updater: ServerUpdater): LspServerDefinition | null { return updateServer(id, updater); }, unregister(id: string): boolean { return unregisterServer(id); }, onChange(listener: Parameters[0]): () => void { return onRegistryChange(listener); }, }; export const bundles = { list(): LspServerBundle[] { return listServerBundles(); }, getForServer(id: string): LspServerBundle | null { return getServerBundle(id); }, unregister(id: string): boolean { return unregisterServerBundle(id); }, }; const lspApi = { defineServer, defineBundle, register, upsert, installers, servers, bundles, }; export default lspApi; ================================================ FILE: src/cm/lsp/clientManager.ts ================================================ import { getIndentUnit, indentUnit } from "@codemirror/language"; import type { LSPClientExtension } from "@codemirror/lsp-client"; import { findReferencesKeymap, formatKeymap, jumpToDefinitionKeymap, LSPClient, LSPPlugin, serverCompletion, serverDiagnostics, } from "@codemirror/lsp-client"; import { EditorState, Extension, MapMode } from "@codemirror/state"; import { EditorView, keymap } from "@codemirror/view"; import lspStatusBar from "components/lspStatusBar"; import NotificationManager from "lib/notificationManager"; import Uri from "utils/Uri"; import { clearDiagnosticsEffect } from "./diagnostics"; import { supportsBuiltinFormatting } from "./formattingSupport"; import { inlayHintsExtension } from "./inlayHints"; import { acodeRenameKeymap } from "./rename"; import { ensureServerRunning } from "./serverLauncher"; import serverRegistry from "./serverRegistry"; import { hoverTooltips, signatureHelp } from "./tooltipExtensions"; import { createTransport } from "./transport"; import type { BuiltinExtensionsConfig, ClientManagerOptions, ClientState, DocumentUriContext, FileMetadata, FormattingOptions, LspServerDefinition, NormalizedRootUri, ParsedUri, RootUriContext, TextEdit, Transport, TransportHandle, } from "./types"; import AcodeWorkspace from "./workspace"; function asArray(value: T | T[] | null | undefined): T[] { if (!value) return []; return Array.isArray(value) ? value : [value]; } function pluginKey( serverId: string, rootUri: string | null | undefined, useWorkspaceFolders?: boolean, ): string { // For workspace folders mode, use just the server ID (one client per server type) if (useWorkspaceFolders) { return serverId; } return `${serverId}::${rootUri ?? "__global__"}`; } function safeString(value: unknown): string { return value != null ? String(value) : ""; } function isVerboseLspLoggingEnabled(): boolean { const buildInfo = (globalThis as { BuildInfo?: { debug?: boolean } }) .BuildInfo; return !!buildInfo?.debug; } function logLspInfo(...args: unknown[]): void { if (!isVerboseLspLoggingEnabled()) return; console.info(...args); } function isPlainObject(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } function resolveInitializationOptions( server: LspServerDefinition, clientConfig: Record, ): Record | undefined { const serverOptions = isPlainObject(server.initializationOptions) ? server.initializationOptions : null; const clientOptions = isPlainObject(clientConfig.initializationOptions) ? clientConfig.initializationOptions : null; if (serverOptions && clientOptions) { return { ...serverOptions, ...clientOptions, }; } return serverOptions || clientOptions || undefined; } interface InternalLSPRequest { promise: Promise; } type RequestInnerFn = ( method: string, params: Params, mapped?: boolean, ) => InternalLSPRequest; function connectClient( client: ExtendedLSPClient, transport: Transport, initializationOptions?: Record, ): void { if (!initializationOptions || !Object.keys(initializationOptions).length) { client.connect(transport); return; } const patchedClient = client as unknown as { requestInner: RequestInnerFn; }; const originalRequestInner = patchedClient.requestInner.bind( patchedClient, ) as RequestInnerFn; patchedClient.requestInner = function patchedRequestInner( method: string, params: Params, mapped?: boolean, ): InternalLSPRequest { if (method === "initialize" && isPlainObject(params)) { params = { ...params, initializationOptions, } as Params; } return originalRequestInner(method, params, mapped); }; try { client.connect(transport); } finally { patchedClient.requestInner = originalRequestInner; } } interface BuiltinExtensionsResult { extensions: Extension[]; diagnosticsExtension: Extension | LSPClientExtension | null; } function buildBuiltinExtensions( config: BuiltinExtensionsConfig = {}, ): BuiltinExtensionsResult { const { hover: includeHover = true, completion: includeCompletion = true, signature: includeSignature = true, keymaps: includeKeymaps = true, diagnostics: includeDiagnostics = true, inlayHints: includeInlayHints = false, formatting: includeFormatting = true, } = config; const extensions: Extension[] = []; let diagnosticsExtension: Extension | LSPClientExtension | null = null; if (includeCompletion) extensions.push(serverCompletion()); if (includeHover) extensions.push(hoverTooltips()); if (includeKeymaps) { const bindings = [ ...(includeFormatting ? formatKeymap : []), ...acodeRenameKeymap, ...jumpToDefinitionKeymap, ...findReferencesKeymap, ]; if (bindings.length) { extensions.push(keymap.of(bindings)); } } if (includeSignature) extensions.push(signatureHelp()); if (includeDiagnostics) { const diagExt = serverDiagnostics(); diagnosticsExtension = diagExt; extensions.push(diagExt as Extension); } if (includeInlayHints) { const hintsExt = inlayHintsExtension(); extensions.push(hintsExt as LSPClientExtension as Extension); } return { extensions, diagnosticsExtension }; } interface LSPError extends Error { code?: string; } interface InitContext { key: string; normalizedRootUri: string | null; originalRootUri: string | null; } interface ExtendedLSPClient extends LSPClient { __acodeLoggedInfo?: boolean; } export class LspClientManager { options: ClientManagerOptions; #clients: Map; #pendingClients: Map>; constructor(options: ClientManagerOptions = {}) { this.options = { ...options }; this.#clients = new Map(); this.#pendingClients = new Map(); } setOptions(next: Partial): void { this.options = { ...this.options, ...next }; } getActiveClients(): ClientState[] { return Array.from(this.#clients.values()); } async getExtensionsForFile(metadata: FileMetadata): Promise { const { uri: originalUri, languageId, languageName, view, file, rootUri, } = metadata; const effectiveLang = safeString(languageId ?? languageName).toLowerCase(); if (!effectiveLang) return []; const servers = serverRegistry.getServersForLanguage(effectiveLang); if (!servers.length) return []; const lspExtensions: Extension[] = []; const diagnosticsUiExtension = this.options.diagnosticsUiExtension; for (const server of servers) { const normalizedUri = await this.#resolveDocumentUri(server, { uri: originalUri, file, view, languageId: effectiveLang, rootUri, }); if (!normalizedUri) { console.warn( `Cannot resolve document URI for LSP server ${server.id}: ${originalUri}`, ); continue; } let targetLanguageId = effectiveLang; if (server.resolveLanguageId) { try { const resolved = server.resolveLanguageId({ languageId: effectiveLang, languageName, uri: normalizedUri, file, }); if (resolved) targetLanguageId = safeString(resolved); } catch (error) { console.warn( `LSP server ${server.id} failed to resolve language id for ${normalizedUri}`, error, ); } } try { const clientState = await this.#ensureClient(server, { uri: normalizedUri, file, view, languageId: targetLanguageId, rootUri, }); const plugin = clientState.client.plugin( normalizedUri, targetLanguageId, ); const aliases = originalUri && originalUri !== normalizedUri ? [originalUri] : []; clientState.attach(normalizedUri, view as EditorView, aliases); lspExtensions.push(plugin); } catch (error) { const lspError = error as LSPError; if (lspError?.code === "LSP_SERVER_UNAVAILABLE") { console.info( `Skipping LSP client for ${server.id}: ${lspError.message}`, ); continue; } console.error( `Failed to initialize LSP client for ${server.id}`, error, ); } } if (diagnosticsUiExtension && lspExtensions.length) { lspExtensions.push(...asArray(diagnosticsUiExtension)); } return lspExtensions; } async formatDocument( metadata: FileMetadata, options: FormattingOptions = {}, ): Promise { const { uri: originalUri, languageId, languageName, view, file } = metadata; const effectiveLang = safeString(languageId ?? languageName).toLowerCase(); if (!effectiveLang || !view) return false; const servers = serverRegistry.getServersForLanguage(effectiveLang); if (!servers.length) return false; for (const server of servers) { if (!supportsBuiltinFormatting(server)) continue; try { const normalizedUri = await this.#resolveDocumentUri(server, { uri: originalUri, file, view, languageId: effectiveLang, rootUri: metadata.rootUri, }); if (!normalizedUri) { console.warn( `Cannot resolve document URI for formatting with ${server.id}: ${originalUri}`, ); continue; } const context: RootUriContext = { uri: normalizedUri, languageId: effectiveLang, view, file, rootUri: metadata.rootUri, }; const state = await this.#ensureClient(server, context); const capabilities = state.client.serverCapabilities; if (!capabilities?.documentFormattingProvider) continue; state.attach(normalizedUri, view); const plugin = LSPPlugin.get(view); if (!plugin) continue; plugin.client.sync(); const edits = await state.client.request< { textDocument: { uri: string }; options: FormattingOptions }, TextEdit[] | null >("textDocument/formatting", { textDocument: { uri: normalizedUri }, options: buildFormattingOptions(view, options), }); if (!edits || !edits.length) { plugin.client.sync(); return true; } const applied = applyTextEdits(plugin, view, edits); if (applied) { plugin.client.sync(); return true; } } catch (error) { console.error(`LSP formatting failed for ${server.id}`, error); } } return false; } detach(uri: string, view: EditorView): void { for (const state of this.#clients.values()) { state.detach(uri, view); } } async dispose(): Promise { try { interface FileWithSession { id?: string; type?: string; session?: EditorState; } interface EditorManagerLike { files?: FileWithSession[]; editor?: EditorView; activeFile?: FileWithSession; } const em = (globalThis as Record).editorManager as | EditorManagerLike | undefined; if (em?.editor) { try { em.editor.dispatch({ effects: clearDiagnosticsEffect() }); if (em.activeFile?.type === "editor") { em.activeFile.session = em.editor.state; } } catch { /* View may be disposed */ } } if (em?.files) { for (const file of em.files) { if (file?.type !== "editor" || file.id === em.activeFile?.id) continue; const session = file.session; if (session && typeof session.update === "function") { try { file.session = session.update({ effects: clearDiagnosticsEffect(), }).state; } catch { /* State update failed */ } } } } } catch { /* Ignore errors */ } const disposeOps: Promise[] = []; for (const [key, state] of this.#clients.entries()) { disposeOps.push(state.dispose()); this.#clients.delete(key); } await Promise.allSettled(disposeOps); } async #ensureClient( server: LspServerDefinition, context: RootUriContext, ): Promise { const useWsFolders = server.useWorkspaceFolders === true; const resolvedRoot = await this.#resolveRootUri(server, context); const { normalizedRootUri, originalRootUri } = normalizeRootUriForServer( server, resolvedRoot, ); // For workspace folders mode, use a shared key based on server ID only const key = pluginKey(server.id, normalizedRootUri, useWsFolders); // Return existing client if already initialized if (this.#clients.has(key)) { const existing = this.#clients.get(key)!; // For workspace folders mode, add the new folder to the existing server if (useWsFolders && normalizedRootUri) { const workspace = existing.client.workspace as AcodeWorkspace | null; if (workspace && !workspace.hasWorkspaceFolder(normalizedRootUri)) { workspace.addWorkspaceFolder(normalizedRootUri); } } return existing; } // If initialization is already in progress, wait for it if (this.#pendingClients.has(key)) { return this.#pendingClients.get(key)!; } // Create and track the pending initialization const initPromise = this.#initializeClient(server, context, { key, normalizedRootUri: useWsFolders ? null : normalizedRootUri, originalRootUri: useWsFolders ? null : originalRootUri, }); this.#pendingClients.set(key, initPromise); try { return await initPromise; } finally { this.#pendingClients.delete(key); } } async #initializeClient( server: LspServerDefinition, context: RootUriContext, initContext: InitContext, ): Promise { const { key, normalizedRootUri, originalRootUri } = initContext; const workspaceOptions = { displayFile: this.options.displayFile, openFile: this.options.openFile, resolveLanguageId: this.options.resolveLanguageId, }; const clientConfig = { ...(server.clientConfig ?? {}) }; const initializationOptions = resolveInitializationOptions( server, clientConfig as Record, ); const builtinConfig = clientConfig.builtinExtensions ?? {}; const useDefaultExtensions = clientConfig.useDefaultExtensions !== false; const { extensions: defaultExtensions, diagnosticsExtension } = useDefaultExtensions ? buildBuiltinExtensions({ hover: builtinConfig.hover !== false, completion: builtinConfig.completion !== false, signature: builtinConfig.signature !== false, keymaps: builtinConfig.keymaps !== false, diagnostics: builtinConfig.diagnostics !== false, inlayHints: builtinConfig.inlayHints === true, formatting: builtinConfig.formatting !== false, }) : { extensions: [], diagnosticsExtension: null }; const extraExtensions = asArray(this.options.clientExtensions); const serverExtensions = asArray(clientConfig.extensions); interface ExtensionWithCapabilities { clientCapabilities?: { textDocument?: { publishDiagnostics?: unknown; }; }; } const wantsCustomDiagnostics = [ ...extraExtensions, ...serverExtensions, ].some((ext) => { const extWithCaps = ext as ExtensionWithCapabilities; return !!extWithCaps?.clientCapabilities?.textDocument ?.publishDiagnostics; }); const filteredBuiltins = wantsCustomDiagnostics && diagnosticsExtension ? defaultExtensions.filter((ext) => ext !== diagnosticsExtension) : defaultExtensions; const progressCapabilities: LSPClientExtension = { clientCapabilities: { window: { workDoneProgress: true, }, }, }; const mergedExtensions = [ ...filteredBuiltins, ...extraExtensions, ...serverExtensions, progressCapabilities, ]; clientConfig.extensions = mergedExtensions; const existingHandlers = clientConfig.notificationHandlers ?? {}; type LogLevel = "error" | "warn" | "log" | "info"; interface LogMessageParams { type?: number; message?: string; } interface ShowMessageParams { type?: number; message?: string; } clientConfig.notificationHandlers = { ...existingHandlers, "window/logMessage": (_client: LSPClient, params: unknown): boolean => { const logParams = params as LogMessageParams; if (!logParams?.message) return false; const { type, message } = logParams; let level: LogLevel = "info"; switch (type) { case 1: level = "error"; break; case 2: level = "warn"; break; case 4: level = "log"; break; default: level = "info"; } const logFn = console[level] ?? console.info; logFn(`[LSP:${server.id}] ${message}`); return true; }, "window/showMessage": (_client: LSPClient, params: unknown): boolean => { const showParams = params as ShowMessageParams; if (!showParams?.message) return false; const { type, message } = showParams; const serverLabel = server.label || server.id; // Helper to clean and truncate message for notifications const cleanMessage = (msg: string, maxLen = 150): string => { // Take only first line let cleaned = msg.split("\n")[0].trim(); if (cleaned.length > maxLen) { cleaned = cleaned.slice(0, maxLen - 3) + "..."; } return cleaned; }; // Use notifications for errors and warnings if (type === 1 || type === 2) { const notificationManager = new NotificationManager(); notificationManager.pushNotification({ title: serverLabel, message: cleanMessage(message), icon: type === 1 ? "error" : "warningreport_problem", type: type === 1 ? "error" : "warning", }); logLspInfo(`[LSP:${server.id}] ${message}`); return true; } // For info/log messages, use status bar briefly lspStatusBar.show({ message: cleanMessage(message, 80), title: serverLabel, type: "info", icon: type === 4 ? "autorenew" : "info", duration: 5000, }); logLspInfo(`[LSP:${server.id}] ${message}`); return true; }, "$/progress": (_client: LSPClient, params: unknown): boolean => { interface ProgressParams { token?: string | number; value?: { kind?: "begin" | "report" | "end"; title?: string; message?: string; percentage?: number; cancellable?: boolean; }; } const progressParams = params as ProgressParams; if (!progressParams?.value) return false; const { kind, title, message, percentage } = progressParams.value; const displayTitle = title || server.label || server.id; // Use server ID + token as unique status ID for concurrent progress tracking const progressToken = progressParams.token; const statusId = `${server.id}-progress-${progressToken ?? "default"}`; if (kind === "begin") { lspStatusBar.show({ id: statusId, message: message || title || "Starting...", title: displayTitle, type: "info", icon: "autorenew", duration: false, showProgress: typeof percentage === "number", progress: percentage, }); } else if (kind === "report") { lspStatusBar.update({ id: statusId, message: message, progress: percentage, }); } else if (kind === "end") { // Just hide the progress item silently, no "Complete" message lspStatusBar.hideById(statusId); } logLspInfo( `[LSP:${server.id}] Progress: ${kind} - ${message || title || ""} ${typeof percentage === "number" ? `(${percentage}%)` : ""}`, ); return true; }, "$/typescriptVersion": (_client: LSPClient, params: unknown): boolean => { interface TypeScriptVersionParams { version?: string; source?: string; } const versionParams = params as TypeScriptVersionParams; if (!versionParams?.version) return false; const serverLabel = server.label || server.id; const source = versionParams.source || "bundled"; logLspInfo( `[LSP:${server.id}] TypeScript ${versionParams.version} (${source})`, ); // Show briefly in status bar lspStatusBar.show({ message: `TypeScript ${versionParams.version}`, title: serverLabel, type: "info", icon: "code", duration: 3000, }); return true; }, }; // Log unhandled notifications to help debug what servers are sending const unhandledNotificationKey = "unhandledNotification" as keyof typeof clientConfig; if (!(unhandledNotificationKey in clientConfig)) { ( clientConfig as Record< string, (client: LSPClient, method: string, params: unknown) => void > ).unhandledNotification = ( _client: LSPClient, method: string, params: unknown, ) => { logLspInfo( `[LSP:${server.id}] Unhandled notification: ${method}`, params, ); }; } if (!clientConfig.workspace) { clientConfig.workspace = (client: LSPClient) => new AcodeWorkspace(client, workspaceOptions); } if (normalizedRootUri && !clientConfig.rootUri) { clientConfig.rootUri = normalizedRootUri; } if (!normalizedRootUri && clientConfig.rootUri) { delete clientConfig.rootUri; } if (server.startupTimeout && !clientConfig.timeout) { clientConfig.timeout = server.startupTimeout; } let transportHandle: TransportHandle | undefined; let client: ExtendedLSPClient | undefined; try { // Get session from server ID for auto-port discovery const session = server.id; const serverResult = await ensureServerRunning(server, session); // Use discovered port if available (for auto-port discovery) const dynamicPort = serverResult.discoveredPort; transportHandle = createTransport(server, { ...context, rootUri: normalizedRootUri ?? null, originalRootUri: originalRootUri ?? undefined, dynamicPort, }); await transportHandle.ready; client = new LSPClient(clientConfig) as ExtendedLSPClient; connectClient(client, transportHandle.transport, initializationOptions); await client.initializing; if (!client.__acodeLoggedInfo) { // Log root URI info to console if (normalizedRootUri) { if (originalRootUri && originalRootUri !== normalizedRootUri) { logLspInfo( `[LSP:${server.id}] root ${normalizedRootUri} (from ${originalRootUri})`, ); } else { logLspInfo(`[LSP:${server.id}] root`, normalizedRootUri); } } else if (originalRootUri) { logLspInfo(`[LSP:${server.id}] root ignored`, originalRootUri); } if (initializationOptions) { logLspInfo( `[LSP:${server.id}] initializationOptions keys`, Object.keys(initializationOptions), ); } logLspInfo(`[LSP:${server.id}] initialized`); client.__acodeLoggedInfo = true; } } catch (error) { transportHandle?.dispose?.(); throw error; } const state = this.#createClientState({ key, server, client, transportHandle, normalizedRootUri, originalRootUri, }); this.#clients.set(key, state); return state; } #createClientState(params: { key: string; server: LspServerDefinition; client: LSPClient; transportHandle: TransportHandle; normalizedRootUri: string | null; originalRootUri: string | null; }): ClientState { const { key, server, client, transportHandle, normalizedRootUri, originalRootUri, } = params; const fileRefs = new Map>(); const uriAliases = new Map(); const effectiveRoot = normalizedRootUri ?? originalRootUri ?? null; const attach = ( uri: string, view: EditorView, aliases: string[] = [], ): void => { const existing = fileRefs.get(uri) ?? new Set(); existing.add(view); fileRefs.set(uri, existing); uriAliases.set(uri, uri); for (const alias of aliases) { if (!alias || alias === uri) continue; uriAliases.set(alias, uri); } const suffix = effectiveRoot ? ` (root ${effectiveRoot})` : ""; logLspInfo(`[LSP:${server.id}] attached to ${uri}${suffix}`); }; const detach = (uri: string, view?: EditorView): void => { const actualUri = uriAliases.get(uri) ?? uri; const existing = fileRefs.get(actualUri); if (!existing) return; if (view) existing.delete(view); if (!view || !existing.size) { fileRefs.delete(actualUri); for (const [alias, target] of uriAliases.entries()) { if (target === actualUri) { uriAliases.delete(alias); } } try { // Only pass uri to closeFile - view is not needed for closing // and passing it may cause issues if the view is already disposed (client.workspace as AcodeWorkspace)?.closeFile?.(actualUri); } catch (error) { console.warn(`Failed to close LSP file ${actualUri}`, error); } } if (!fileRefs.size) { this.options.onClientIdle?.({ server, client, rootUri: effectiveRoot, }); } }; const dispose = async (): Promise => { try { client.disconnect(); } catch (error) { console.warn(`Error disconnecting LSP client ${server.id}`, error); } try { await transportHandle.dispose?.(); } catch (error) { console.warn(`Error disposing LSP transport ${server.id}`, error); } this.#clients.delete(key); }; return { server, client, transport: transportHandle, rootUri: effectiveRoot, attach, detach, dispose, }; } async #resolveRootUri( server: LspServerDefinition, context: RootUriContext, ): Promise { if (typeof server.rootUri === "function") { try { const value = await server.rootUri(context?.uri ?? "", context); if (value) return safeString(value); } catch (error) { console.warn(`Server root resolver failed for ${server.id}`, error); } } if (context?.rootUri) return safeString(context.rootUri); if (typeof this.options.resolveRoot === "function") { try { const value = await this.options.resolveRoot(context); if (value) return safeString(value); } catch (error) { console.warn("Global LSP root resolver failed", error); } } return null; } async #resolveDocumentUri( server: LspServerDefinition, context: RootUriContext, ): Promise { const originalUri = context?.uri; if (!originalUri) return null; let normalizedUri = normalizeDocumentUri(originalUri); if (!normalizedUri) { // Fall back to cache file path for providers that do not expose a file:// URI. const cacheFile = context.file?.cacheFile; if (cacheFile && typeof cacheFile === "string") { normalizedUri = buildFileUri(cacheFile.replace(/^file:\/\//, "")); if (normalizedUri) { console.info( `LSP using cache path for unrecognized URI: ${originalUri} -> ${normalizedUri}`, ); } } } if (typeof server.documentUri === "function") { try { const value = await server.documentUri(originalUri, { ...context, normalizedUri, } as DocumentUriContext); if (value) return safeString(value); } catch (error) { console.warn( `Server document URI resolver failed for ${server.id}`, error, ); } } return normalizedUri; } } interface Change { from: number; to: number; insert: string; } function applyTextEdits( plugin: LSPPlugin, view: EditorView, edits: TextEdit[], ): boolean { const changes: Change[] = []; for (const edit of edits) { if (!edit?.range) continue; let fromBase: number; let toBase: number; try { fromBase = plugin.fromPosition(edit.range.start, plugin.syncedDoc); toBase = plugin.fromPosition(edit.range.end, plugin.syncedDoc); } catch (_) { continue; } const fromResult = plugin.unsyncedChanges.mapPos( fromBase, 1, MapMode.TrackDel, ); const toResult = plugin.unsyncedChanges.mapPos( toBase, -1, MapMode.TrackDel, ); if (fromResult == null || toResult == null) continue; const insert = typeof edit.newText === "string" ? edit.newText.replace(/\r\n/g, "\n") : ""; changes.push({ from: fromResult, to: toResult, insert }); } if (!changes.length) return false; changes.sort((a, b) => a.from - b.from || a.to - b.to); view.dispatch({ changes }); return true; } function buildFormattingOptions( view: EditorView, overrides: FormattingOptions = {}, ): FormattingOptions { const state = view?.state; if (!state) return { ...overrides }; const unitValue = state.facet(indentUnit); const unit = typeof unitValue === "string" && unitValue.length ? unitValue : String(unitValue ?? "\t"); let tabSize = getIndentUnit(state); if ( typeof tabSize !== "number" || !Number.isFinite(tabSize) || tabSize <= 0 ) { tabSize = resolveIndentWidth(unit); } const insertSpaces = !unit.includes("\t"); return { tabSize, insertSpaces, ...overrides, }; } function resolveIndentWidth(unit: string): number { if (typeof unit !== "string" || !unit.length) return 4; let width = 0; for (const ch of unit) { if (ch === "\t") return 4; width += 1; } return width || 4; } const defaultManager = new LspClientManager(); export default defaultManager; function normalizeRootUriForServer( _server: LspServerDefinition, rootUri: string | null, ): NormalizedRootUri { if (!rootUri || typeof rootUri !== "string") { return { normalizedRootUri: null, originalRootUri: null }; } const schemeMatch = /^([a-zA-Z][\w+\-.]*):/.exec(rootUri); const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null; // Already a file:// URI - use as-is if (scheme === "file") { return { normalizedRootUri: rootUri, originalRootUri: rootUri }; } // Try to convert content:// URIs to file:// URIs if (scheme === "content") { const fileUri = contentUriToFileUri(rootUri); if (fileUri) { return { normalizedRootUri: fileUri, originalRootUri: rootUri }; } // Can't convert to file:// - server won't work properly return { normalizedRootUri: null, originalRootUri: rootUri }; } // Unknown scheme - try to use as-is return { normalizedRootUri: rootUri, originalRootUri: rootUri }; } function normalizeDocumentUri(uri: string | null | undefined): string | null { if (!uri || typeof uri !== "string") return null; const schemeMatch = /^([a-zA-Z][\w+\-.]*):/.exec(uri); const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null; // Already a file:// URI or untitled use as-is if (scheme === "file" || scheme === "untitled") { return uri; } // Convert content:// URIs to file:// URIs if (scheme === "content") { const fileUri = contentUriToFileUri(uri); if (fileUri) { return fileUri; } return null; } // Unknown scheme return uri; } function contentUriToFileUri(uri: string): string | null { try { const parsed = Uri.parse(uri) as ParsedUri | null; if (!parsed || typeof parsed !== "object") return null; const { docId, rootUri, isFileUri } = parsed; if (!docId) return null; if (isFileUri && rootUri) { return rootUri; } const providerMatch = /^content:\/\/com\.((?![:<>"/\\|?*]).*?)\.documents\//.exec( rootUri ?? "", ); const providerId = providerMatch ? providerMatch[1] : null; let normalized = docId.trim(); if (!normalized) return null; switch (providerId) { case "foxdebug.acode": case "foxdebug.acodefree": normalized = normalized.replace(/:+$/, ""); if (!normalized) return null; if (normalized.startsWith("raw:/")) { normalized = normalized.slice(4); } else if (normalized.startsWith("raw:")) { normalized = normalized.slice(4); } if (!normalized.startsWith("/")) return null; return buildFileUri(normalized); case "android.externalstorage": normalized = normalized.replace(/:+$/, ""); if (!normalized) return null; if (normalized.startsWith("/")) { return buildFileUri(normalized); } if (normalized.startsWith("raw:/")) { return buildFileUri(normalized.slice(4)); } if (normalized.startsWith("raw:")) { return buildFileUri(normalized.slice(4)); } const separator = normalized.indexOf(":"); if (separator === -1) return null; const root = normalized.slice(0, separator); const remainder = normalized.slice(separator + 1); if (!remainder) return null; switch (root) { case "primary": return buildFileUri(`/storage/emulated/0/${remainder}`); default: if (/^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$/.test(root)) { return buildFileUri(`/storage/${root}/${remainder}`); } } return null; default: return null; } } catch (_) { return null; } } function buildFileUri(pathname: string): string | null { if (!pathname) return null; const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`; const encoded = encodeURI(normalized).replace(/#/g, "%23"); return `file://${encoded}`; } ================================================ FILE: src/cm/lsp/codeActions.ts ================================================ import { LSPPlugin } from "@codemirror/lsp-client"; import { EditorView } from "@codemirror/view"; import toast from "components/toast"; import select from "dialogs/select"; import type { CodeAction, CodeActionContext, CodeActionKind, Command, Diagnostic, Range as LspRange, WorkspaceEdit, } from "vscode-languageserver-types"; import type { Position, Range } from "./types"; import type AcodeWorkspace from "./workspace"; type CodeActionResponse = (CodeAction | Command)[] | null; const CODE_ACTION_KINDS = { QUICK_FIX: "quickfix", REFACTOR: "refactor", REFACTOR_EXTRACT: "refactor.extract", REFACTOR_INLINE: "refactor.inline", REFACTOR_REWRITE: "refactor.rewrite", SOURCE: "source", SOURCE_ORGANIZE_IMPORTS: "source.organizeImports", SOURCE_FIX_ALL: "source.fixAll", } as const; const CODE_ACTION_ICONS: Record = { quickfix: "build", refactor: "code", "refactor.extract": "call_split", "refactor.inline": "call_merge", "refactor.rewrite": "edit", source: "settings", "source.organizeImports": "sort", "source.fixAll": "done_all", }; function getCodeActionIcon(kind?: CodeActionKind): string { if (!kind) return "icon zap"; for (const [prefix, icon] of Object.entries(CODE_ACTION_ICONS)) { if (kind.startsWith(prefix)) return icon; } return "icon zap"; } function formatCodeActionKind(kind?: CodeActionKind): string { if (!kind) return ""; return kind .split(".") .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) .join(" › "); } function isCommand(item: CodeAction | Command): item is Command { return ( "command" in item && typeof item.command === "string" && !("edit" in item) ); } function lspPositionToOffset( doc: { line: (n: number) => { from: number } }, pos: Position, ): number { return doc.line(pos.line + 1).from + pos.character; } async function requestCodeActions( plugin: LSPPlugin, range: LspRange, diagnostics: Diagnostic[] = [], ): Promise { const context: CodeActionContext = { diagnostics, triggerKind: 1, // CodeActionTriggerKind.Invoked }; return plugin.client.request< { textDocument: { uri: string }; range: LspRange; context: CodeActionContext; }, CodeActionResponse >("textDocument/codeAction", { textDocument: { uri: plugin.uri }, range, context, }); } async function resolveCodeAction( plugin: LSPPlugin, action: CodeAction, ): Promise { // If action already has an edit, no need to resolve if (action.edit) return action; const capabilities = plugin.client.serverCapabilities; const provider = capabilities?.codeActionProvider; const supportsResolve = typeof provider === "object" && provider !== null && "resolveProvider" in provider && provider.resolveProvider === true; if (!supportsResolve) return action; // Resolve to get the edit property (lazy computation per LSP 3.16+) try { const resolved = await plugin.client.request( "codeAction/resolve", action, ); return resolved ?? action; } catch (error) { console.warn("[LSP:CodeAction] Failed to resolve:", error); return action; } } async function executeCommand( plugin: LSPPlugin, command: Command, ): Promise { try { await plugin.client.request< { command: string; arguments?: unknown[] }, unknown >("workspace/executeCommand", { command: command.command, arguments: command.arguments, }); return true; } catch (error) { // -32601 = Method not implemented (expected for some LSP servers) const lspError = error as { code?: number }; if (lspError?.code !== -32601) { console.warn("[LSP:CodeAction] Command execution failed:", error); } return false; } } interface LspChange { range: Range; newText: string; } async function applyChangesToFile( workspace: AcodeWorkspace, uri: string, changes: LspChange[], mapping: { mapPosition: (uri: string, pos: Position) => number }, ): Promise { const file = workspace.getFile(uri); if (file) { const view = file.getView(); if (view) { view.dispatch({ changes: changes.map((c) => ({ from: mapping.mapPosition(uri, c.range.start), to: mapping.mapPosition(uri, c.range.end), insert: c.newText, })), userEvent: "codeAction", }); return true; } } const displayedView = await workspace.displayFile(uri); if (!displayedView?.state?.doc) { console.warn(`[LSP:CodeAction] Could not open file: ${uri}`); return false; } displayedView.dispatch({ changes: changes.map((c) => ({ from: lspPositionToOffset(displayedView.state.doc, c.range.start), to: lspPositionToOffset(displayedView.state.doc, c.range.end), insert: c.newText, })), userEvent: "codeAction", }); return true; } async function applyWorkspaceEdit( view: EditorView, edit: WorkspaceEdit, ): Promise { const plugin = LSPPlugin.get(view); if (!plugin) return false; const workspace = plugin.client.workspace as AcodeWorkspace; if (!workspace) return false; let filesChanged = 0; const result = await plugin.client.withMapping(async (mapping) => { // Handle simple changes format if (edit.changes) { for (const uri in edit.changes) { const changes = edit.changes[uri] as LspChange[]; if ( changes.length && (await applyChangesToFile(workspace, uri, changes, mapping)) ) { filesChanged++; } } } // Handle documentChanges format (supports versioned edits) if (edit.documentChanges) { for (const docChange of edit.documentChanges) { if ("textDocument" in docChange && "edits" in docChange) { const uri = docChange.textDocument.uri; const edits = docChange.edits as LspChange[]; if ( edits.length && (await applyChangesToFile(workspace, uri, edits, mapping)) ) { filesChanged++; } } } } return filesChanged; }); return (result ?? 0) > 0; } /** * Apply a code action following the LSP spec: * "If both edit and command are supplied, first the edit is applied, then the command is executed" */ async function applyCodeAction( view: EditorView, action: CodeAction, ): Promise { const plugin = LSPPlugin.get(view); if (!plugin) return false; plugin.client.sync(); // Resolve to get the edit if not already present const resolved = await resolveCodeAction(plugin, action); let success = false; // Step 1: Apply workspace edit if present if (resolved.edit) { success = await applyWorkspaceEdit(view, resolved.edit); } // Step 2: Execute command if present (after edit per LSP spec) if (resolved.command) { const commandSuccess = await executeCommand(plugin, resolved.command); success = success || commandSuccess; } plugin.client.sync(); return success; } export interface CodeActionItem { title: string; kind?: CodeActionKind; icon: string; isPreferred?: boolean; disabled?: boolean; disabledReason?: string; action: CodeAction | Command; } export async function fetchCodeActions( view: EditorView, ): Promise { const plugin = LSPPlugin.get(view); if (!plugin) return []; const capabilities = plugin.client.serverCapabilities; if (!capabilities?.codeActionProvider) return []; const { from, to } = view.state.selection.main; const range: LspRange = { start: plugin.toPosition(from), end: plugin.toPosition(to), }; plugin.client.sync(); try { const response = await requestCodeActions(plugin, range); if (!response?.length) return []; const items: CodeActionItem[] = response.map((item) => { if (isCommand(item)) { return { title: item.title, icon: "terminal", action: item }; } return { title: item.title, kind: item.kind, icon: getCodeActionIcon(item.kind), isPreferred: item.isPreferred, disabled: !!item.disabled, disabledReason: item.disabled?.reason, action: item, }; }); // Sort: preferred first, then quickfixes, then alphabetically items.sort((a, b) => { if (a.isPreferred && !b.isPreferred) return -1; if (!a.isPreferred && b.isPreferred) return 1; if (a.kind?.startsWith("quickfix") && !b.kind?.startsWith("quickfix")) return -1; if (!a.kind?.startsWith("quickfix") && b.kind?.startsWith("quickfix")) return 1; return a.title.localeCompare(b.title); }); return items; } catch (error) { console.error("[LSP:CodeAction] Failed to fetch:", error); return []; } } export async function executeCodeAction( view: EditorView, item: CodeActionItem, ): Promise { const plugin = LSPPlugin.get(view); if (!plugin) return false; try { plugin.client.sync(); // Handle standalone Command (not CodeAction) if (isCommand(item.action)) { return executeCommand(plugin, item.action); } // Handle CodeAction return applyCodeAction(view, item.action); } catch (error) { console.error("[LSP:CodeAction] Failed to execute:", error); return false; } } export function supportsCodeActions(view: EditorView): boolean { const plugin = LSPPlugin.get(view); return !!plugin?.client.serverCapabilities?.codeActionProvider; } export async function showCodeActionsMenu(view: EditorView): Promise { if (!supportsCodeActions(view)) return false; const items = await fetchCodeActions(view); if (!items.length) { toast("No code actions available"); return false; } const selectItems = items.map((item, i) => ({ value: String(i), text: item.title, icon: item.icon, disabled: item.disabled, })); try { const result = await select( strings["code actions"] || "Code Actions", selectItems as unknown as string[], { hideOnSelect: true }, ); if (result !== null && result !== undefined) { const index = Number.parseInt(String(result), 10); if (!Number.isNaN(index) && index >= 0 && index < items.length) { await executeCodeAction(view, items[index]); view.focus(); return true; } } } catch { // User cancelled selection } view.focus(); return false; } export async function performQuickFix(view: EditorView): Promise { const items = await fetchCodeActions(view); if (!items.length) return false; // Find preferred action or first quickfix const quickFix = items.find((i) => i.isPreferred) ?? items.find((i) => i.kind?.startsWith("quickfix")); if (quickFix) { return executeCodeAction(view, quickFix); } // Fall back to showing menu return showCodeActionsMenu(view); } export { CODE_ACTION_KINDS, formatCodeActionKind, getCodeActionIcon }; ================================================ FILE: src/cm/lsp/diagnostics.ts ================================================ import { Diagnostic, linter, lintGutter } from "@codemirror/lint"; import type { LSPClient } from "@codemirror/lsp-client"; import { LSPPlugin } from "@codemirror/lsp-client"; import type { Extension } from "@codemirror/state"; import { EditorState, MapMode, StateEffect, StateField, } from "@codemirror/state"; import { type EditorView, ViewPlugin } from "@codemirror/view"; import type { LSPClientWithWorkspace, LSPPluginAPI, LspDiagnostic, PublishDiagnosticsParams, RawDiagnostic, } from "./types"; const setPublishedDiagnostics = StateEffect.define(); let diagnosticsEventTimer: ReturnType | null = null; let diagnosticsViewCount = 0; export const LSP_DIAGNOSTICS_EVENT = "acode:lsp-diagnostics-updated"; function isCoarsePointerDevice(): boolean { if (typeof window !== "undefined") { try { if (window.matchMedia?.("(pointer: coarse)").matches) { return true; } } catch (_) { // Ignore matchMedia failures and fall back to maxTouchPoints. } } return ( typeof navigator !== "undefined" && Number(navigator.maxTouchPoints || 0) > 0 ); } function emitDiagnosticsUpdated(): void { if ( typeof document === "undefined" || typeof document.dispatchEvent !== "function" ) { return; } let event: CustomEvent | Event; try { event = new CustomEvent(LSP_DIAGNOSTICS_EVENT); } catch (_) { try { event = document.createEvent("CustomEvent"); (event as CustomEvent).initCustomEvent( LSP_DIAGNOSTICS_EVENT, false, false, undefined, ); } catch (_) { return; } } document.dispatchEvent(event); } function clearScheduledDiagnosticsUpdated(): void { if (diagnosticsEventTimer == null) return; clearTimeout(diagnosticsEventTimer); diagnosticsEventTimer = null; } const lspPublishedDiagnostics = StateField.define({ create(): LspDiagnostic[] { return []; }, update(value: LspDiagnostic[], tr): LspDiagnostic[] { for (const effect of tr.effects) { if (effect.is(setPublishedDiagnostics)) { value = effect.value; } } return value; }, }); type DiagnosticSeverity = "error" | "warning" | "info" | "hint"; const severities: DiagnosticSeverity[] = [ "hint", "error", "warning", "info", "hint", ]; function collectLspDiagnostics( plugin: LSPPluginAPI, diagnostics: RawDiagnostic[], ): LspDiagnostic[] { const items: LspDiagnostic[] = []; const { syncedDoc } = plugin; for (const diagnostic of diagnostics) { let from: number; let to: number; try { const mappedFrom = plugin.fromPosition( diagnostic.range.start, plugin.syncedDoc, ); const mappedTo = plugin.fromPosition( diagnostic.range.end, plugin.syncedDoc, ); const fromResult = plugin.unsyncedChanges.mapPos(mappedFrom); const toResult = plugin.unsyncedChanges.mapPos(mappedTo); if (fromResult === null || toResult === null) continue; from = fromResult; to = toResult; } catch (_) { continue; } if (to > syncedDoc.length) continue; const severity = severities[diagnostic.severity ?? 0] ?? "info"; const source = diagnostic.code ? `${diagnostic.source ? `${diagnostic.source}-` : ""}${diagnostic.code}` : undefined; items.push({ from, to, severity, message: diagnostic.message, source, }); } return items; } function storeLspDiagnostics( items: LspDiagnostic[], ): StateEffect { return setPublishedDiagnostics.of(items); } function sameDiagnostics( current: readonly LspDiagnostic[], next: readonly LspDiagnostic[], ): boolean { if (current.length !== next.length) return false; for (let index = 0; index < current.length; index++) { const left = current[index]; const right = next[index]; if ( left.from !== right.from || left.to !== right.to || left.severity !== right.severity || left.message !== right.message || left.source !== right.source ) { return false; } } return true; } function scheduleDiagnosticsUpdated(): void { if (diagnosticsEventTimer != null) return; diagnosticsEventTimer = setTimeout(() => { diagnosticsEventTimer = null; if (diagnosticsViewCount > 0) { emitDiagnosticsUpdated(); } }, 32); } const diagnosticsLifecyclePlugin = ViewPlugin.fromClass( class { constructor() { diagnosticsViewCount++; } destroy(): void { diagnosticsViewCount = Math.max(0, diagnosticsViewCount - 1); if (!diagnosticsViewCount) { clearScheduledDiagnosticsUpdated(); } } }, ); function mapDiagnostics( plugin: LSPPluginAPI, state: EditorState, ): Diagnostic[] { const stored = state.field(lspPublishedDiagnostics); const changes = plugin.unsyncedChanges; const mapped: Diagnostic[] = []; for (const diagnostic of stored) { let from: number | null; let to: number | null; try { from = changes.mapPos(diagnostic.from, 1, MapMode.TrackDel); to = changes.mapPos(diagnostic.to, -1, MapMode.TrackDel); } catch (_) { continue; } if (from != null && to != null) { mapped.push({ ...diagnostic, from, to }); } } return mapped; } function lspLinterSource(view: EditorView): Diagnostic[] { const plugin = LSPPlugin.get(view) as LSPPluginAPI | null; if (!plugin) return []; return mapDiagnostics(plugin, view.state); } export function lspDiagnosticsClientExtension(): { clientCapabilities: Record; notificationHandlers: Record< string, (client: LSPClient, params: PublishDiagnosticsParams) => boolean >; } { return { clientCapabilities: { textDocument: { publishDiagnostics: { relatedInformation: true, codeDescriptionSupport: true, dataSupport: true, versionSupport: true, }, }, }, notificationHandlers: { "textDocument/publishDiagnostics": ( client: LSPClient, params: PublishDiagnosticsParams, ): boolean => { const clientWithWorkspace = client as unknown as LSPClientWithWorkspace; const file = clientWithWorkspace.workspace.getFile(params.uri); if ( !file || (params.version != null && params.version !== file.version) ) { return true; } const view = file.getView(); if (!view) return true; const plugin = LSPPlugin.get(view) as LSPPluginAPI | null; if (!plugin) return true; const diagnostics = collectLspDiagnostics(plugin, params.diagnostics); const current = view.state.field(lspPublishedDiagnostics, false) ?? []; if (sameDiagnostics(current, diagnostics)) { return true; } view.dispatch({ effects: storeLspDiagnostics(diagnostics), }); scheduleDiagnosticsUpdated(); return true; }, }, }; } export function lspDiagnosticsUiExtension(includeGutter = true): Extension[] { const diagnosticsMarkerFilter = isCoarsePointerDevice() ? () => [] : undefined; const diagnosticsTooltipFilter = isCoarsePointerDevice() ? () => [] : undefined; const extensions: Extension[] = [ diagnosticsLifecyclePlugin, lspPublishedDiagnostics, linter(lspLinterSource, { needsRefresh(update) { return update.transactions.some((tr) => tr.effects.some((effect) => effect.is(setPublishedDiagnostics)), ); }, markerFilter: diagnosticsMarkerFilter, tooltipFilter: diagnosticsTooltipFilter, // keep panel closed by default autoPanel: false, }), ]; if (includeGutter) { extensions.splice( 1, 0, lintGutter({ tooltipFilter: diagnosticsTooltipFilter, }), ); } return extensions; } interface DiagnosticsExtension { clientCapabilities: Record; notificationHandlers: Record< string, (client: LSPClient, params: PublishDiagnosticsParams) => boolean >; editorExtension: Extension[]; } export function lspDiagnosticsExtension( includeGutter = true, ): DiagnosticsExtension { return { ...lspDiagnosticsClientExtension(), editorExtension: lspDiagnosticsUiExtension(includeGutter), }; } export default lspDiagnosticsExtension; export function clearDiagnosticsEffect(): StateEffect { return setPublishedDiagnostics.of([]); } export function getLspDiagnostics(state: EditorState | null): LspDiagnostic[] { if (!state || typeof state.field !== "function") return []; try { const stored = state.field(lspPublishedDiagnostics, false); if (!stored || !Array.isArray(stored)) return []; return stored.map((diagnostic) => ({ ...diagnostic })); } catch (_) { return []; } } ================================================ FILE: src/cm/lsp/documentSymbols.ts ================================================ /** * LSP Document Symbols Extension for CodeMirror * * Provides document symbol information (functions, classes, variables, etc.) from language servers. */ import { LSPPlugin } from "@codemirror/lsp-client"; import type { EditorView } from "@codemirror/view"; import type { DocumentSymbol, Position, Range, SymbolInformation, SymbolKind, } from "vscode-languageserver-types"; import type { LSPPluginAPI } from "./types"; interface DocumentSymbolParams { textDocument: { uri: string }; } export interface ProcessedSymbol { name: string; kind: SymbolKind; kindName: string; detail?: string; range: { startLine: number; startCharacter: number; endLine: number; endCharacter: number; }; selectionRange: { startLine: number; startCharacter: number; endLine: number; endCharacter: number; }; children?: ProcessedSymbol[]; depth: number; containerName?: string; } export interface FlatSymbol { name: string; kind: SymbolKind; kindName: string; detail?: string; line: number; character: number; endLine: number; endCharacter: number; containerName?: string; depth: number; } const SYMBOL_KIND_NAMES: Record = { 1: "File", 2: "Module", 3: "Namespace", 4: "Package", 5: "Class", 6: "Method", 7: "Property", 8: "Field", 9: "Constructor", 10: "Enum", 11: "Interface", 12: "Function", 13: "Variable", 14: "Constant", 15: "String", 16: "Number", 17: "Boolean", 18: "Array", 19: "Object", 20: "Key", 21: "Null", 22: "EnumMember", 23: "Struct", 24: "Event", 25: "Operator", 26: "TypeParameter", }; const SYMBOL_KIND_ICONS: Record = { 1: "insert_drive_file", 2: "view_module", 3: "view_module", 4: "folder", 5: "class", 6: "functions", 7: "label", 8: "label", 9: "functions", 10: "list", 11: "category", 12: "functions", 13: "code", 14: "lock", 15: "text_fields", 16: "pin", 17: "toggle_on", 18: "data_array", 19: "data_object", 20: "key", 21: "not_interested", 22: "list", 23: "data_object", 24: "bolt", 25: "calculate", 26: "text_fields", }; export function getSymbolKindName(kind: SymbolKind): string { return SYMBOL_KIND_NAMES[kind] || "Unknown"; } export function getSymbolKindIcon(kind: SymbolKind): string { return SYMBOL_KIND_ICONS[kind] || "code"; } function isDocumentSymbol( item: DocumentSymbol | SymbolInformation, ): item is DocumentSymbol { return "selectionRange" in item; } function processDocumentSymbol( symbol: DocumentSymbol, depth = 0, containerName?: string, ): ProcessedSymbol { const processed: ProcessedSymbol = { name: symbol.name, kind: symbol.kind, kindName: getSymbolKindName(symbol.kind), detail: symbol.detail, range: { startLine: symbol.range.start.line, startCharacter: symbol.range.start.character, endLine: symbol.range.end.line, endCharacter: symbol.range.end.character, }, selectionRange: { startLine: symbol.selectionRange.start.line, startCharacter: symbol.selectionRange.start.character, endLine: symbol.selectionRange.end.line, endCharacter: symbol.selectionRange.end.character, }, depth, containerName, }; if (symbol.children && symbol.children.length > 0) { processed.children = symbol.children.map((child) => processDocumentSymbol(child, depth + 1, symbol.name), ); } return processed; } function processSymbolInformation( symbol: SymbolInformation, depth = 0, ): ProcessedSymbol { return { name: symbol.name, kind: symbol.kind, kindName: getSymbolKindName(symbol.kind), range: { startLine: symbol.location.range.start.line, startCharacter: symbol.location.range.start.character, endLine: symbol.location.range.end.line, endCharacter: symbol.location.range.end.character, }, selectionRange: { startLine: symbol.location.range.start.line, startCharacter: symbol.location.range.start.character, endLine: symbol.location.range.end.line, endCharacter: symbol.location.range.end.character, }, containerName: symbol.containerName, depth, }; } function flattenSymbols( symbols: ProcessedSymbol[], result: FlatSymbol[] = [], ): FlatSymbol[] { for (const symbol of symbols) { result.push({ name: symbol.name, kind: symbol.kind, kindName: symbol.kindName, detail: symbol.detail, line: symbol.selectionRange.startLine, character: symbol.selectionRange.startCharacter, endLine: symbol.selectionRange.endLine, endCharacter: symbol.selectionRange.endCharacter, containerName: symbol.containerName, depth: symbol.depth, }); if (symbol.children) { flattenSymbols(symbol.children, result); } } return result; } export async function fetchDocumentSymbols( view: EditorView, ): Promise { const plugin = LSPPlugin.get(view) as LSPPluginAPI | null; if (!plugin) { return null; } const client = plugin.client; const capabilities = client.serverCapabilities; if (!capabilities?.documentSymbolProvider) { return null; } client.sync(); const params: DocumentSymbolParams = { textDocument: { uri: plugin.uri }, }; try { const response = await client.request< DocumentSymbolParams, (DocumentSymbol | SymbolInformation)[] | null >("textDocument/documentSymbol", params); if (!response || response.length === 0) { return []; } if (isDocumentSymbol(response[0])) { return (response as DocumentSymbol[]).map((sym) => processDocumentSymbol(sym), ); } return (response as SymbolInformation[]).map((sym) => processSymbolInformation(sym), ); } catch (error) { console.warn("Failed to fetch document symbols:", error); return null; } } export async function getDocumentSymbolsFlat( view: EditorView, ): Promise { const symbols = await fetchDocumentSymbols(view); if (!symbols) { return []; } return flattenSymbols(symbols); } export async function navigateToSymbol( view: EditorView, symbol: FlatSymbol | ProcessedSymbol, ): Promise { try { const doc = view.state.doc; let targetLine: number; let targetChar: number; if ("line" in symbol) { targetLine = symbol.line; targetChar = symbol.character; } else { targetLine = symbol.selectionRange.startLine; targetChar = symbol.selectionRange.startCharacter; } const lineNumber = targetLine + 1; if (lineNumber < 1 || lineNumber > doc.lines) { return false; } const line = doc.line(lineNumber); const pos = Math.min(line.from + targetChar, line.to); view.dispatch({ selection: { anchor: pos }, scrollIntoView: true, }); view.focus(); return true; } catch (error) { console.warn("Failed to navigate to symbol:", error); return false; } } export function supportsDocumentSymbols(view: EditorView): boolean { const plugin = LSPPlugin.get(view) as LSPPluginAPI | null; if (!plugin?.client.connected) { return false; } return !!plugin.client.serverCapabilities?.documentSymbolProvider; } export interface DocumentSymbolsResult { symbols: ProcessedSymbol[]; flat: FlatSymbol[]; } export async function getDocumentSymbols( view: EditorView, ): Promise { const symbols = await fetchDocumentSymbols(view); if (symbols === null) { return null; } return { symbols, flat: flattenSymbols(symbols), }; } export { SymbolKind } from "vscode-languageserver-types"; ================================================ FILE: src/cm/lsp/formatter.ts ================================================ import type { EditorView } from "@codemirror/view"; import { getModes } from "cm/modelist"; import toast from "components/toast"; import lspClientManager from "./clientManager"; import { supportsBuiltinFormatting } from "./formattingSupport"; import serverRegistry from "./serverRegistry"; import type { AcodeApi, FileMetadata } from "./types"; interface Mode { name?: string; extensions?: string; } interface EditorManagerWithLsp { editor?: EditorView; activeFile?: AcodeFile; getLspMetadata?: (file: AcodeFile) => FileMetadata | null; } function getActiveMetadata( manager: EditorManagerWithLsp | undefined, file: AcodeFile | undefined, ): (FileMetadata & { view?: EditorView }) | null { if (!manager?.getLspMetadata || !file) return null; const metadata = manager.getLspMetadata(file); if (!metadata) return null; return { ...metadata, view: manager.editor, }; } export function registerLspFormatter(acode: AcodeApi): void { const languages = new Set(); serverRegistry.listServers().forEach((server) => { if (!supportsBuiltinFormatting(server)) return; (server.languages || []).forEach((lang) => { if (lang) languages.add(String(lang)); }); }); const extensions = languages.size ? collectFormatterExtensions(languages) : ["*"]; acode.registerFormatter( "lsp", extensions, async () => { const manager = window.editorManager as EditorManagerWithLsp | undefined; const file = manager?.activeFile; const metadata = getActiveMetadata(manager, file); if (!metadata) { toast("LSP formatter unavailable"); return false; } const languageId = metadata.languageId; if (!languageId) { toast("Unknown language for LSP formatting"); return false; } const servers = serverRegistry .getServersForLanguage(languageId) .filter(supportsBuiltinFormatting); if (!servers.length) { toast("No LSP formatter available"); return false; } const fullMetadata = { ...metadata, languageName: metadata.languageName || languageId, }; const success = await lspClientManager.formatDocument(fullMetadata); if (!success) { toast("LSP formatter failed"); } return success; }, "Language Server", ); } function collectFormatterExtensions(languages: Set): string[] { const extensions = new Set(); const modeMap = new Map(); try { const modes = getModes() as Mode[]; modes.forEach((mode) => { const key = String(mode?.name ?? "") .trim() .toLowerCase(); if (key) modeMap.set(key, mode); }); } catch (_) { // Ignore mode loading errors } languages.forEach((language) => { const key = String(language ?? "") .trim() .toLowerCase(); if (!key) return; extensions.add(key); const mode = modeMap.get(key); if (!mode?.extensions) return; String(mode.extensions) .split("|") .forEach((part) => { const ext = part.trim(); if (ext && !ext.startsWith("^")) { extensions.add(ext); } }); }); if (!extensions.size) { return ["*"]; } return Array.from(extensions); } ================================================ FILE: src/cm/lsp/formattingSupport.ts ================================================ import type { LspServerDefinition } from "./types"; export function supportsBuiltinFormatting( server: LspServerDefinition, ): boolean { return server.clientConfig?.builtinExtensions?.formatting !== false; } ================================================ FILE: src/cm/lsp/index.ts ================================================ export { bundles, default as lspApi, defineBundle, defineServer, installers, register, servers, upsert, } from "./api"; export { default as clientManager, LspClientManager } from "./clientManager"; export type { CodeActionItem } from "./codeActions"; export { CODE_ACTION_KINDS, executeCodeAction, fetchCodeActions, formatCodeActionKind, getCodeActionIcon, performQuickFix, showCodeActionsMenu, supportsCodeActions, } from "./codeActions"; export { clearDiagnosticsEffect, getLspDiagnostics, LSP_DIAGNOSTICS_EVENT, lspDiagnosticsClientExtension, lspDiagnosticsExtension, lspDiagnosticsUiExtension, } from "./diagnostics"; export type { DocumentSymbolsResult, FlatSymbol, ProcessedSymbol, } from "./documentSymbols"; export { fetchDocumentSymbols, getDocumentSymbols, getDocumentSymbolsFlat, getSymbolKindIcon, getSymbolKindName, navigateToSymbol, SymbolKind, supportsDocumentSymbols, } from "./documentSymbols"; export { registerLspFormatter } from "./formatter"; export type { InlayHintsConfig } from "./inlayHints"; export { inlayHintsClientExtension, inlayHintsEditorExtension, inlayHintsExtension, } from "./inlayHints"; export { closeReferencesPanel, findAllReferences, findAllReferencesInTab, } from "./references"; export { acodeRenameExtension, acodeRenameKeymap, renameSymbol, } from "./rename"; export { ensureServerRunning, resetManagedServers, stopManagedServer, } from "./serverLauncher"; export { default as serverRegistry } from "./serverRegistry"; export { nextSignature, prevSignature, showSignatureHelp, } from "./tooltipExtensions"; export { createTransport } from "./transport"; export type { BuiltinExtensionsConfig, ClientManagerOptions, ClientState, DiagnosticRelatedInformation, DocumentUriContext, FileMetadata, FormattingOptions, LSPClientWithWorkspace, LSPDiagnostic, LSPFormattingOptions, LSPPluginAPI, LspDiagnostic, LspServerDefinition, Position, Range, TextEdit, TransportDescriptor, TransportHandle, WorkspaceOptions, } from "./types"; export { default as AcodeWorkspace } from "./workspace"; ================================================ FILE: src/cm/lsp/inlayHints.ts ================================================ /** * LSP Inlay Hints Extension for CodeMirror * * Provides inline hints (type annotations, parameter names, etc.) from language servers. */ import type { LSPClient, LSPClientExtension } from "@codemirror/lsp-client"; import { LSPPlugin } from "@codemirror/lsp-client"; import type { Extension, Range } from "@codemirror/state"; import { RangeSet, StateEffect, StateField } from "@codemirror/state"; import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType, } from "@codemirror/view"; import type { InlayHint, InlayHintLabelPart, Position, } from "vscode-languageserver-types"; import type { LSPPluginAPI } from "./types"; // ============================================================================ // Types // ============================================================================ interface InlayHintParams { textDocument: { uri: string }; range: { start: Position; end: Position }; } interface ProcessedHint { pos: number; label: string; paddingLeft?: boolean; paddingRight?: boolean; tooltip?: string; } export interface InlayHintsConfig { enabled?: boolean; debounceMs?: number; showTypes?: boolean; showParameters?: boolean; maxHints?: number; } // LSP InlayHintKind constants const TYPE_HINT = 1; const PARAM_HINT = 2; // ============================================================================ // State // ============================================================================ const setHints = StateEffect.define(); const hintsField = StateField.define({ create: () => [], update(hints, tr) { for (const e of tr.effects) { if (e.is(setHints)) return e.value; } return hints; }, }); // ============================================================================ // Widget // ============================================================================ class HintWidget extends WidgetType { constructor( readonly label: string, readonly padLeft: boolean, readonly padRight: boolean, readonly tooltip: string | undefined, ) { super(); } eq(other: HintWidget): boolean { return ( this.label === other.label && this.padLeft === other.padLeft && this.padRight === other.padRight ); } toDOM(): HTMLSpanElement { const el = document.createElement("span"); el.className = `cm-inlay-hint${this.padLeft ? " cm-inlay-hint-pl" : ""}${this.padRight ? " cm-inlay-hint-pr" : ""}`; el.textContent = this.label; if (this.tooltip) el.title = this.tooltip; return el; } ignoreEvent(): boolean { return true; } } // ============================================================================ // Decorations // ============================================================================ function buildDecos(hints: ProcessedHint[], docLen: number): DecorationSet { if (!hints.length) return Decoration.none; const decos: Range[] = []; for (const h of hints) { if (h.pos < 0 || h.pos > docLen) continue; decos.push( Decoration.widget({ widget: new HintWidget( h.label, h.paddingLeft ?? false, h.paddingRight ?? false, h.tooltip, ), side: 1, }).range(h.pos), ); } return RangeSet.of(decos, true); } // ============================================================================ // Plugin // ============================================================================ function createPlugin(config: InlayHintsConfig) { const delay = config.debounceMs ?? 200; const max = config.maxHints ?? 500; const showTypes = config.showTypes !== false; const showParams = config.showParameters !== false; return ViewPlugin.fromClass( class { decorations: DecorationSet = Decoration.none; timer: ReturnType | null = null; reqId = 0; constructor(private view: EditorView) { this.fetch(); } update(update: ViewUpdate): void { if ( update.transactions.some((t) => t.effects.some((e) => e.is(setHints))) ) { this.decorations = buildDecos( update.state.field(hintsField, false) ?? [], update.state.doc.length, ); } if (update.docChanged || update.viewportChanged) { this.schedule(); } } schedule(): void { if (this.timer) clearTimeout(this.timer); this.timer = setTimeout(() => { this.timer = null; this.fetch(); }, delay); } async fetch(): Promise { const lsp = LSPPlugin.get(this.view) as LSPPluginAPI | null; if (!lsp?.client.connected) return; const caps = lsp.client.serverCapabilities; if (!caps?.inlayHintProvider) return; lsp.client.sync(); const id = ++this.reqId; const doc = this.view.state.doc; // Visible range with buffer const { from, to } = this.view.viewport; const buf = 20; const startLn = Math.max(1, doc.lineAt(Math.max(0, from)).number - buf); const endLn = Math.min( doc.lines, doc.lineAt(Math.min(doc.length, to)).number + buf, ); try { const hints = await lsp.client.request< InlayHintParams, InlayHint[] | null >("textDocument/inlayHint", { textDocument: { uri: lsp.uri }, range: { start: lsp.toPosition(doc.line(startLn).from), end: lsp.toPosition(doc.line(endLn).to), }, }); if (id !== this.reqId) return; const processed = this.process(lsp, hints ?? [], doc.length); this.view.dispatch({ effects: setHints.of(processed) }); } catch { // Non-critical - silently ignore } } process( lsp: LSPPluginAPI, hints: InlayHint[], docLen: number, ): ProcessedHint[] { const result: ProcessedHint[] = []; for (const h of hints) { if (h.kind === TYPE_HINT && !showTypes) continue; if (h.kind === PARAM_HINT && !showParams) continue; let pos: number; try { pos = lsp.fromPosition(h.position, lsp.syncedDoc); const mapped = lsp.unsyncedChanges.mapPos(pos); if (mapped === null) continue; pos = mapped; } catch { continue; } if (pos < 0 || pos > docLen) continue; const label = typeof h.label === "string" ? h.label : Array.isArray(h.label) ? h.label.map((p: InlayHintLabelPart) => p.value).join("") : ""; if (!label) continue; const tooltip = typeof h.tooltip === "string" ? h.tooltip : h.tooltip && typeof h.tooltip === "object" && "value" in h.tooltip ? (h.tooltip as { value: string }).value : undefined; result.push({ pos, label, paddingLeft: h.paddingLeft, paddingRight: h.paddingRight, tooltip, }); if (result.length >= max) break; } return result.sort((a, b) => a.pos - b.pos); } destroy(): void { if (this.timer) clearTimeout(this.timer); } }, { decorations: (v) => v.decorations }, ); } // ============================================================================ // Styles // ============================================================================ const styles = EditorView.baseTheme({ ".cm-inlay-hint": { display: "inline-block", fontFamily: "inherit", fontSize: "0.9em", fontStyle: "italic", borderRadius: "3px", padding: "0 3px", margin: "0 2px", verticalAlign: "baseline", pointerEvents: "none", }, "&light .cm-inlay-hint": { color: "#6a737d", backgroundColor: "rgba(27, 31, 35, 0.05)", }, "&dark .cm-inlay-hint": { color: "#6a9955", backgroundColor: "rgba(255, 255, 255, 0.05)", }, ".cm-inlay-hint-pl": { marginLeft: "4px" }, ".cm-inlay-hint-pr": { marginRight: "4px" }, }); // ============================================================================ // Exports // ============================================================================ export function inlayHintsClientExtension(): LSPClientExtension { return { clientCapabilities: { textDocument: { inlayHint: { dynamicRegistration: true, resolveSupport: { properties: [ "tooltip", "textEdits", "label.tooltip", "label.location", "label.command", ], }, }, }, }, }; } export function inlayHintsEditorExtension( config: InlayHintsConfig = {}, ): Extension { if (config.enabled === false) return []; return [hintsField, createPlugin(config), styles]; } export function inlayHintsExtension( config: InlayHintsConfig = {}, ): LSPClientExtension & { editorExtension: Extension } { return { ...inlayHintsClientExtension(), editorExtension: inlayHintsEditorExtension(config), }; } export default inlayHintsExtension; ================================================ FILE: src/cm/lsp/installRuntime.ts ================================================ function getExecutor(): Executor { const executor = (globalThis as unknown as { Executor?: Executor }).Executor; if (!executor) { throw new Error("Executor plugin is not available"); } return executor; } function getBackgroundExecutor(): Executor { const executor = getExecutor(); return executor.BackgroundExecutor ?? executor; } export function quoteArg(value: unknown): string { const str = String(value ?? ""); if (!str.length) return "''"; if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(str)) return str; return `'${str.replace(/'/g, "'\\''")}'`; } export function formatCommand( command: string | string[] | null | undefined, ): string { if (Array.isArray(command)) { return command.map((part) => quoteArg(part)).join(" "); } if (typeof command === "string") { return command.trim(); } return ""; } function wrapShellCommand(command: string): string { const script = command.trim(); return `sh -lc ${quoteArg(`set -e\n${script}`)}`; } export async function runQuickCommand(command: string): Promise { const wrapped = wrapShellCommand(command); return getBackgroundExecutor().execute(wrapped, true); } export async function runForegroundCommand(command: string): Promise { const wrapped = wrapShellCommand(command); return getExecutor().execute(wrapped, true); } ================================================ FILE: src/cm/lsp/installerUtils.ts ================================================ const ARCH_ALIASES = { aarch64: ["aarch64", "arm64", "arm64-v8a"], x86_64: ["x86_64", "amd64"], armv7: ["armv7", "armv7l", "armeabi-v7a"], } as const; export type NormalizedArch = keyof typeof ARCH_ALIASES; export function normalizeArchitecture(arch: string | null | undefined): string { const normalized = String(arch || "") .trim() .toLowerCase(); for (const [canonical, aliases] of Object.entries(ARCH_ALIASES)) { if (aliases.includes(normalized as never)) { return canonical; } } return normalized; } export function getArchitectureMatchers( assets: Record | undefined | null, ): Array<{ canonicalArch: string; aliases: string[]; asset: string }> { if (!assets || typeof assets !== "object") return []; const resolved = new Map(); for (const [rawArch, rawAsset] of Object.entries(assets)) { const asset = String(rawAsset || "").trim(); if (!asset) continue; const canonicalArch = normalizeArchitecture(rawArch); if (!canonicalArch) continue; const aliases = ( ARCH_ALIASES[canonicalArch as NormalizedArch] || [canonicalArch] ).map((value) => String(value)); resolved.set(canonicalArch, { aliases, asset }); } return Array.from(resolved.entries()).map(([canonicalArch, value]) => ({ canonicalArch, aliases: value.aliases, asset: value.asset, })); } export function buildShellArchCase( assets: Record | undefined | null, quote: (value: unknown) => string, ): string { return getArchitectureMatchers(assets) .map( ({ aliases, asset }) => `\t${aliases.join("|")}) ASSET=${quote(asset)} ;;`, ) .join("\n"); } ================================================ FILE: src/cm/lsp/providerUtils.ts ================================================ import type { BridgeConfig, InstallCheckResult, LauncherInstallConfig, LspServerBundle, LspServerManifest, TransportDescriptor, } from "./types"; export interface ManagedServerOptions { id: string; label: string; languages: string[]; enabled?: boolean; useWorkspaceFolders?: boolean; command?: string; args?: string[]; transport?: Partial; bridge?: Partial | null; installer?: LauncherInstallConfig; checkCommand?: string; versionCommand?: string; updateCommand?: string; uninstallCommand?: string; startupTimeout?: number; initializationOptions?: Record; clientConfig?: LspServerManifest["clientConfig"]; resolveLanguageId?: LspServerManifest["resolveLanguageId"]; rootUri?: LspServerManifest["rootUri"]; documentUri?: LspServerManifest["documentUri"]; capabilityOverrides?: Record; } export interface BundleHooks { getExecutable?: ( serverId: string, manifest: LspServerManifest, ) => string | null | undefined; checkInstallation?: ( serverId: string, manifest: LspServerManifest, ) => Promise; installServer?: ( serverId: string, manifest: LspServerManifest, mode: "install" | "update" | "reinstall", options?: { promptConfirm?: boolean }, ) => Promise; } export function defineBundle(options: { id: string; label?: string; servers: LspServerManifest[]; hooks?: BundleHooks; }): LspServerBundle { const { id, label, servers, hooks } = options; return { id, label, getServers: () => servers, ...hooks, }; } export function defineServer(options: ManagedServerOptions): LspServerManifest { const { id, label, languages, enabled = true, useWorkspaceFolders = false, command, args, transport, bridge, installer, checkCommand, versionCommand, updateCommand, uninstallCommand, startupTimeout, initializationOptions, clientConfig, resolveLanguageId, rootUri, documentUri, capabilityOverrides, } = options; const bridgeCommand = command || bridge?.command; return { id, label, languages, enabled, useWorkspaceFolders, transport: { kind: "websocket", ...(transport || {}), } as TransportDescriptor, launcher: { checkCommand, versionCommand, updateCommand, uninstallCommand, install: installer, bridge: bridgeCommand ? { kind: "axs", command: bridgeCommand, args: args || bridge?.args, port: bridge?.port, session: bridge?.session, } : undefined, }, startupTimeout, initializationOptions, clientConfig, resolveLanguageId, rootUri, documentUri, capabilityOverrides, }; } export const installers = { apk(options: { packages: string[]; executable: string; label?: string; source?: string; }): LauncherInstallConfig { return { kind: "apk", source: options.source || "apk", label: options.label, executable: options.executable, packages: options.packages, }; }, npm(options: { packages: string[]; executable: string; label?: string; source?: string; global?: boolean; }): LauncherInstallConfig { return { kind: "npm", source: options.source || "npm", label: options.label, executable: options.executable, packages: options.packages, global: options.global, }; }, pip(options: { packages: string[]; executable: string; label?: string; source?: string; breakSystemPackages?: boolean; }): LauncherInstallConfig { return { kind: "pip", source: options.source || "pip", label: options.label, executable: options.executable, packages: options.packages, breakSystemPackages: options.breakSystemPackages, }; }, cargo(options: { packages: string[]; executable: string; label?: string; source?: string; }): LauncherInstallConfig { return { kind: "cargo", source: options.source || "cargo", label: options.label, executable: options.executable, packages: options.packages, }; }, manual(options: { binaryPath: string; executable?: string; label?: string; source?: string; }): LauncherInstallConfig { return { kind: "manual", source: options.source || "manual", label: options.label, executable: options.executable || options.binaryPath, binaryPath: options.binaryPath, }; }, shell(options: { command: string; executable: string; updateCommand?: string; uninstallCommand?: string; label?: string; source?: string; }): LauncherInstallConfig { return { kind: "shell", source: options.source || "custom", label: options.label, executable: options.executable, command: options.command, updateCommand: options.updateCommand, uninstallCommand: options.uninstallCommand, }; }, githubRelease(options: { repo: string; binaryPath: string; executable?: string; assetNames: Record; extractFile?: string; archiveType?: "zip" | "binary"; label?: string; source?: string; }): LauncherInstallConfig { return { kind: "github-release", source: options.source || "github-release", label: options.label, executable: options.executable || options.binaryPath, repo: options.repo, assetNames: options.assetNames, extractFile: options.extractFile, archiveType: options.archiveType, binaryPath: options.binaryPath, }; }, }; ================================================ FILE: src/cm/lsp/references.ts ================================================ import fsOperation from "fileSystem"; import { LSPPlugin } from "@codemirror/lsp-client"; import type { EditorView } from "@codemirror/view"; import { openReferencesTab, showReferencesPanel, } from "components/referencesPanel"; import settings from "lib/settings"; interface Position { line: number; character: number; } interface Range { start: Position; end: Position; } interface Location { uri: string; range: Range; } interface ReferenceWithContext extends Location { lineText?: string; } interface ReferenceParams { textDocument: { uri: string }; position: Position; context: { includeDeclaration: boolean }; } async function fetchLineText(uri: string, line: number): Promise { try { interface EditorManagerLike { getFile?: (uri: string, type: string) => EditorFileLike | null; } interface EditorFileLike { session?: { doc?: { line?: (n: number) => { text?: string } | null; toString?: () => string; }; }; } const em = (globalThis as Record).editorManager as | EditorManagerLike | undefined; const openFile = em?.getFile?.(uri, "uri"); if (openFile?.session?.doc) { const doc = openFile.session.doc; if (typeof doc.line === "function") { const lineObj = doc.line(line + 1); if (lineObj && typeof lineObj.text === "string") { return lineObj.text; } } if (typeof doc.toString === "function") { const content = doc.toString(); const lines = content.split("\n"); if (lines[line] !== undefined) { return lines[line]; } } } const fs = fsOperation(uri); if (fs && (await fs.exists())) { const encoding = (settings as { value?: { defaultFileEncoding?: string } })?.value ?.defaultFileEncoding || "utf-8"; const content = await fs.readFile(encoding); if (typeof content === "string") { const lines = content.split("\n"); if (lines[line] !== undefined) { return lines[line]; } } } } catch (error) { console.warn(`Failed to fetch line text for ${uri}:${line}`, error); } return ""; } function getWordAtCursor(view: EditorView): string { const { state } = view; const pos = state.selection.main.head; const word = state.wordAt(pos); if (word) { return state.doc.sliceString(word.from, word.to); } return ""; } async function fetchReferences( view: EditorView, ): Promise<{ symbolName: string; references: ReferenceWithContext[] } | null> { const plugin = LSPPlugin.get(view); if (!plugin) { return null; } const client = plugin.client; const capabilities = client.serverCapabilities; if (!capabilities?.referencesProvider) { const toast = (globalThis as Record).toast as | ((msg: string) => void) | undefined; toast?.("Language server does not support find references"); return null; } const { state } = view; const pos = state.selection.main.head; const line = state.doc.lineAt(pos); const lineNumber = line.number - 1; const character = pos - line.from; const uri = plugin.uri; const symbolName = getWordAtCursor(view); client.sync(); const params: ReferenceParams = { textDocument: { uri }, position: { line: lineNumber, character }, context: { includeDeclaration: true }, }; const locations = await client.request( "textDocument/references", params, ); if (!locations || locations.length === 0) { return { symbolName, references: [] }; } const refsWithContext: ReferenceWithContext[] = await Promise.all( locations.map(async (loc) => { const lineText = await fetchLineText(loc.uri, loc.range.start.line); return { ...loc, lineText, }; }), ); return { symbolName, references: refsWithContext }; } export async function findAllReferences(view: EditorView): Promise { const plugin = LSPPlugin.get(view); if (!plugin) { return false; } const symbolName = getWordAtCursor(view); const panel = showReferencesPanel({ symbolName }); try { const result = await fetchReferences(view); if (result === null) { panel.setError("Failed to fetch references"); return false; } panel.setReferences(result.references); return true; } catch (error) { console.error("Find references failed:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; panel.setError(errorMessage); return false; } } export async function findAllReferencesInTab( view: EditorView, ): Promise { const plugin = LSPPlugin.get(view); if (!plugin) { const toast = (globalThis as Record).toast as | ((msg: string) => void) | undefined; toast?.("Language server not available"); return false; } try { const result = await fetchReferences(view); if (result === null) { return false; } if (result.references.length === 0) { const toast = (globalThis as Record).toast as | ((msg: string) => void) | undefined; toast?.("No references found"); return true; } openReferencesTab({ symbolName: result.symbolName, references: result.references, }); return true; } catch (error) { console.error("Find references in tab failed:", error); return false; } } export function closeReferencesPanel(): boolean { const { hideReferencesPanel } = require("components/referencesPanel"); hideReferencesPanel(); return true; } ================================================ FILE: src/cm/lsp/rename.ts ================================================ import { LSPPlugin } from "@codemirror/lsp-client"; import { type Command, EditorView, type KeyBinding, keymap, } from "@codemirror/view"; import prompt from "dialogs/prompt"; import type * as lsp from "vscode-languageserver-protocol"; import type AcodeWorkspace from "./workspace"; interface RenameParams { newName: string; position: lsp.Position; textDocument: { uri: string }; } interface TextDocumentEdit { range: lsp.Range; newText: string; } interface PrepareRenameResponse { range?: lsp.Range; placeholder?: string; defaultBehavior?: boolean; } interface LspChange { range: lsp.Range; newText: string; } function getRename(plugin: LSPPlugin, pos: number, newName: string) { return plugin.client.request( "textDocument/rename", { newName, position: plugin.toPosition(pos), textDocument: { uri: plugin.uri }, }, ); } function getPrepareRename(plugin: LSPPlugin, pos: number) { return plugin.client.request< { position: lsp.Position; textDocument: { uri: string } }, PrepareRenameResponse | lsp.Range | null >("textDocument/prepareRename", { position: plugin.toPosition(pos), textDocument: { uri: plugin.uri }, }); } async function performRename(view: EditorView): Promise { const wordRange = view.state.wordAt(view.state.selection.main.head); const plugin = LSPPlugin.get(view); if (!plugin) { return false; } const capabilities = plugin.client.serverCapabilities; const renameProvider = capabilities?.renameProvider; if (renameProvider === false || renameProvider === undefined) { return false; } if (!wordRange) { return false; } const word = view.state.sliceDoc(wordRange.from, wordRange.to); let initialValue = word; let canRename = true; const supportsPrepare = typeof renameProvider === "object" && renameProvider !== null && "prepareProvider" in renameProvider && renameProvider.prepareProvider === true; if (supportsPrepare) { try { plugin.client.sync(); const prepareResult = await getPrepareRename(plugin, wordRange.from); if (prepareResult === null) { canRename = false; } else if (typeof prepareResult === "object" && prepareResult !== null) { if ("placeholder" in prepareResult && prepareResult.placeholder) { initialValue = prepareResult.placeholder; } else if ( "defaultBehavior" in prepareResult && prepareResult.defaultBehavior ) { initialValue = word; } else if ("start" in prepareResult && "end" in prepareResult) { const from = plugin.fromPosition(prepareResult.start); const to = plugin.fromPosition(prepareResult.end); initialValue = view.state.sliceDoc(from, to); } else if ("range" in prepareResult && prepareResult.range) { const from = plugin.fromPosition(prepareResult.range.start); const to = plugin.fromPosition(prepareResult.range.end); initialValue = view.state.sliceDoc(from, to); } } } catch (error) { console.warn("[LSP:Rename] prepareRename failed, using word:", error); } } if (!canRename) { const alert = (await import("dialogs/alert")).default; alert("Rename", "Cannot rename this symbol."); return true; } const newName = await prompt( strings["new name"] || "New name", initialValue, "text", { required: true, placeholder: strings["enter new name"] || "Enter new name", }, ); if (newName === null || newName === initialValue) { return true; } try { await doRename(view, String(newName), wordRange.from); } catch (error) { console.error("[LSP:Rename] Rename failed:", error); const errorMessage = error instanceof Error ? error.message : "Failed to rename symbol"; const alert = (await import("dialogs/alert")).default; alert("Rename Error", errorMessage); } return true; } function lspPositionToOffset( doc: { line: (n: number) => { from: number } }, pos: lsp.Position, ): number { const line = doc.line(pos.line + 1); return line.from + pos.character; } async function applyChangesToFile( workspace: AcodeWorkspace, uri: string, lspChanges: LspChange[], mapping: { mapPosition: (uri: string, pos: lsp.Position) => number }, ): Promise { const file = workspace.getFile(uri); if (file) { const view = file.getView(); if (view) { view.dispatch({ changes: lspChanges.map((change) => ({ from: mapping.mapPosition(uri, change.range.start), to: mapping.mapPosition(uri, change.range.end), insert: change.newText, })), userEvent: "rename", }); return true; } } const displayedView = await workspace.displayFile(uri); if (!displayedView?.state?.doc) { console.warn(`[LSP:Rename] Could not open file: ${uri}`); return false; } const doc = displayedView.state.doc; displayedView.dispatch({ changes: lspChanges.map((change) => ({ from: lspPositionToOffset(doc, change.range.start), to: lspPositionToOffset(doc, change.range.end), insert: change.newText, })), userEvent: "rename", }); return true; } async function doRename( view: EditorView, newName: string, position: number, ): Promise { const plugin = LSPPlugin.get(view); if (!plugin) return; plugin.client.sync(); const response = await plugin.client.withMapping((mapping) => getRename(plugin, position, newName).then((response) => { if (!response) return null; return { response, mapping }; }), ); if (!response) { console.info("[LSP:Rename] No changes returned from server"); return; } const { response: workspaceEdit, mapping } = response; const workspace = plugin.client.workspace as AcodeWorkspace; let filesChanged = 0; if (workspaceEdit.changes) { for (const uri in workspaceEdit.changes) { const lspChanges = workspaceEdit.changes[uri] as TextDocumentEdit[]; if (!lspChanges.length) continue; const success = await applyChangesToFile( workspace, uri, lspChanges, mapping, ); if (success) filesChanged++; } } if (workspaceEdit.documentChanges) { for (const docChange of workspaceEdit.documentChanges) { if ("textDocument" in docChange && "edits" in docChange) { const uri = docChange.textDocument.uri; const edits = docChange.edits as TextDocumentEdit[]; if (!edits.length) continue; const success = await applyChangesToFile( workspace, uri, edits, mapping, ); if (success) filesChanged++; } } } console.info( `[LSP:Rename] Renamed to "${newName}" in ${filesChanged} file(s)`, ); } export const renameSymbol: Command = (view) => { performRename(view).catch((error) => { console.error("[LSP:Rename] Rename command failed:", error); }); return true; }; export const acodeRenameKeymap: readonly KeyBinding[] = [ { key: "F2", run: renameSymbol, preventDefault: true }, ]; export const acodeRenameExtension = () => keymap.of([...acodeRenameKeymap]); ================================================ FILE: src/cm/lsp/serverCatalog.ts ================================================ import { builtinServerBundles } from "./servers"; import type { LspServerBundle, LspServerManifest } from "./types"; function toKey(id: string | undefined | null): string { return String(id ?? "") .trim() .toLowerCase(); } interface RegistryAdapter { registerServer: ( definition: LspServerManifest, options?: { replace?: boolean }, ) => unknown; unregisterServer: (id: string) => boolean; } const bundles = new Map(); const bundleServers = new Map>(); const serverOwners = new Map(); let registryAdapter: RegistryAdapter | null = null; let builtinsRegistered = false; export function bindServerRegistry(adapter: RegistryAdapter): void { registryAdapter = adapter; } function requireRegistry(): RegistryAdapter { if (!registryAdapter) { throw new Error("LSP server catalog is not bound to the registry"); } return registryAdapter; } function resolveBundleServers(bundle: LspServerBundle): LspServerManifest[] { const servers = bundle.getServers(); return Array.isArray(servers) ? servers : []; } export function registerServerBundle( bundle: LspServerBundle, options: { replace?: boolean } = {}, ): LspServerBundle { const { replace = false } = options; const key = toKey(bundle.id); if (!key) { throw new Error("LSP server bundle requires a non-empty id"); } if (bundles.has(key) && !replace) { const existing = bundles.get(key); if (existing) return existing; } const registry = requireRegistry(); const definitions = resolveBundleServers(bundle); const previousIds = bundleServers.get(key) || new Set(); const nextIds = new Set(); for (const definition of definitions) { const serverId = toKey(definition.id); if (!serverId) { throw new Error(`LSP server bundle ${key} returned a server without id`); } const owner = serverOwners.get(serverId); if (owner && owner !== key && !replace) { throw new Error( `LSP server ${serverId} is already provided by ${owner}; ${key} must replace explicitly`, ); } registry.registerServer(definition, { replace: true }); serverOwners.set(serverId, key); nextIds.add(serverId); } for (const previousId of previousIds) { if (!nextIds.has(previousId) && serverOwners.get(previousId) === key) { registry.unregisterServer(previousId); serverOwners.delete(previousId); } } const normalizedBundle = { ...bundle, id: key, }; bundles.set(key, normalizedBundle); bundleServers.set(key, nextIds); return normalizedBundle; } export function unregisterServerBundle(id: string): boolean { const key = toKey(id); if (!key || !bundles.has(key)) return false; const registry = requireRegistry(); for (const serverId of bundleServers.get(key) || []) { if (serverOwners.get(serverId) === key) { registry.unregisterServer(serverId); serverOwners.delete(serverId); } } bundleServers.delete(key); return bundles.delete(key); } export function listServerBundles(): LspServerBundle[] { return Array.from(bundles.values()); } export function getServerBundle(id: string): LspServerBundle | null { const owner = serverOwners.get(toKey(id)); if (!owner) return null; return bundles.get(owner) || null; } export function ensureBuiltinBundlesRegistered(): void { if (builtinsRegistered) return; builtinServerBundles.forEach((bundle) => { registerServerBundle(bundle, { replace: false }); }); builtinsRegistered = true; } ================================================ FILE: src/cm/lsp/serverLauncher.ts ================================================ import lspStatusBar from "components/lspStatusBar"; import toast from "components/toast"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import { buildShellArchCase } from "./installerUtils"; import { formatCommand, quoteArg, runForegroundCommand, runQuickCommand, } from "./installRuntime"; import { getServerBundle } from "./serverCatalog"; import type { BridgeConfig, InstallCheckResult, InstallStatus, LauncherConfig, LspServerDefinition, LspServerStats, LspServerStatsFormatted, ManagedServerEntry, PortInfo, WaitOptions, } from "./types"; const managedServers = new Map(); const checkedCommands = new Map(); const pendingInstallChecks = new Map>(); const announcedServers = new Set(); const STATUS_PRESENT: InstallStatus = "present"; const STATUS_DECLINED: InstallStatus = "declined"; const STATUS_FAILED: InstallStatus = "failed"; const AXS_BINARY = "$PREFIX/axs"; function getTerminalRequiredMessage(): string { return ( strings?.terminal_required_message_for_lsp ?? "Terminal not installed. Please install Terminal first to use LSP servers." ); } interface LspError extends Error { code?: string; } function getExecutor(): Executor { const executor = (globalThis as unknown as { Executor?: Executor }).Executor; if (!executor) { throw new Error("Executor plugin is not available"); } return executor; } /** * Get the background executor */ function getBackgroundExecutor(): Executor { const executor = getExecutor(); return executor.BackgroundExecutor ?? executor; } function joinCommand(command: string, args: string[] = []): string { if (!Array.isArray(args) || !args.length) return quoteArg(command); return [quoteArg(command), ...args.map((arg) => quoteArg(arg))].join(" "); } export { formatCommand } from "./installRuntime"; // ============================================================================ // Auto-Port Discovery // ============================================================================ // Cache for the filesDir path let cachedFilesDir: string | null = null; /** * Get the terminal home directory from system.getFilesDir(). * This is where axs stores port files. */ async function getTerminalHomeDir(): Promise { if (cachedFilesDir) { return `${cachedFilesDir}/alpine/home`; } const system = ( globalThis as unknown as { system?: { getFilesDir: ( success: (filesDir: string) => void, error: (error: string) => void, ) => void; }; } ).system; if (!system?.getFilesDir) { throw new Error("System plugin is not available"); } return new Promise((resolve, reject) => { system.getFilesDir( (filesDir: string) => { cachedFilesDir = filesDir; resolve(`${filesDir}/alpine/home`); }, (error: string) => reject(new Error(error)), ); }); } /** * Get the port file path for a given server and session. * Port file format: ~/.axs/lsp_ports/{serverName}_{session} */ async function getPortFilePath( serverName: string, session: string, ): Promise { const homeDir = await getTerminalHomeDir(); // Use just the binary name (not full path), mirroring axs behavior const baseName = serverName.split("/").pop() || serverName; return `file://${homeDir}/.axs/lsp_ports/${baseName}_${session}`; } /** * Read the port from a port file using the filesystem API. * Returns null if the file doesn't exist or contains invalid data. */ async function readPortFromFile(filePath: string): Promise { try { // Dynamic import to get fsOperation const { default: fsOperation } = await import("fileSystem"); const fs = fsOperation(filePath); // Check if file exists first const exists = await fs.exists(); if (!exists) { return null; } // Read the file content as text const content = (await fs.readFile("utf-8")) as string; const port = Number.parseInt(content.trim(), 10); if (!Number.isFinite(port) || port <= 0 || port > 65535) { return null; } return port; } catch { // File doesn't exist or couldn't be read return null; } } /** * Get the port for a running LSP server from the axs port file. * @param serverName - The LSP server binary name (e.g., "typescript-language-server") * @param session - Session ID for port file naming */ export async function getLspPort( serverName: string, session: string, ): Promise { try { const filePath = await getPortFilePath(serverName, session); const port = await readPortFromFile(filePath); if (port === null) { return null; } return { port, filePath, session }; } catch { return null; } } /** * Wait for the server ready signal (when axs prints "listening on"). * The axs proxy writes the port file immediately after binding, then prints the message. * So once the signal is received, the port file should be available. */ async function waitForServerReady( serverId: string, timeout = 10000, ): Promise { const deadline = Date.now() + timeout; const pollInterval = 50; while (Date.now() < deadline) { if (serverReadySignals.has(serverId)) { serverReadySignals.delete(serverId); return true; } await sleep(pollInterval); } return false; } /** * Wait for the port file to be available after server signals ready. * This is the most efficient approach: wait for ready signal, then read port. */ async function waitForPort( serverId: string, serverName: string, session: string, timeout = 10000, ): Promise { // First, wait for the server to signal it's ready const ready = await waitForServerReady(serverId, timeout); if (!ready) { console.warn( `[LSP:${serverId}] Server did not signal ready within timeout`, ); } // The port file should be available now (axs writes it before printing "listening on") // Read it directly const portInfo = await getLspPort(serverName, session); if (!portInfo && ready) { // Server signaled ready but port file not found - retry a few times for (let i = 0; i < 5; i++) { await sleep(100); const retryPortInfo = await getLspPort(serverName, session); if (retryPortInfo) { return retryPortInfo; } } } return portInfo; } /** * Quick check if a server is running and connectable. * Attempts a fast WebSocket connection test. */ async function checkServerAlive(url: string, timeout = 1000): Promise { return new Promise((resolve) => { try { const ws = new WebSocket(url); const timer = setTimeout(() => { try { ws.close(); } catch {} resolve(false); }, timeout); ws.onopen = () => { clearTimeout(timer); try { ws.close(); } catch {} resolve(true); }; ws.onerror = () => { clearTimeout(timer); resolve(false); }; ws.onclose = () => { clearTimeout(timer); resolve(false); }; } catch { resolve(false); } }); } /** * Check if we can reuse an existing server by testing the port. * Returns the port number if the server is alive, null otherwise. */ export async function canReuseExistingServer( server: LspServerDefinition, session: string, ): Promise { const bridge = server.launcher?.bridge; const serverName = resolveServerExecutable(server) || bridge?.command || server.launcher?.command || server.id; const portInfo = await getLspPort(serverName, session); if (!portInfo) { return null; } const url = `ws://127.0.0.1:${portInfo.port}/`; const alive = await checkServerAlive(url, 1000); if (alive) { console.info( `[LSP:${server.id}] Reusing existing server on port ${portInfo.port}`, ); return portInfo.port; } console.info( `[LSP:${server.id}] Found stale port file, will start new server`, ); return null; } function buildAxsBridgeCommand( bridge: BridgeConfig | undefined, commandOverride?: string | null, session?: string, ): string | null { if (!bridge || bridge.kind !== "axs") return null; const binary = commandOverride || bridge.command ? String(commandOverride || bridge.command) : (() => { throw new Error("Bridge requires a command to execute"); })(); const args: string[] = Array.isArray(bridge.args) ? bridge.args.map((arg) => String(arg)) : []; // Use session ID or bridge session or server command as fallback session const effectiveSession = session || bridge.session || binary; const parts = [AXS_BINARY, "lsp"]; // Add --session flag for port file naming parts.push("--session", quoteArg(effectiveSession)); // Only add --port if explicitly specified if ( typeof bridge.port === "number" && bridge.port > 0 && bridge.port <= 65535 ) { parts.push("--port", String(bridge.port)); } parts.push(quoteArg(binary)); if (args.length) { parts.push("--"); args.forEach((arg) => parts.push(quoteArg(arg))); } return parts.join(" "); } function resolveStartCommand( server: LspServerDefinition, session?: string, ): string | null { const launcher = server.launcher; if (!launcher) return null; const executable = resolveServerExecutable(server); if (launcher.startCommand) { return formatCommand(launcher.startCommand); } if (launcher.command) { return joinCommand(executable || launcher.command, launcher.args); } if (launcher.bridge) { return buildAxsBridgeCommand(launcher.bridge, executable, session); } return null; } export function getStartCommand(server: LspServerDefinition): string | null { return resolveStartCommand(server); } function getInstallCacheKey(server: LspServerDefinition): string | null { const checkCommand = server.launcher?.checkCommand || buildDerivedCheckCommand(server); if (!checkCommand) return null; return `${server.id}:${checkCommand}`; } function normalizeInstallSpec(server: LspServerDefinition) { const install = server.launcher?.install; if (!install) return null; const packages = Array.isArray(install.packages) ? install.packages .map((entry) => String(entry || "").trim()) .filter(Boolean) : []; const kind = install.kind || (install.binaryPath ? "manual" : null) || (install.source === "apk" ? "apk" : null) || (install.source === "npm" ? "npm" : null) || (install.source === "pip" ? "pip" : null) || (install.source === "cargo" ? "cargo" : null) || (install.command ? "shell" : null) || "shell"; return { ...install, kind, packages, command: typeof install.command === "string" && install.command.trim() ? install.command.trim() : undefined, updateCommand: typeof install.updateCommand === "string" && install.updateCommand.trim() ? install.updateCommand.trim() : undefined, source: install.source || (kind === "shell" ? "custom" : kind === "manual" ? "manual" : kind), executable: typeof install.executable === "string" && install.executable.trim() ? install.executable.trim() : undefined, binaryPath: typeof install.binaryPath === "string" && install.binaryPath.trim() ? install.binaryPath.trim() : undefined, repo: typeof install.repo === "string" && install.repo.trim() ? install.repo.trim() : undefined, assetNames: install.assetNames && typeof install.assetNames === "object" ? Object.fromEntries( Object.entries(install.assetNames) .map(([key, value]) => [String(key), String(value || "").trim()]) .filter(([, value]) => Boolean(value)), ) : {}, archiveType: install.archiveType === "binary" ? "binary" : "zip", extractFile: typeof install.extractFile === "string" && install.extractFile.trim() ? install.extractFile.trim() : undefined, npmCommand: typeof install.npmCommand === "string" && install.npmCommand.trim() ? install.npmCommand.trim() : "npm", pipCommand: typeof install.pipCommand === "string" && install.pipCommand.trim() ? install.pipCommand.trim() : "pip", pythonCommand: typeof install.pythonCommand === "string" && install.pythonCommand.trim() ? install.pythonCommand.trim() : "python3", global: install.global !== false, breakSystemPackages: install.breakSystemPackages !== false, }; } function getInstallerExecutable(server: LspServerDefinition): string | null { const install = normalizeInstallSpec(server); if (!install) return null; return install.binaryPath || install.executable || null; } function getProviderExecutable(server: LspServerDefinition): string | null { const bundle = getServerBundle(server.id); if (!bundle?.getExecutable) return null; try { return bundle.getExecutable(server.id, server) || null; } catch (error) { console.warn(`Failed to resolve bundle executable for ${server.id}`, error); return null; } } function resolveServerExecutable(server: LspServerDefinition): string | null { return ( getProviderExecutable(server) || getInstallerExecutable(server) || server.launcher?.bridge?.command || server.launcher?.command || null ); } function getInstallLabel(server: LspServerDefinition): string { return ( normalizeInstallSpec(server)?.label || server.launcher?.install?.label || server.label || server.id ).trim(); } function buildUninstallCommand(server: LspServerDefinition): string | null { const spec = normalizeInstallSpec(server); if (!spec) return null; if (spec.uninstallCommand) { return spec.uninstallCommand; } if (server.launcher?.uninstallCommand) { return server.launcher.uninstallCommand; } switch (spec.kind) { case "apk": return spec.packages.length ? `apk del ${spec.packages.map((entry) => quoteArg(entry)).join(" ")}` : null; case "npm": { if (!spec.packages.length) return null; const npmCommand = spec.npmCommand || "npm"; const uninstallFlags = spec.global !== false ? "uninstall -g" : "uninstall"; return `${npmCommand} ${uninstallFlags} ${spec.packages.map((entry) => quoteArg(entry)).join(" ")}`; } case "pip": return spec.packages.length ? `${spec.pipCommand || "pip"} uninstall -y ${spec.packages.map((entry) => quoteArg(entry)).join(" ")}` : null; case "cargo": return spec.packages.length ? spec.packages .map((entry) => `cargo uninstall ${quoteArg(entry)}`) .join(" && ") : null; case "github-release": case "manual": return spec.binaryPath ? `rm -f ${quoteArg(spec.binaryPath)}` : null; default: return null; } } function buildInstallCommand( server: LspServerDefinition, mode: "install" | "update" = "install", ): string | null { const spec = normalizeInstallSpec(server); if (!spec) return null; if (mode === "update" && spec.updateCommand) { return spec.updateCommand; } switch (spec.kind) { case "apk": return spec.packages.length ? `apk add --no-cache ${spec.packages.map((entry) => quoteArg(entry)).join(" ")}` : null; case "npm": { if (!spec.packages.length) return null; const npmCommand = spec.npmCommand || "npm"; const installFlags = spec.global !== false ? "install -g" : "install"; return `apk add --no-cache nodejs npm && ${npmCommand} ${installFlags} ${spec.packages.map((entry) => quoteArg(entry)).join(" ")}`; } case "pip": { if (!spec.packages.length) return null; const pipCommand = spec.pipCommand || "pip"; const breakPackages = spec.breakSystemPackages !== false ? "PIP_BREAK_SYSTEM_PACKAGES=1 " : ""; return `apk add --no-cache python3 py3-pip && ${breakPackages}${pipCommand} install ${spec.packages.map((entry) => quoteArg(entry)).join(" ")}`; } case "cargo": return spec.packages.length ? `apk add --no-cache rust cargo && cargo install ${spec.packages.map((entry) => quoteArg(entry)).join(" ")}` : null; case "github-release": { if (!spec.repo || !spec.binaryPath) return null; const caseLines = buildShellArchCase(spec.assetNames, quoteArg); if (!caseLines) return null; const archivePath = '"$TMP_DIR/$ASSET"'; const extractedFile = quoteArg(spec.extractFile || "luau-lsp"); const installTarget = quoteArg(spec.binaryPath); const downloadUrl = `https://github.com/${spec.repo}/releases/latest/download/$ASSET`; if (spec.archiveType === "binary") { return `apk add --no-cache curl && ARCH="$(uname -m)" && case "$ARCH" in\n${caseLines}\n\t*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;\nesac && TMP_DIR="$(mktemp -d)" && cleanup() { rm -rf "$TMP_DIR"; } && trap cleanup EXIT && curl -fsSL "${downloadUrl}" -o ${archivePath} && install -Dm755 ${archivePath} ${installTarget}`; } return `apk add --no-cache curl unzip && ARCH="$(uname -m)" && case "$ARCH" in\n${caseLines}\n\t*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;\nesac && TMP_DIR="$(mktemp -d)" && cleanup() { rm -rf "$TMP_DIR"; } && trap cleanup EXIT && curl -fsSL "${downloadUrl}" -o ${archivePath} && unzip -oq ${archivePath} -d "$TMP_DIR" && install -Dm755 "$TMP_DIR"/${extractedFile} ${installTarget}`; } case "manual": return null; default: return spec.command || null; } } function buildDerivedCheckCommand(server: LspServerDefinition): string | null { const binary = resolveServerExecutable(server)?.trim() || ""; const install = normalizeInstallSpec(server); if (install?.kind === "manual" && install.binaryPath) { return `test -x ${quoteArg(install.binaryPath)}`; } if (binary.includes("/")) { return `test -x ${quoteArg(binary)}`; } if (binary) { return `which ${quoteArg(binary)}`; } return null; } function getUpdateCommand(server: LspServerDefinition): string | null { const launcher = server.launcher; if (!launcher) return null; if ( typeof launcher.updateCommand === "string" && launcher.updateCommand.trim() ) { return launcher.updateCommand.trim(); } return buildInstallCommand(server, "update"); } async function readServerVersion( server: LspServerDefinition, ): Promise { const command = server.launcher?.versionCommand; if (!command) return null; try { const output = await runQuickCommand(command); const version = String(output || "") .split("\n") .map((line) => line.trim()) .find(Boolean); return version || null; } catch { return null; } } export function getInstallCommand( server: LspServerDefinition, mode: "install" | "update" = "install", ): string | null { if (mode === "update") { return getUpdateCommand(server); } return buildInstallCommand(server, "install"); } export function getInstallSource(server: LspServerDefinition): string | null { return normalizeInstallSpec(server)?.source || null; } export function getUninstallCommand( server: LspServerDefinition, ): string | null { return buildUninstallCommand(server); } export async function checkServerInstallation( server: LspServerDefinition, ): Promise { const bundle = getServerBundle(server.id); if (bundle?.checkInstallation) { try { const result = await bundle.checkInstallation(server.id, server); if (result) return result; } catch (error) { return { status: "failed", version: null, canInstall: Boolean(getInstallCommand(server, "install")), canUpdate: Boolean(getInstallCommand(server, "update")), message: error instanceof Error ? error.message : String(error), }; } } const launcher = server.launcher; const installCommand = getInstallCommand(server, "install"); const updateCommand = getInstallCommand(server, "update"); const checkCommand = launcher?.checkCommand || buildDerivedCheckCommand(server); if (!checkCommand) { return { status: "unknown", version: await readServerVersion(server), canInstall: Boolean(installCommand), canUpdate: Boolean(updateCommand), message: "No install check configured for this server.", }; } try { await runQuickCommand(checkCommand); return { status: "present", version: await readServerVersion(server), canInstall: Boolean(installCommand), canUpdate: Boolean(updateCommand), }; } catch (error) { return { status: installCommand ? "missing" : "failed", version: null, canInstall: Boolean(installCommand), canUpdate: Boolean(updateCommand), message: error instanceof Error ? error.message : String(error), }; } } export function resetInstallState(serverId?: string): void { if (!serverId) { checkedCommands.clear(); return; } const prefix = `${serverId}:`; for (const key of Array.from(checkedCommands.keys())) { if (key.startsWith(prefix)) { checkedCommands.delete(key); } } } async function ensureInstalled(server: LspServerDefinition): Promise { const launcher = server.launcher; const checkCommand = launcher?.checkCommand || buildDerivedCheckCommand(server); if (!checkCommand) return true; const cacheKey = getInstallCacheKey(server); if (!cacheKey) return true; // Return cached result if already checked if (checkedCommands.has(cacheKey)) { const status = checkedCommands.get(cacheKey); if (status === STATUS_PRESENT) { return true; } if (status === STATUS_DECLINED) { return false; } checkedCommands.delete(cacheKey); } // If there's already a pending check for this server, wait for it if (pendingInstallChecks.has(cacheKey)) { const pending = pendingInstallChecks.get(cacheKey); if (pending) return pending; } // Create and track the pending promise const checkPromise = performInstallCheck(server, launcher, cacheKey); pendingInstallChecks.set(cacheKey, checkPromise); try { return await checkPromise; } finally { pendingInstallChecks.delete(cacheKey); } } interface LoaderDialog { show: () => void; destroy: () => void; } type InstallActionMode = "install" | "update" | "reinstall"; export async function installServer( server: LspServerDefinition, mode: InstallActionMode = "install", options: { promptConfirm?: boolean } = {}, ): Promise { const bundle = getServerBundle(server.id); if (bundle?.installServer) { return bundle.installServer(server.id, server, mode, options); } const { promptConfirm = true } = options; const cacheKey = getInstallCacheKey(server); const displayLabel = getInstallLabel(server); const isUpdate = mode === "update"; const actionLabel = isUpdate ? "Update" : "Install"; const command = mode === "install" ? getInstallCommand(server, "install") : getUpdateCommand(server); if (!command) { throw new Error( `${displayLabel} has no ${actionLabel.toLowerCase()} command.`, ); } if (promptConfirm) { const shouldContinue = await confirm( displayLabel, `${actionLabel} ${displayLabel} language server?`, ); if (!shouldContinue) { if (cacheKey) { checkedCommands.set(cacheKey, STATUS_DECLINED); } return false; } } let loadingDialog: LoaderDialog | null = null; try { loadingDialog = loader.create( displayLabel, `${actionLabel}ing ${displayLabel}...`, ); loadingDialog.show(); await runForegroundCommand(command); resetInstallState(server.id); const result = await checkServerInstallation(server); if (cacheKey && result.status === "present") { checkedCommands.set(cacheKey, STATUS_PRESENT); } toast( result.status === "present" ? `${displayLabel} ${isUpdate ? "updated" : "installed"}` : `${displayLabel} ${actionLabel.toLowerCase()} finished`, ); return true; } catch (error) { console.error(`Failed to ${actionLabel.toLowerCase()} ${server.id}`, error); if (cacheKey) { checkedCommands.set(cacheKey, STATUS_FAILED); } toast(strings?.error ?? "Error"); throw error; } finally { loadingDialog?.destroy?.(); } } export async function uninstallServer( server: LspServerDefinition, options: { promptConfirm?: boolean } = {}, ): Promise { const bundle = getServerBundle(server.id); if (bundle?.uninstallServer) { return bundle.uninstallServer(server.id, server, options); } const { promptConfirm = true } = options; const cacheKey = getInstallCacheKey(server); const displayLabel = getInstallLabel(server); const command = getUninstallCommand(server); if (!command) { throw new Error(`${displayLabel} has no uninstall command.`); } if (promptConfirm) { const shouldContinue = await confirm( displayLabel, `Uninstall ${displayLabel} language server?`, ); if (!shouldContinue) { return false; } } let loadingDialog: LoaderDialog | null = null; try { loadingDialog = loader.create( displayLabel, `Uninstalling ${displayLabel}...`, ); loadingDialog.show(); await runForegroundCommand(command); if (cacheKey) { checkedCommands.delete(cacheKey); } resetInstallState(server.id); stopManagedServer(server.id); return true; } catch (error) { console.error(`Failed to uninstall ${server.id}`, error); toast(strings?.error ?? "Error"); throw error; } finally { loadingDialog?.destroy(); } } async function performInstallCheck( server: LspServerDefinition, launcher: LauncherConfig | undefined, cacheKey: string, ): Promise { try { const checkCommand = launcher?.checkCommand || buildDerivedCheckCommand(server); if (checkCommand) { await runQuickCommand(checkCommand); } checkedCommands.set(cacheKey, STATUS_PRESENT); return true; } catch (error) { if (!getInstallCommand(server, "install")) { checkedCommands.set(cacheKey, STATUS_FAILED); console.warn( `LSP server ${server.id} is missing check command result and has no installer.`, error, ); throw error; } const installed = await installServer(server, "install", { promptConfirm: true, }); if (!installed) { checkedCommands.set(cacheKey, STATUS_DECLINED); return false; } checkedCommands.set(cacheKey, STATUS_PRESENT); return true; } } async function startInteractiveServer( command: string, serverId: string, ): Promise { const executor = getExecutor(); const callback: ExecutorCallback = (type, data) => { if (type === "stderr") { if (/proot warning/i.test(data)) return; console.warn(`[LSP:${serverId}] ${data}`); } else if (type === "stdout" && data && data.trim()) { console.info(`[LSP:${serverId}] ${data}`); // Detect when the axs proxy signals it's listening if (/listening on/i.test(data)) { signalServerReady(serverId); } } }; const uuid = await executor.start(command, callback, true); managedServers.set(serverId, { uuid, command, startedAt: Date.now(), }); return uuid; } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Tracks servers that have signaled they're ready (listening) * Key: serverId, Value: timestamp when ready */ const serverReadySignals = new Map(); /** * Called when stdout contains a "listening" message from the axs proxy. * This signals that the server is ready to accept connections. */ export function signalServerReady(serverId: string): void { serverReadySignals.set(serverId, Date.now()); } /** * Wait for the LSP server to be ready. * * This function polls for a ready signal (set when stdout contains "listening") */ async function waitForWebSocket( url: string, options: WaitOptions = {}, ): Promise { const { delay = 100, // Poll interval probeTimeout = 5000, // Max wait time } = options; // Extract server ID from URL (e.g., "ws://127.0.0.1:2090" -> check by port) const portMatch = url.match(/:(\d+)/); const port = portMatch ? portMatch[1] : null; // Find the server ID that's starting on this port let targetServerId: string | null = null; const entries = Array.from(managedServers.entries()); for (const [serverId, entry] of entries) { if ( entry.command.includes(`--port ${port}`) || entry.command.includes(`:${port}`) ) { targetServerId = serverId; break; } } const deadline = Date.now() + probeTimeout; while (Date.now() < deadline) { // Check if we got a ready signal if (targetServerId && serverReadySignals.has(targetServerId)) { // Server is ready, clear the signal and return serverReadySignals.delete(targetServerId); return; } await sleep(delay); } // Timeout reached, proceed anyway (transport will retry if needed) console.debug( `[LSP] waitForWebSocket timed out for ${url}, proceeding anyway`, ); } export interface EnsureServerResult { uuid: string | null; /** Port discovered from port file (for auto-port discovery) */ discoveredPort?: number; } export async function ensureServerRunning( server: LspServerDefinition, session?: string, ): Promise { const launcher = server.launcher; if (!launcher) return { uuid: null }; // Derive session from server ID if not provided const effectiveSession = session || server.id; // Check if server is already running via port file (dead client detection) const bridge = launcher.bridge; const serverName = resolveServerExecutable(server) || bridge?.command || launcher.command || server.id; try { const existingPort = await canReuseExistingServer(server, effectiveSession); if (existingPort !== null) { // Server is already running and responsive, no need to start return { uuid: null, discoveredPort: existingPort }; } } catch { // Failed to check, proceed with normal startup } const terminal = ( globalThis as unknown as { Terminal?: { isInstalled?: () => Promise | boolean }; } ).Terminal; let isTerminalInstalled = false; try { isTerminalInstalled = Boolean(await terminal?.isInstalled?.()); } catch {} if (!isTerminalInstalled) { const message = getTerminalRequiredMessage(); alert(strings?.error, message); const unavailable: LspError = new Error(message); unavailable.code = "LSP_SERVER_UNAVAILABLE"; throw unavailable; } const installed = await ensureInstalled(server); if (!installed) { const unavailable: LspError = new Error( `Language server ${server.id} is not available.`, ); unavailable.code = "LSP_SERVER_UNAVAILABLE"; throw unavailable; } const key = server.id; if (managedServers.has(key)) { const existing = managedServers.get(key); return { uuid: existing?.uuid ?? null }; } const command = resolveStartCommand(server, effectiveSession); if (!command) { return { uuid: null }; } try { const uuid = await startInteractiveServer(command, key); // For auto-port discovery, wait for server ready signal then read port let discoveredPort: number | undefined; if (bridge && !bridge.port) { // Auto-port mode - wait for server ready signal and then read port file const portInfo = await waitForPort( key, serverName, effectiveSession, 10000, ); if (portInfo) { discoveredPort = portInfo.port; console.info( `[LSP:${server.id}] Auto-discovered port ${discoveredPort}`, ); // Update managed server entry with the port const entry = managedServers.get(key); if (entry) { entry.port = discoveredPort; } } } else if ( server.transport?.url && (server.transport.kind === "websocket" || server.transport.kind === "stdio") ) { // Fixed port mode - wait for the server to signal ready await waitForWebSocket(server.transport.url); } if (!announcedServers.has(key)) { console.info(`[LSP:${server.id}] ${server.label} connected`); announcedServers.add(key); } return { uuid, discoveredPort }; } catch (error) { console.error(`Failed to start language server ${server.id}`, error); const errorMessage = error instanceof Error ? error.message : String(error); lspStatusBar.show({ message: errorMessage || "Connection failed", title: `${server.label} failed`, type: "error", icon: "error", duration: false, }); const entry = managedServers.get(key); if (entry) { getExecutor() .stop(entry.uuid) .catch((err: Error) => { console.warn( `Failed to stop language server shell ${server.id}`, err, ); }); managedServers.delete(key); } const unavailable: LspError = new Error( `Language server ${server.id} failed to start (${errorMessage})`, ); unavailable.code = "LSP_SERVER_UNAVAILABLE"; throw unavailable; } } export function stopManagedServer(serverId: string): void { const entry = managedServers.get(serverId); if (!entry) return; const executor = getExecutor(); executor.stop(entry.uuid).catch((error: Error) => { console.warn(`Failed to stop language server ${serverId}`, error); }); managedServers.delete(serverId); announcedServers.delete(serverId); // Stop foreground service when all servers are stopped if (managedServers.size === 0) { executor.stopService().catch(() => {}); } } export function resetManagedServers(): void { for (const id of Array.from(managedServers.keys())) { stopManagedServer(id); } managedServers.clear(); // Ensure foreground service is stopped getExecutor() .stopService() .catch(() => {}); } /** * Get managed server info by server ID */ export function getManagedServerInfo( serverId: string, ): ManagedServerEntry | null { return managedServers.get(serverId) ?? null; } /** * Get all managed servers */ export function getAllManagedServers(): Map { return new Map(managedServers); } function formatMemory(bytes: number): string { if (!bytes || bytes <= 0) return "—"; const mb = bytes / (1024 * 1024); if (mb >= 1) return `${mb.toFixed(1)} MB`; const kb = bytes / 1024; return `${kb.toFixed(0)} KB`; } function formatUptime(seconds: number): string { if (!seconds || seconds <= 0) return "—"; if (seconds < 60) return `${seconds}s`; const mins = Math.floor(seconds / 60); const secs = seconds % 60; if (mins < 60) return `${mins}m ${secs}s`; const hours = Math.floor(mins / 60); const remainingMins = mins % 60; return `${hours}h ${remainingMins}m`; } /** * Fetch server stats from the axs proxy /status endpoint * @param serverId - The server ID to fetch stats for * @param timeout - Timeout in milliseconds (default: 2000) */ export async function getServerStats( serverId: string, timeout = 2000, ): Promise { const entry = managedServers.get(serverId); if (!entry?.port) { return null; } try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await fetch(`http://127.0.0.1:${entry.port}/status`, { signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { return null; } const data = (await response.json()) as LspServerStats; // Aggregate stats from all processes let totalMemory = 0; let maxUptime = 0; let firstPid: number | null = null; for (const proc of data.processes || []) { totalMemory += proc.memory_bytes || 0; if (proc.uptime_secs > maxUptime) { maxUptime = proc.uptime_secs; } if (firstPid === null && proc.pid) { firstPid = proc.pid; } } return { memoryBytes: totalMemory, memoryFormatted: formatMemory(totalMemory), uptimeSeconds: maxUptime, uptimeFormatted: formatUptime(maxUptime), pid: firstPid, processCount: data.processes?.length ?? 0, }; } catch { return null; } } ================================================ FILE: src/cm/lsp/serverRegistry.ts ================================================ import { bindServerRegistry, ensureBuiltinBundlesRegistered, } from "./serverCatalog"; import type { AcodeClientConfig, BridgeConfig, LanguageResolverContext, LauncherConfig, LspServerDefinition, LspServerManifest, RegistryEventListener, RegistryEventType, RootUriContext, TransportDescriptor, WebSocketTransportOptions, } from "./types"; const registry = new Map(); const listeners = new Set(); function toKey(id: string | undefined | null): string { return String(id ?? "") .trim() .toLowerCase(); } function clone(value: T): T | undefined { if (!value || typeof value !== "object") return undefined; try { return JSON.parse(JSON.stringify(value)) as T; } catch (_) { return value; } } function sanitizeLanguages(languages: string[] = []): string[] { if (!Array.isArray(languages)) return []; return languages .map((lang) => String(lang ?? "") .trim() .toLowerCase(), ) .filter(Boolean); } function parsePort(value: unknown): number | null { const num = Number(value); if (!Number.isFinite(num)) return null; const int = Math.floor(num); if (int !== num || int <= 0 || int > 65535) return null; return int; } interface RawBridgeConfig { kind?: string; port?: unknown; command?: string; args?: unknown[]; session?: string; } function sanitizeInstallKind( value: unknown, ): | "apk" | "npm" | "pip" | "cargo" | "github-release" | "manual" | "shell" | undefined { switch (value) { case "apk": case "npm": case "pip": case "cargo": case "github-release": case "manual": case "shell": return value; default: return undefined; } } function sanitizeBridge( serverId: string, bridge: RawBridgeConfig | undefined | null, ): BridgeConfig | undefined { if (!bridge || typeof bridge !== "object") return undefined; const kind = bridge.kind ?? "axs"; if (kind !== "axs") { throw new Error( `LSP server ${serverId} declares unsupported bridge kind ${kind}`, ); } // Port is now optional - if not provided, auto-port discovery will be used const port = bridge.port ? (parsePort(bridge.port) ?? undefined) : undefined; const command = bridge.command ? String(bridge.command) : null; if (!command) { throw new Error(`LSP server ${serverId} bridge must supply a command`); } const args = Array.isArray(bridge.args) ? bridge.args.map((arg) => String(arg)) : undefined; return { kind: "axs", port, command, args, session: bridge.session ? String(bridge.session) : undefined, }; } interface RawTransportDescriptor { kind?: string; command?: string; args?: unknown[]; options?: Record | WebSocketTransportOptions; url?: string; } interface RawLauncherConfig { command?: string; args?: unknown[]; startCommand?: string | string[]; checkCommand?: string; versionCommand?: string; updateCommand?: string; install?: { kind?: string; command?: string; updateCommand?: string; uninstallCommand?: string; label?: string; source?: string; executable?: string; packages?: unknown[]; pipCommand?: string; npmCommand?: string; pythonCommand?: string; global?: boolean; breakSystemPackages?: boolean; repo?: string; assetNames?: Record; archiveType?: string; extractFile?: string; binaryPath?: string; }; bridge?: RawBridgeConfig; } export type RawServerDefinition = LspServerManifest; function sanitizeDefinition( definition: RawServerDefinition, ): LspServerDefinition { if (!definition || typeof definition !== "object") { throw new TypeError("LSP server definition must be an object"); } const id = toKey(definition.id); if (!id) throw new Error("LSP server definition requires a non-empty id"); const transport: RawTransportDescriptor = definition.transport ?? {}; const kind = (transport.kind ?? "stdio") as | "stdio" | "websocket" | "external"; if (!transport || typeof transport !== "object") { throw new Error(`LSP server ${id} is missing a transport descriptor`); } if ( !("languages" in definition) || !sanitizeLanguages(definition.languages).length ) { throw new Error(`LSP server ${id} must declare supported languages`); } if (kind === "stdio" && !transport.command) { throw new Error(`LSP server ${id} (stdio) requires a command`); } // Websocket transport requires a URL unless a bridge is configured for auto-port discovery const hasBridge = definition.launcher?.bridge?.command; if (kind === "websocket" && !transport.url && !hasBridge) { throw new Error( `LSP server ${id} (websocket) requires a url or a launcher bridge`, ); } const transportOptions: Record = transport.options && typeof transport.options === "object" ? { ...transport.options } : {}; const sanitizedTransport: TransportDescriptor = { kind, command: transport.command, args: Array.isArray(transport.args) ? transport.args.map((arg) => String(arg)) : undefined, options: transportOptions, url: transport.url, protocols: undefined, }; let launcher: LauncherConfig | undefined; if (definition.launcher && typeof definition.launcher === "object") { const rawLauncher = definition.launcher; const installExecutable = typeof rawLauncher.install?.executable === "string" ? rawLauncher.install.executable.trim() : ""; launcher = { command: rawLauncher.command, args: Array.isArray(rawLauncher.args) ? rawLauncher.args.map((arg) => String(arg)) : undefined, startCommand: Array.isArray(rawLauncher.startCommand) ? rawLauncher.startCommand.map((arg) => String(arg)) : rawLauncher.startCommand, checkCommand: rawLauncher.checkCommand, versionCommand: rawLauncher.versionCommand, updateCommand: rawLauncher.updateCommand, uninstallCommand: rawLauncher.uninstallCommand, install: rawLauncher.install && typeof rawLauncher.install === "object" ? { kind: sanitizeInstallKind(rawLauncher.install.kind), command: rawLauncher.install.command ?? "", updateCommand: rawLauncher.install.updateCommand, uninstallCommand: rawLauncher.install.uninstallCommand, label: rawLauncher.install.label, source: rawLauncher.install.source, executable: installExecutable || undefined, packages: Array.isArray(rawLauncher.install.packages) ? rawLauncher.install.packages.map((entry) => String(entry)) : undefined, pipCommand: rawLauncher.install.pipCommand, npmCommand: rawLauncher.install.npmCommand, pythonCommand: rawLauncher.install.pythonCommand, global: rawLauncher.install.global, breakSystemPackages: rawLauncher.install.breakSystemPackages, repo: rawLauncher.install.repo, assetNames: rawLauncher.install.assetNames && typeof rawLauncher.install.assetNames === "object" ? Object.fromEntries( Object.entries(rawLauncher.install.assetNames).map( ([key, value]) => [String(key), String(value)], ), ) : undefined, archiveType: rawLauncher.install.archiveType === "binary" ? "binary" : "zip", extractFile: rawLauncher.install.extractFile, binaryPath: rawLauncher.install.binaryPath, } : undefined, bridge: sanitizeBridge(id, rawLauncher.bridge), }; const installKind = launcher.install?.kind; const isManagedInstall = installKind && installKind !== "shell"; if (isManagedInstall) { const providedExecutable = launcher.install?.binaryPath || launcher.install?.executable; if (!providedExecutable) { throw new Error( `LSP server ${id} managed installers must declare install.binaryPath or install.executable`, ); } } } const sanitized: LspServerDefinition = { id, label: definition.label ?? id, enabled: definition.enabled !== false, languages: sanitizeLanguages(definition.languages), transport: sanitizedTransport, initializationOptions: clone(definition.initializationOptions), clientConfig: clone(definition.clientConfig), startupTimeout: typeof definition.startupTimeout === "number" ? definition.startupTimeout : undefined, capabilityOverrides: clone(definition.capabilityOverrides), rootUri: typeof definition.rootUri === "function" ? definition.rootUri : null, documentUri: typeof definition.documentUri === "function" ? definition.documentUri : null, resolveLanguageId: typeof definition.resolveLanguageId === "function" ? definition.resolveLanguageId : null, launcher, useWorkspaceFolders: definition.useWorkspaceFolders === true, }; if (!Object.keys(transportOptions).length) { sanitized.transport.options = undefined; } return sanitized; } function notify(event: RegistryEventType, payload: LspServerDefinition): void { listeners.forEach((fn) => { try { fn(event, payload); } catch (error) { console.error("LSP server registry listener failed", error); } }); } export interface RegisterServerOptions { replace?: boolean; } export function registerServer( definition: RawServerDefinition, options: RegisterServerOptions = {}, ): LspServerDefinition { const { replace = false } = options; const normalized = sanitizeDefinition(definition); const exists = registry.has(normalized.id); if (exists && !replace) { const existing = registry.get(normalized.id); if (existing) return existing; } registry.set(normalized.id, normalized); notify("register", normalized); return normalized; } export function unregisterServer(id: string): boolean { const key = toKey(id); if (!key || !registry.has(key)) return false; const existing = registry.get(key); registry.delete(key); if (existing) { notify("unregister", existing); } return true; } export type ServerUpdater = ( current: LspServerDefinition, ) => Partial | null; export function updateServer( id: string, updater: ServerUpdater, ): LspServerDefinition | null { const key = toKey(id); if (!key || !registry.has(key)) return null; const current = registry.get(key); if (!current) return null; const next = updater({ ...current }); if (!next) return current; const normalized = sanitizeDefinition({ ...current, ...next, id: current.id, }); registry.set(key, normalized); notify("update", normalized); return normalized; } export function getServer(id: string): LspServerDefinition | null { return registry.get(toKey(id)) ?? null; } export function listServers(): LspServerDefinition[] { return Array.from(registry.values()); } export interface GetServersOptions { includeDisabled?: boolean; } export function getServersForLanguage( languageId: string, options: GetServersOptions = {}, ): LspServerDefinition[] { const { includeDisabled = false } = options; const langKey = toKey(languageId); if (!langKey) return []; return listServers().filter((server) => { if (!includeDisabled && !server.enabled) return false; return server.languages.includes(langKey); }); } export function onRegistryChange(listener: RegistryEventListener): () => void { if (typeof listener !== "function") return () => {}; listeners.add(listener); return () => listeners.delete(listener); } bindServerRegistry({ registerServer, unregisterServer, }); ensureBuiltinBundlesRegistered(); export default { registerServer, unregisterServer, updateServer, getServer, getServersForLanguage, listServers, onRegistryChange, }; ================================================ FILE: src/cm/lsp/servers/index.ts ================================================ import type { LspServerBundle, LspServerManifest } from "../types"; import { javascriptBundle, javascriptServers } from "./javascript"; import { luauBundle, luauServers } from "./luau"; import { pythonBundle, pythonServers } from "./python"; import { systemsBundle, systemsServers } from "./systems"; import { webBundle, webServers } from "./web"; export const builtinServers: LspServerManifest[] = [ ...javascriptServers, ...pythonServers, ...luauServers, ...webServers, ...systemsServers, ]; export const builtinServerBundles: LspServerBundle[] = [ javascriptBundle, pythonBundle, luauBundle, webBundle, systemsBundle, ]; ================================================ FILE: src/cm/lsp/servers/javascript.ts ================================================ import { defineBundle, defineServer, installers } from "../providerUtils"; import type { LspServerBundle, LspServerManifest } from "../types"; import { resolveJsTsLanguageId } from "./shared"; export const javascriptServers: LspServerManifest[] = [ defineServer({ id: "typescript", label: "TypeScript / JavaScript", useWorkspaceFolders: true, languages: [ "javascript", "javascriptreact", "typescript", "typescriptreact", "tsx", "jsx", ], transport: { kind: "websocket", }, command: "typescript-language-server", args: ["--stdio"], checkCommand: "which typescript-language-server", installer: installers.npm({ executable: "typescript-language-server", packages: ["typescript-language-server", "typescript"], }), enabled: true, initializationOptions: { provideFormatter: true, hostInfo: "acode", tsserver: { maxTsServerMemory: 4096, useSeparateSyntaxServer: true, }, preferences: { includeInlayParameterNameHints: "all", includeInlayParameterNameHintsWhenArgumentMatchesName: true, includeInlayFunctionParameterTypeHints: true, includeInlayVariableTypeHints: true, includeInlayVariableTypeHintsWhenTypeMatchesName: false, includeInlayPropertyDeclarationTypeHints: true, includeInlayFunctionLikeReturnTypeHints: true, includeInlayEnumMemberValueHints: true, importModuleSpecifierPreference: "shortest", importModuleSpecifierEnding: "auto", includePackageJsonAutoImports: "auto", provideRefactorNotApplicableReason: true, allowIncompleteCompletions: true, allowRenameOfImportPath: true, generateReturnInDocTemplate: true, organizeImportsIgnoreCase: "auto", organizeImportsCollation: "ordinal", organizeImportsCollationConfig: "default", autoImportFileExcludePatterns: [], preferTypeOnlyAutoImports: false, }, completions: { completeFunctionCalls: true, }, diagnostics: { reportStyleChecksAsWarnings: true, }, }, resolveLanguageId: ({ languageId, languageName }) => resolveJsTsLanguageId(languageId, languageName), }), defineServer({ id: "vtsls", label: "TypeScript / JavaScript (vtsls)", useWorkspaceFolders: true, languages: [ "javascript", "javascriptreact", "typescript", "typescriptreact", "tsx", "jsx", ], transport: { kind: "websocket", }, command: "vtsls", args: ["--stdio"], checkCommand: "which vtsls", installer: installers.npm({ executable: "vtsls", packages: ["@vtsls/language-server"], }), enabled: false, initializationOptions: { hostInfo: "acode", typescript: { enablePromptUseWorkspaceTsdk: true, inlayHints: { parameterNames: { enabled: "all", suppressWhenArgumentMatchesName: false, }, parameterTypes: { enabled: true, }, variableTypes: { enabled: true, suppressWhenTypeMatchesName: false, }, propertyDeclarationTypes: { enabled: true, }, functionLikeReturnTypes: { enabled: true, }, enumMemberValues: { enabled: true, }, }, suggest: { completeFunctionCalls: true, includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true, includeAutomaticOptionalChainCompletions: true, includeCompletionsWithSnippetText: true, includeCompletionsWithClassMemberSnippets: true, includeCompletionsWithObjectLiteralMethodSnippets: true, autoImports: true, classMemberSnippets: { enabled: true, }, objectLiteralMethodSnippets: { enabled: true, }, }, preferences: { importModuleSpecifier: "shortest", importModuleSpecifierEnding: "auto", includePackageJsonAutoImports: "auto", preferTypeOnlyAutoImports: false, quoteStyle: "auto", jsxAttributeCompletionStyle: "auto", }, format: { enable: true, insertSpaceAfterCommaDelimiter: true, insertSpaceAfterSemicolonInForStatements: true, insertSpaceBeforeAndAfterBinaryOperators: true, insertSpaceAfterKeywordsInControlFlowStatements: true, insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, placeOpenBraceOnNewLineForFunctions: false, placeOpenBraceOnNewLineForControlBlocks: false, semicolons: "ignore", }, updateImportsOnFileMove: { enabled: "always", }, codeActionsOnSave: { organizeImports: false, addMissingImports: false, }, workspaceSymbols: { scope: "allOpenProjects", }, }, javascript: { inlayHints: { parameterNames: { enabled: "all", suppressWhenArgumentMatchesName: false, }, parameterTypes: { enabled: true, }, variableTypes: { enabled: true, suppressWhenTypeMatchesName: false, }, propertyDeclarationTypes: { enabled: true, }, functionLikeReturnTypes: { enabled: true, }, enumMemberValues: { enabled: true, }, }, suggest: { completeFunctionCalls: true, includeCompletionsForModuleExports: true, autoImports: true, classMemberSnippets: { enabled: true, }, }, preferences: { importModuleSpecifier: "shortest", quoteStyle: "auto", }, format: { enable: true, }, updateImportsOnFileMove: { enabled: "always", }, }, tsserver: { maxTsServerMemory: 8092, }, vtsls: { experimental: { completion: { enableServerSideFuzzyMatch: true, entriesLimit: 5000, }, }, autoUseWorkspaceTsdk: true, }, }, resolveLanguageId: ({ languageId, languageName }) => resolveJsTsLanguageId(languageId, languageName), }), defineServer({ id: "eslint", label: "ESLint", languages: [ "javascript", "javascriptreact", "typescript", "typescriptreact", "tsx", "jsx", "vue", "svelte", "html", "markdown", "json", "jsonc", ], transport: { kind: "websocket", }, command: "vscode-eslint-language-server", args: ["--stdio"], checkCommand: "which vscode-eslint-language-server", installer: installers.npm({ executable: "vscode-eslint-language-server", packages: ["vscode-langservers-extracted"], }), enabled: false, initializationOptions: { validate: "on", rulesCustomizations: [], run: "onType", nodePath: null, workingDirectory: { mode: "auto", }, problems: { shortenToSingleLine: false, }, codeActionOnSave: { enable: true, rules: [], mode: "all", }, codeAction: { disableRuleComment: { enable: true, location: "separateLine", commentStyle: "line", }, showDocumentation: { enable: true, }, }, experimental: { useFlatConfig: false, }, format: { enable: true, }, quiet: false, onIgnoredFiles: "off", useESLintClass: false, }, clientConfig: { builtinExtensions: { hover: false, completion: false, signature: false, keymaps: false, diagnostics: true, }, }, resolveLanguageId: ({ languageId, languageName }) => resolveJsTsLanguageId(languageId, languageName), }), ]; export const javascriptBundle: LspServerBundle = defineBundle({ id: "builtin-javascript", label: "JavaScript / TypeScript", servers: javascriptServers, }); ================================================ FILE: src/cm/lsp/servers/luau.ts ================================================ import toast from "components/toast"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import { buildShellArchCase } from "../installerUtils"; import { quoteArg, runForegroundCommand, runQuickCommand, } from "../installRuntime"; import { defineBundle, defineServer, installers } from "../providerUtils"; import type { InstallCheckResult, LspServerBundle, LspServerManifest, } from "../types"; function isGlibcRuntimeError(output: string): boolean { return ( output.includes("ld-linux-aarch64.so.1") || output.includes("ld-linux-x86-64.so.2") || output.includes("Error loading shared library") || output.includes("__fprintf_chk") || output.includes("__snprintf_chk") || output.includes("__vsnprintf_chk") || output.includes("__libc_single_threaded") || output.includes("GLIBC_") ); } function getLuauRuntimeFailureMessage(output: string): string { if (isGlibcRuntimeError(output)) { return "Luau release binary requires glibc and is not runnable in this Alpine/musl environment."; } const firstLine = String(output || "") .split("\n") .map((line) => line.trim()) .find(Boolean); return firstLine || "Luau binary is installed but not runnable."; } async function readLuauRuntimeFailure(binaryPath: string): Promise { const command = `${quoteArg(binaryPath)} --help >/dev/null 2>&1 || ${quoteArg(binaryPath)} lsp --help >/dev/null 2>&1`; try { await runQuickCommand(command); return ""; } catch (error) { const primaryMessage = error instanceof Error ? error.message : String(error); try { const lddOutput = await runQuickCommand( `command -v ldd >/dev/null 2>&1 && ldd ${quoteArg(binaryPath)} 2>&1 || true`, ); return [primaryMessage, lddOutput].filter(Boolean).join("\n"); } catch { return primaryMessage; } } } export const luauServers: LspServerManifest[] = [ defineServer({ id: "luau", label: "Luau", useWorkspaceFolders: true, languages: ["luau"], command: "/usr/local/bin/luau-lsp", args: ["lsp"], installer: installers.githubRelease({ repo: "JohnnyMorganz/luau-lsp", binaryPath: "/usr/local/bin/luau-lsp", assetNames: { aarch64: "luau-lsp-linux-arm64.zip", arm64: "luau-lsp-linux-arm64.zip", "arm64-v8a": "luau-lsp-linux-arm64.zip", x86_64: "luau-lsp-linux-x86_64.zip", amd64: "luau-lsp-linux-x86_64.zip", }, extractFile: "luau-lsp", }), enabled: false, }), ]; export const luauBundle: LspServerBundle = defineBundle({ id: "builtin-luau", label: "Luau", servers: luauServers, hooks: { getExecutable: (_, manifest) => manifest.launcher?.install?.binaryPath || manifest.launcher?.install?.executable || null, async checkInstallation(_, manifest): Promise { const binary = manifest.launcher?.install?.binaryPath || manifest.launcher?.install?.executable; if (!binary) { return { status: "failed", version: null, canInstall: true, canUpdate: true, message: "Luau bundle is missing a binary path", }; } try { await runQuickCommand(`test -x ${quoteArg(binary)}`); const runtimeFailure = await readLuauRuntimeFailure(binary); if (runtimeFailure) { return { status: "failed", version: null, canInstall: true, canUpdate: true, message: getLuauRuntimeFailureMessage(runtimeFailure), }; } return { status: "present", version: null, canInstall: true, canUpdate: true, }; } catch (error) { return { status: "missing", version: null, canInstall: true, canUpdate: true, message: error instanceof Error ? error.message : String(error), }; } }, async installServer(_, manifest, mode, options = {}): Promise { const { promptConfirm = true } = options; const install = manifest.launcher?.install; const assetCases = buildShellArchCase(install?.assetNames, quoteArg); const binaryPath = install?.binaryPath; const repo = install?.repo; if (!assetCases || !binaryPath || !repo) { throw new Error("Luau bundle is missing release metadata"); } const label = manifest.label || "Luau"; const actionLabel = mode === "update" ? "Update" : "Install"; if (promptConfirm) { const shouldContinue = await confirm( label, `${actionLabel} ${label} language server?`, ); if (!shouldContinue) { return false; } } const downloadUrl = `https://github.com/${repo}/releases/latest/download/$ASSET`; const command = `apk add --no-cache curl unzip && ARCH="$(uname -m)" && case "$ARCH" in ${assetCases} \t*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; esac && apk add --no-cache gcompat libstdc++ && TMP_DIR="$(mktemp -d)" && cleanup() { rm -rf "$TMP_DIR"; } && trap cleanup EXIT && curl -fsSL "${downloadUrl}" -o "$TMP_DIR/$ASSET" && unzip -oq "$TMP_DIR/$ASSET" -d "$TMP_DIR" && chmod +x "$TMP_DIR/luau-lsp" && if ! "$TMP_DIR/luau-lsp" --help >/dev/null 2>&1 && ! "$TMP_DIR/luau-lsp" lsp --help >/dev/null 2>&1; then command -v ldd >/dev/null 2>&1 && ldd "$TMP_DIR/luau-lsp" >&2 || true; echo "Luau release binary is not runnable in this environment." >&2; exit 1; fi && install -Dm755 "$TMP_DIR/luau-lsp" ${quoteArg(binaryPath)}`; const loadingDialog = loader.create( label, `${actionLabel}ing ${label}...`, ); try { loadingDialog.show(); await runForegroundCommand(command); const runtimeFailure = await readLuauRuntimeFailure(binaryPath); if (runtimeFailure) { await runQuickCommand(`rm -f ${quoteArg(binaryPath)}`); throw new Error(getLuauRuntimeFailureMessage(runtimeFailure)); } toast(`${label} ${mode === "update" ? "updated" : "installed"}`); return true; } catch (error) { console.error(`Failed to ${actionLabel.toLowerCase()} ${label}`, error); toast(strings?.error ?? "Error"); throw error; } finally { loadingDialog.destroy(); } }, }, }); ================================================ FILE: src/cm/lsp/servers/python.ts ================================================ import { defineBundle, defineServer, installers } from "../providerUtils"; import type { LspServerBundle, LspServerManifest } from "../types"; export const pythonServers: LspServerManifest[] = [ defineServer({ id: "ty", label: "Python (ty)", languages: ["python"], command: "ty", args: ["server"], checkCommand: "which ty", installer: installers.pip({ executable: "ty", packages: ["ty"], }), enabled: true, }), defineServer({ id: "python", label: "Python (pylsp)", languages: ["python"], command: "pylsp", checkCommand: "which pylsp", installer: installers.pip({ executable: "pylsp", packages: ["python-lsp-server[all]"], }), initializationOptions: { pylsp: { plugins: { pyflakes: { enabled: true }, pycodestyle: { enabled: true }, mccabe: { enabled: true }, }, }, }, enabled: false, }), ]; export const pythonBundle: LspServerBundle = defineBundle({ id: "builtin-python", label: "Python", servers: pythonServers, }); ================================================ FILE: src/cm/lsp/servers/shared.ts ================================================ export function normalizeServerLanguageKey( value: string | undefined | null, ): string { return String(value ?? "") .trim() .toLowerCase(); } export function resolveJsTsLanguageId( languageId: string | undefined, languageName: string | undefined, ): string | null { const lang = normalizeServerLanguageKey(languageId ?? languageName); switch (lang) { case "tsx": case "typescriptreact": return "typescriptreact"; case "jsx": case "javascriptreact": return "javascriptreact"; case "ts": return "typescript"; case "js": return "javascript"; default: return lang || null; } } ================================================ FILE: src/cm/lsp/servers/systems.ts ================================================ import { defineBundle, defineServer, installers } from "../providerUtils"; import type { LspServerBundle, LspServerManifest } from "../types"; export const systemsServers: LspServerManifest[] = [ defineServer({ id: "clangd", label: "C / C++ (clangd)", languages: ["c", "cpp"], command: "clangd", args: [ "--background-index=0", "--clang-tidy=0", "--header-insertion=never", ], checkCommand: "which clangd", installer: installers.apk({ executable: "clangd", packages: ["clang-extra-tools"], }), enabled: false, }), defineServer({ id: "gopls", label: "Go (gopls)", languages: ["go", "go.mod", "go.sum", "gotmpl"], command: "gopls", args: ["serve"], checkCommand: "which gopls", installer: installers.apk({ executable: "gopls", packages: ["go", "gopls"], }), initializationOptions: { usePlaceholders: false, completeUnimported: true, deepCompletion: true, completionBudget: "100ms", matcher: "Fuzzy", staticcheck: true, gofumpt: true, hints: { assignVariableTypes: true, compositeLiteralFields: true, compositeLiteralTypes: true, constantValues: true, functionTypeParameters: true, parameterNames: true, rangeVariableTypes: true, }, diagnosticsDelay: "250ms", diagnosticsTrigger: "Edit", annotations: { bounds: true, escape: true, inline: true, nil: true, }, semanticTokens: true, analyses: { nilness: true, unusedparams: true, unusedvariable: true, unusedwrite: true, shadow: true, fieldalignment: false, stringintconv: true, }, importShortcut: "Both", symbolMatcher: "FastFuzzy", symbolStyle: "Dynamic", symbolScope: "all", local: "", linksInHover: true, hoverKind: "FullDocumentation", verboseOutput: false, }, enabled: true, }), defineServer({ id: "rust-analyzer", label: "Rust (rust-analyzer)", useWorkspaceFolders: true, languages: ["rust"], command: "rust-analyzer", checkCommand: "which rust-analyzer", installer: installers.apk({ executable: "rust-analyzer", packages: ["rust", "cargo", "rust-analyzer"], }), initializationOptions: { cargo: { allFeatures: true, buildScripts: { enable: true, }, loadOutDirsFromCheck: true, }, procMacro: { enable: true, attributes: { enable: true, }, }, checkOnSave: { enable: true, command: "clippy", extraArgs: ["--no-deps"], }, diagnostics: { enable: true, experimental: { enable: true, }, }, inlayHints: { bindingModeHints: { enable: false, }, chainingHints: { enable: true, }, closingBraceHints: { enable: true, minLines: 25, }, closureReturnTypeHints: { enable: "with_block", }, lifetimeElisionHints: { enable: "skip_trivial", useParameterNames: true, }, maxLength: 25, parameterHints: { enable: true, }, reborrowHints: { enable: "mutable", }, typeHints: { enable: true, hideClosureInitialization: false, hideNamedConstructor: false, }, }, lens: { enable: true, debug: { enable: true, }, implementations: { enable: true, }, references: { adt: { enable: false }, enumVariant: { enable: false }, method: { enable: false }, trait: { enable: false }, }, run: { enable: true, }, }, completion: { autoimport: { enable: true, }, autoself: { enable: true, }, callable: { snippets: "fill_arguments", }, postfix: { enable: true, }, privateEditable: { enable: false, }, }, semanticHighlighting: { doc: { comment: { inject: { enable: true, }, }, }, operator: { enable: true, specialization: { enable: true, }, }, punctuation: { enable: false, separate: { macro: { bang: true, }, }, specialization: { enable: true, }, }, strings: { enable: true, }, }, hover: { actions: { debug: { enable: true, }, enable: true, gotoTypeDef: { enable: true, }, implementations: { enable: true, }, references: { enable: true, }, run: { enable: true, }, }, documentation: { enable: true, }, links: { enable: true, }, }, workspace: { symbol: { search: { kind: "all_symbols", scope: "workspace", }, }, }, rustfmt: { extraArgs: [], overrideCommand: null, rangeFormatting: { enable: false, }, }, }, enabled: true, }), ]; export const systemsBundle: LspServerBundle = defineBundle({ id: "builtin-systems", label: "Systems", servers: systemsServers, }); ================================================ FILE: src/cm/lsp/servers/web.ts ================================================ import { defineBundle, defineServer, installers } from "../providerUtils"; import type { LspServerBundle, LspServerManifest } from "../types"; export const webServers: LspServerManifest[] = [ defineServer({ id: "html", label: "HTML", languages: ["html", "vue", "svelte"], command: "vscode-html-language-server", args: ["--stdio"], checkCommand: "which vscode-html-language-server", installer: installers.npm({ executable: "vscode-html-language-server", packages: ["vscode-langservers-extracted"], }), clientConfig: { builtinExtensions: { keymaps: false, }, }, enabled: true, }), defineServer({ id: "css", label: "CSS", languages: ["css", "scss", "less"], command: "vscode-css-language-server", args: ["--stdio"], checkCommand: "which vscode-css-language-server", installer: installers.npm({ executable: "vscode-css-language-server", packages: ["vscode-langservers-extracted"], }), clientConfig: { builtinExtensions: { keymaps: false, }, }, enabled: true, }), defineServer({ id: "json", label: "JSON", languages: ["json", "jsonc"], command: "vscode-json-language-server", args: ["--stdio"], checkCommand: "which vscode-json-language-server", installer: installers.npm({ executable: "vscode-json-language-server", packages: ["vscode-langservers-extracted"], }), clientConfig: { builtinExtensions: { keymaps: false, }, }, enabled: true, }), ]; export const webBundle: LspServerBundle = defineBundle({ id: "builtin-web", label: "Web", servers: webServers, }); ================================================ FILE: src/cm/lsp/tooltipExtensions.ts ================================================ import { highlightingFor, type Language, language as languageFacet, } from "@codemirror/language"; import { LSPPlugin } from "@codemirror/lsp-client"; import { type Extension, Prec, StateEffect, StateField, } from "@codemirror/state"; import { type Command, closeHoverTooltips, EditorView, hasHoverTooltips, hoverTooltip, type KeyBinding, keymap, showTooltip, type Tooltip, ViewPlugin, type ViewUpdate, } from "@codemirror/view"; import { highlightCode } from "@lezer/highlight"; import type { HoverParams, SignatureHelpContext, SignatureHelpParams, } from "vscode-languageserver-protocol"; import type { Hover, SignatureHelp as LspSignatureHelp, MarkedString, MarkupContent, } from "vscode-languageserver-types"; interface LspClientInternals { config?: { highlightLanguage?: (language: string) => Language | null | undefined; }; hasCapability?: (name: string) => boolean; } const SIGNATURE_TRIGGER_DELAY = 120; const SIGNATURE_RETRIGGER_DELAY = 250; function fromPosition( doc: EditorView["state"]["doc"], position: { line: number; character: number }, ): number { const line = doc.line(position.line + 1); return Math.min(line.to, line.from + position.character); } function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (match) => { switch (match) { case "&": return "&"; case "<": return "<"; case ">": return ">"; case '"': return """; default: return "'"; } }); } function renderCode(plugin: LSPPlugin, code: MarkedString): string { const client = plugin.client as typeof plugin.client & LspClientInternals; if (typeof code === "string") { return plugin.docToHTML(code, "markdown"); } const { language, value } = code; let lang = client.config?.highlightLanguage?.(language || "") ?? undefined; if (!lang) { const viewLang = plugin.view.state.facet(languageFacet); if (viewLang && (!language || viewLang.name === language)) { lang = viewLang; } } if (!lang) return escapeHtml(value); let result = ""; highlightCode( value, lang.parser.parse(value), { style: (tags) => highlightingFor(plugin.view.state, tags) }, (text, cls) => { result += cls ? `${escapeHtml(text)}` : escapeHtml(text); }, () => { result += "
"; }, ); return result; } function renderTooltipContent( plugin: LSPPlugin, value: string | MarkupContent | MarkedString | MarkedString[], ): string { if (Array.isArray(value)) { return value.map((item) => renderCode(plugin, item)).join("
"); } if ( typeof value === "string" || (typeof value === "object" && value != null && "language" in value) ) { return renderCode(plugin, value); } return plugin.docToHTML(value); } function isPointerOrTouchSelection(update: ViewUpdate): boolean { return ( update.selectionSet && update.transactions.some( (tr) => tr.isUserEvent("pointer") || tr.isUserEvent("select.pointer") || tr.isUserEvent("touch") || tr.isUserEvent("select.touch"), ) ); } function closeHoverIfNeeded(view: EditorView): void { if (hasHoverTooltips(view.state)) { view.dispatch({ effects: closeHoverTooltips }); } } function hoverRequest(plugin: LSPPlugin, pos: number) { const client = plugin.client as typeof plugin.client & LspClientInternals; if (client.hasCapability?.("hoverProvider") === false) { return Promise.resolve(null); } plugin.client.sync(); return plugin.client.request( "textDocument/hover", { position: plugin.toPosition(pos), textDocument: { uri: plugin.uri }, }, ); } function lspTooltipSource( view: EditorView, pos: number, ): Promise { const plugin = LSPPlugin.get(view); if (!plugin) return Promise.resolve(null); return hoverRequest(plugin, pos).then((result) => { if (!result) return null; return { pos: result.range ? fromPosition(view.state.doc, result.range.start) : pos, end: result.range ? fromPosition(view.state.doc, result.range.end) : pos, create() { const dom = document.createElement("div"); dom.className = "cm-lsp-hover-tooltip cm-lsp-documentation"; dom.innerHTML = renderTooltipContent(plugin, result.contents); return { dom }; }, above: true, }; }); } const closeHoverOnInteraction = ViewPlugin.fromClass( class { constructor(readonly view: EditorView) {} }, { eventObservers: { pointerdown() { closeHoverIfNeeded(this.view); }, touchstart() { closeHoverIfNeeded(this.view); }, wheel() { closeHoverIfNeeded(this.view); }, scroll() { closeHoverIfNeeded(this.view); }, }, }, ); function getSignatureHelp( plugin: LSPPlugin, pos: number, context: SignatureHelpContext, ) { const client = plugin.client as typeof plugin.client & LspClientInternals; if (client.hasCapability?.("signatureHelpProvider") === false) { return Promise.resolve(null); } plugin.client.sync(); return plugin.client.request( "textDocument/signatureHelp", { context, position: plugin.toPosition(pos), textDocument: { uri: plugin.uri }, }, ); } function sameSignatures(a: LspSignatureHelp, b: LspSignatureHelp): boolean { if (a.signatures.length !== b.signatures.length) return false; return a.signatures.every((signature, index) => { return signature.label === b.signatures[index]?.label; }); } function sameActiveParam( a: LspSignatureHelp, b: LspSignatureHelp, active: number, ): boolean { const current = a.signatures[active]; const next = b.signatures[active]; if (!current || !next) return false; return ( (current.activeParameter ?? a.activeParameter) === (next.activeParameter ?? b.activeParameter) ); } class SignatureState { constructor( readonly data: LspSignatureHelp, readonly active: number, readonly tooltip: Tooltip, ) {} } const signatureEffect = StateEffect.define<{ data: LspSignatureHelp; active: number; pos: number; } | null>(); function signatureTooltip( data: LspSignatureHelp, active: number, pos: number, ): Tooltip { return { pos, above: true, create: (view) => drawSignatureTooltip(view, data, active), }; } const signatureState = StateField.define({ create() { return null; }, update(value, tr) { for (const effect of tr.effects) { if (effect.is(signatureEffect)) { if (effect.value) { return new SignatureState( effect.value.data, effect.value.active, signatureTooltip( effect.value.data, effect.value.active, effect.value.pos, ), ); } return null; } } if (value && tr.docChanged) { return new SignatureState(value.data, value.active, { ...value.tooltip, pos: tr.changes.mapPos(value.tooltip.pos), }); } return value; }, provide: (field) => showTooltip.from(field, (value) => value?.tooltip ?? null), }); function drawSignatureTooltip( view: EditorView, data: LspSignatureHelp, active: number, ) { const dom = document.createElement("div"); dom.className = "cm-lsp-signature-tooltip"; if (data.signatures.length > 1) { dom.classList.add("cm-lsp-signature-multiple"); const num = dom.appendChild(document.createElement("div")); num.className = "cm-lsp-signature-num"; num.textContent = `${active + 1}/${data.signatures.length}`; } const signature = data.signatures[active]; if (!signature) { return { dom }; } const sig = dom.appendChild(document.createElement("div")); sig.className = "cm-lsp-signature"; let activeFrom = 0; let activeTo = 0; const activeParamIndex = signature.activeParameter ?? data.activeParameter; const activeParam = activeParamIndex != null && signature.parameters ? signature.parameters[activeParamIndex] : null; if (activeParam && Array.isArray(activeParam.label)) { [activeFrom, activeTo] = activeParam.label; } else if (activeParam) { const found = signature.label.indexOf(activeParam.label as string); if (found > -1) { activeFrom = found; activeTo = found + activeParam.label.length; } } if (activeTo) { sig.appendChild( document.createTextNode(signature.label.slice(0, activeFrom)), ); const activeElt = sig.appendChild(document.createElement("span")); activeElt.className = "cm-lsp-active-parameter"; activeElt.textContent = signature.label.slice(activeFrom, activeTo); sig.appendChild(document.createTextNode(signature.label.slice(activeTo))); } else { sig.textContent = signature.label; } if (signature.documentation) { const plugin = LSPPlugin.get(view); if (plugin) { const docs = dom.appendChild(document.createElement("div")); docs.className = "cm-lsp-signature-documentation cm-lsp-documentation"; docs.innerHTML = plugin.docToHTML(signature.documentation); } } return { dom }; } const signaturePlugin = ViewPlugin.fromClass( class { activeRequest: { pos: number; drop: boolean } | null = null; delayedRequest = 0; constructor(readonly view: EditorView) {} update(update: ViewUpdate) { const pointerOrTouchSelection = isPointerOrTouchSelection(update); if (this.activeRequest) { if (update.selectionSet) { this.activeRequest.drop = true; this.activeRequest = null; } else if (update.docChanged) { this.activeRequest.pos = update.changes.mapPos( this.activeRequest.pos, ); } } const plugin = LSPPlugin.get(update.view); if (!plugin) return; const sigState = update.view.state.field(signatureState); let triggerCharacter = ""; if ( update.docChanged && update.transactions.some((tr) => tr.isUserEvent("input.type")) ) { const serverConf = plugin.client.serverCapabilities?.signatureHelpProvider; const triggers = (serverConf?.triggerCharacters || []).concat( (sigState && serverConf?.retriggerCharacters) || [], ); if (triggers.length) { update.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => { const insertedText = inserted.toString(); if (!insertedText) return; for (const trigger of triggers) { if (insertedText.includes(trigger)) { triggerCharacter = trigger; } } }); } } if (triggerCharacter) { this.scheduleRequest( plugin, { triggerKind: 2, isRetrigger: !!sigState, triggerCharacter, activeSignatureHelp: sigState?.data, }, SIGNATURE_TRIGGER_DELAY, ); } else if (sigState && update.selectionSet && !pointerOrTouchSelection) { this.scheduleRequest( plugin, { triggerKind: 3, isRetrigger: true, activeSignatureHelp: sigState.data, }, SIGNATURE_RETRIGGER_DELAY, ); } } scheduleRequest( plugin: LSPPlugin, context: SignatureHelpContext, delay: number, ) { if (this.delayedRequest) { clearTimeout(this.delayedRequest); } this.delayedRequest = window.setTimeout(() => { this.delayedRequest = 0; this.startRequest(plugin, context); }, delay); } startRequest(plugin: LSPPlugin, context: SignatureHelpContext) { if (this.delayedRequest) { clearTimeout(this.delayedRequest); this.delayedRequest = 0; } const { view } = plugin; const pos = view.state.selection.main.head; if (this.activeRequest) this.activeRequest.drop = true; const request = (this.activeRequest = { pos, drop: false }); getSignatureHelp(plugin, pos, context).then( (result) => { if (request.drop) return; if (result && result.signatures.length) { const current = view.state.field(signatureState); const same = current && sameSignatures(current.data, result); const active = same && context.triggerKind === 3 ? current!.active : (result.activeSignature ?? 0); if (same && sameActiveParam(current!.data, result, active)) return; view.dispatch({ effects: signatureEffect.of({ data: result, active, pos: same ? current!.tooltip.pos : request.pos, }), }); } else if (view.state.field(signatureState, false)) { view.dispatch({ effects: signatureEffect.of(null) }); } }, context.triggerKind === 1 ? (error) => plugin.reportError("Signature request failed", error) : undefined, ); } close() { if (this.delayedRequest) { clearTimeout(this.delayedRequest); this.delayedRequest = 0; } if (this.activeRequest) { this.activeRequest.drop = true; this.activeRequest = null; } if (this.view.state.field(signatureState, false)) { this.view.dispatch({ effects: signatureEffect.of(null) }); } } destroy() { this.close(); } }, { eventObservers: { pointerdown() { this.close(); }, touchstart() { this.close(); }, wheel() { this.close(); }, scroll() { this.close(); }, }, }, ); export const showSignatureHelp: Command = (view) => { let plugin = view.plugin(signaturePlugin); if (!plugin) { view.dispatch({ effects: StateEffect.appendConfig.of([signatureState, signaturePlugin]), }); plugin = view.plugin(signaturePlugin); } const field = view.state.field(signatureState); if (!plugin || field === undefined) return false; const lspPlugin = LSPPlugin.get(view); if (!lspPlugin) return false; plugin.startRequest(lspPlugin, { triggerKind: 1, activeSignatureHelp: field ? field.data : undefined, isRetrigger: !!field, }); return true; }; export const nextSignature: Command = (view) => { const field = view.state.field(signatureState, false); if (!field) return false; if (field.active < field.data.signatures.length - 1) { view.dispatch({ effects: signatureEffect.of({ data: field.data, active: field.active + 1, pos: field.tooltip.pos, }), }); } return true; }; export const prevSignature: Command = (view) => { const field = view.state.field(signatureState, false); if (!field) return false; if (field.active > 0) { view.dispatch({ effects: signatureEffect.of({ data: field.data, active: field.active - 1, pos: field.tooltip.pos, }), }); } return true; }; export const signatureKeymap: readonly KeyBinding[] = [ { key: "Mod-Shift-Space", run: showSignatureHelp }, { key: "Mod-Shift-ArrowUp", run: prevSignature }, { key: "Mod-Shift-ArrowDown", run: nextSignature }, ]; export function hoverTooltips(config: { hoverTime?: number } = {}): Extension { return [ hoverTooltip(lspTooltipSource, { hideOnChange: true, hoverTime: config.hoverTime, }), closeHoverOnInteraction, ]; } export function signatureHelp(config: { keymap?: boolean } = {}): Extension { return [ signatureState, signaturePlugin, config.keymap === false ? [] : Prec.high(keymap.of(signatureKeymap)), ]; } ================================================ FILE: src/cm/lsp/transport.ts ================================================ /* Language servers that expose stdio are proxied through a lightweight WebSocket bridge so the CodeMirror client can continue to speak WebSocket. */ import type { Transport } from "@codemirror/lsp-client"; import type { LspServerDefinition, TransportContext, TransportHandle, WebSocketTransportOptions, } from "./types"; const DEFAULT_TIMEOUT = 5000; const RECONNECT_BASE_DELAY = 500; const RECONNECT_MAX_DELAY = 10000; const RECONNECT_MAX_ATTEMPTS = 5; type MessageListener = (data: string) => void; interface TransportInterface extends Transport { send(message: string): void; subscribe(handler: MessageListener): void; unsubscribe(handler: MessageListener): void; } function createWebSocketTransport( server: LspServerDefinition, context: TransportContext, ): TransportHandle { const transport = server.transport; if (!transport) { throw new Error( `LSP server ${server.id} is missing transport configuration`, ); } let url = transport.url; const options: WebSocketTransportOptions = transport.options ?? {}; // Use dynamic port from auto-port discovery if available if (context.dynamicPort && context.dynamicPort > 0) { url = `ws://127.0.0.1:${context.dynamicPort}/`; console.info( `[LSP:${server.id}] Using auto-discovered port ${context.dynamicPort}`, ); } // URL is only required when not using dynamic port if (!url) { throw new Error( `WebSocket transport for ${server.id} has no URL (and no dynamic port available)`, ); } // Store validated URL in a const for TypeScript narrowing in nested functions const wsUrl: string = url; const listeners = new Set(); const binaryMode = !!options.binary; const timeout = options.timeout ?? DEFAULT_TIMEOUT; const enableReconnect = options.reconnect !== false; const maxReconnectAttempts = options.maxReconnectAttempts ?? RECONNECT_MAX_ATTEMPTS; let socket: WebSocket | null = null; let disposed = false; let reconnectAttempts = 0; let reconnectTimer: ReturnType | null = null; let connected = false; const encoder = binaryMode ? new TextEncoder() : null; function createSocket(): WebSocket { try { // pylsp's websocket endpoint does not require subprotocol negotiation. // Avoid passing protocols to keep the handshake simple. const ws = new WebSocket(wsUrl); if (binaryMode) { ws.binaryType = "arraybuffer"; } return ws; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error( `Failed to construct WebSocket for ${server.id} (${wsUrl}): ${message}`, ); } } function handleMessage(event: MessageEvent): void { let data: string; if (typeof event.data === "string") { data = event.data; } else if (event.data instanceof Blob) { // Handle Blob synchronously by queuing - avoids async ordering issues event.data .text() .then((text: string) => { dispatchToListeners(text); }) .catch((err: Error) => { console.error("Failed to read Blob message", err); }); return; } else if (event.data instanceof ArrayBuffer) { data = new TextDecoder().decode(event.data); } else { console.warn( "Unknown WebSocket message type", typeof event.data, event.data, ); data = String(event.data); } dispatchToListeners(data); } function dispatchToListeners(data: string): void { // Debugging aid while stabilising websocket transport if (context?.debugWebSocket) { console.debug(`[LSP:${server.id}] <=`, data); } // Temporary fix // Intercept server requests that the CodeMirror LSP client doesn't handle // The client only handles notifications, but some servers (e.g., TypeScript) // send requests like window/workDoneProgress/create that need a response try { const msg = JSON.parse(data); if ( msg && typeof msg.id !== "undefined" && msg.method === "window/workDoneProgress/create" ) { // This is a request, respond with success const response = JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: null, }); if (context?.debugWebSocket) { console.debug(`[LSP:${server.id}] => (auto-response)`, response); } if (socket && socket.readyState === WebSocket.OPEN) { if (binaryMode && encoder) { socket.send(encoder.encode(response)); } else { socket.send(response); } } // Don't pass this request to listeners since we handled it console.info( `[LSP:${server.id}] Auto-responded to window/workDoneProgress/create`, ); return; } } catch (_) { // Not valid JSON or missing fields, pass through normally } listeners.forEach((listener) => { try { listener(data); } catch (error) { console.error("LSP transport listener failed", error); } }); } function handleClose(event: CloseEvent): void { connected = false; if (disposed) return; const wasClean = event.wasClean || event.code === 1000; if (wasClean) { console.info(`[LSP:${server.id}] WebSocket closed cleanly`); return; } console.warn( `[LSP:${server.id}] WebSocket closed unexpectedly (code: ${event.code})`, ); if (enableReconnect && reconnectAttempts < maxReconnectAttempts) { scheduleReconnect(); } else if (reconnectAttempts >= maxReconnectAttempts) { console.error(`[LSP:${server.id}] Max reconnection attempts reached`); } } function handleError(event: Event): void { if (disposed) return; const errorEvent = event as ErrorEvent; const reason = errorEvent?.message || errorEvent?.type || "connection error"; console.error(`[LSP:${server.id}] WebSocket error: ${reason}`); } function scheduleReconnect(): void { if (disposed || reconnectTimer) return; const delay = Math.min( RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts), RECONNECT_MAX_DELAY, ); reconnectAttempts++; console.info( `[LSP:${server.id}] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`, ); reconnectTimer = setTimeout(() => { reconnectTimer = null; if (disposed) return; attemptReconnect(); }, delay); } function attemptReconnect(): void { if (disposed) return; try { socket = createSocket(); setupSocketHandlers(socket); socket.onopen = () => { connected = true; reconnectAttempts = 0; console.info(`[LSP:${server.id}] Reconnected successfully`); if (socket) { socket.onopen = null; } }; } catch (error) { console.error(`[LSP:${server.id}] Reconnection failed`, error); if (reconnectAttempts < maxReconnectAttempts) { scheduleReconnect(); } } } function setupSocketHandlers(ws: WebSocket): void { ws.onmessage = handleMessage; ws.onclose = handleClose; ws.onerror = handleError; } // Initial socket creation socket = createSocket(); const ready = new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { if (socket) { socket.onopen = null; socket.onerror = null; } try { socket?.close(); } catch (_) { // Ignore close errors } reject(new Error(`Timed out opening WebSocket for ${server.id}`)); }, timeout); if (socket) { socket.onopen = () => { clearTimeout(timeoutId); connected = true; if (socket) { setupSocketHandlers(socket); } resolve(); }; socket.onerror = (event: Event) => { clearTimeout(timeoutId); if (socket) { socket.onopen = null; socket.onerror = null; } const errorEvent = event as ErrorEvent; const reason = errorEvent?.message || errorEvent?.type || "connection error"; reject(new Error(`WebSocket error for ${server.id}: ${reason}`)); }; } }); const transportInterface: TransportInterface = { send(message: string): void { if (!connected || !socket || socket.readyState !== WebSocket.OPEN) { throw new Error("WebSocket transport is not open"); } if (binaryMode && encoder) { socket.send(encoder.encode(message)); } else { socket.send(message); } }, subscribe(handler: MessageListener): void { listeners.add(handler); }, unsubscribe(handler: MessageListener): void { listeners.delete(handler); }, }; const dispose = (): void => { disposed = true; connected = false; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } listeners.clear(); if (socket) { if ( socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING ) { return; } try { socket.close(1000, "Client disposed"); } catch (_) { // Ignore close errors } } }; return { transport: transportInterface, dispose, ready }; } function createStdioTransport( server: LspServerDefinition, context: TransportContext, ): TransportHandle { if (!server.transport) { throw new Error( `LSP server ${server.id} is missing transport configuration`, ); } if ( !server.transport.url && !(context.dynamicPort && context.dynamicPort > 0) ) { throw new Error( `STDIO transport for ${server.id} is missing a websocket bridge url`, ); } if (!server.transport.options?.binary) { console.info( `LSP server ${server.id} is using stdio bridge without binary mode. Falling back to text frames.`, ); } return createWebSocketTransport(server, context); } export function createTransport( server: LspServerDefinition, context: TransportContext = {}, ): TransportHandle { if (!server) { throw new Error("createTransport requires a server configuration"); } if (!server.transport) { throw new Error( `LSP server ${server.id || "unknown"} is missing transport configuration`, ); } const kind = server.transport.kind; if (!kind) { throw new Error( `LSP server ${server.id} transport is missing 'kind' property`, ); } switch (kind) { case "websocket": return createWebSocketTransport(server, context); case "stdio": return createStdioTransport(server, context); case "external": if (typeof server.transport.create === "function") { return server.transport.create(server, context); } throw new Error( `LSP server ${server.id} declares an external transport without a create() factory`, ); default: throw new Error(`Unsupported transport kind: ${kind}`); } } export default { createTransport }; ================================================ FILE: src/cm/lsp/types.ts ================================================ import type { LSPClient, LSPClientConfig, LSPClientExtension, Transport, Workspace, WorkspaceFile, } from "@codemirror/lsp-client"; import type { ChangeSet, Extension, MapMode, Text } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; import type { Diagnostic as LSPDiagnostic, FormattingOptions as LSPFormattingOptions, Position, Range, TextEdit, } from "vscode-languageserver-types"; export type { LSPClient, LSPClientConfig, LSPClientExtension, LSPDiagnostic, LSPFormattingOptions, Position, Range, TextEdit, Transport, Workspace, WorkspaceFile, }; export interface WorkspaceFileUpdate { file: WorkspaceFile; prevDoc: Text; changes: ChangeSet; } // ============================================================================ // Transport Types // ============================================================================ export type TransportKind = "websocket" | "stdio" | "external"; type MaybePromise = T | Promise; export interface WebSocketTransportOptions { binary?: boolean; timeout?: number; reconnect?: boolean; maxReconnectAttempts?: number; } export interface TransportDescriptor { kind: TransportKind; url?: string; command?: string; args?: string[]; options?: WebSocketTransportOptions; protocols?: string[]; create?: ( server: LspServerDefinition, context: TransportContext, ) => TransportHandle; } export interface TransportHandle { transport: Transport; dispose: () => Promise | void; ready: Promise; } export interface TransportContext { uri?: string; file?: AcodeFile; view?: EditorView; languageId?: string; rootUri?: string | null; originalRootUri?: string; debugWebSocket?: boolean; /** Dynamically discovered port from auto-port discovery */ dynamicPort?: number; } // ============================================================================ // Server Registry Types // ============================================================================ export interface BridgeConfig { kind: "axs"; /** Optional port - if not provided, auto-port discovery will be used */ port?: number; command: string; args?: string[]; /** Session ID for port file naming (defaults to command name) */ session?: string; } export type InstallerKind = | "apk" | "npm" | "pip" | "cargo" | "github-release" | "manual" | "shell"; export interface LauncherInstallConfig { kind?: InstallerKind; command?: string; updateCommand?: string; uninstallCommand?: string; label?: string; source?: string; executable?: string; packages?: string[]; pipCommand?: string; npmCommand?: string; pythonCommand?: string; global?: boolean; breakSystemPackages?: boolean; repo?: string; assetNames?: Record; archiveType?: "zip" | "binary"; extractFile?: string; binaryPath?: string; } export interface LauncherConfig { command?: string; args?: string[]; startCommand?: string | string[]; checkCommand?: string; versionCommand?: string; updateCommand?: string; uninstallCommand?: string; install?: LauncherInstallConfig; bridge?: BridgeConfig; } export interface BuiltinExtensionsConfig { hover?: boolean; completion?: boolean; signature?: boolean; keymaps?: boolean; diagnostics?: boolean; inlayHints?: boolean; formatting?: boolean; } export interface AcodeClientConfig { useDefaultExtensions?: boolean; builtinExtensions?: BuiltinExtensionsConfig; extensions?: (Extension | LSPClientExtension)[]; notificationHandlers?: Record< string, (client: LSPClient, params: unknown) => boolean >; workspace?: (client: LSPClient) => Workspace; rootUri?: string; timeout?: number; } export interface LanguageResolverContext { languageId: string; languageName?: string; uri?: string; file?: AcodeFile; } export interface DocumentUriContext extends RootUriContext { normalizedUri?: string | null; } export interface LspServerManifest { id?: string; label?: string; enabled?: boolean; languages?: string[]; transport?: TransportDescriptor; initializationOptions?: Record; clientConfig?: Record | AcodeClientConfig; startupTimeout?: number; capabilityOverrides?: Record; rootUri?: | ((uri: string, context: unknown) => MaybePromise) | ((uri: string, context: RootUriContext) => MaybePromise) | null; documentUri?: | (( uri: string, context: DocumentUriContext, ) => MaybePromise) | null; resolveLanguageId?: | ((context: LanguageResolverContext) => string | null) | null; launcher?: LauncherConfig; useWorkspaceFolders?: boolean; } export interface LspServerBundle { id: string; label?: string; getServers: () => LspServerManifest[]; getExecutable?: ( serverId: string, manifest: LspServerManifest, ) => string | null | undefined; checkInstallation?: ( serverId: string, manifest: LspServerManifest, ) => Promise; installServer?: ( serverId: string, manifest: LspServerManifest, mode: "install" | "update" | "reinstall", options?: { promptConfirm?: boolean }, ) => Promise; uninstallServer?: ( serverId: string, manifest: LspServerManifest, options?: { promptConfirm?: boolean }, ) => Promise; } export type LspServerProvider = LspServerBundle; export interface LspServerDefinition { id: string; label: string; enabled: boolean; languages: string[]; transport: TransportDescriptor; initializationOptions?: Record; clientConfig?: AcodeClientConfig; startupTimeout?: number; capabilityOverrides?: Record; rootUri?: | ((uri: string, context: RootUriContext) => MaybePromise) | null; documentUri?: | (( uri: string, context: DocumentUriContext, ) => MaybePromise) | null; resolveLanguageId?: | ((context: LanguageResolverContext) => string | null) | null; launcher?: LauncherConfig; /** * When true, uses a single server instance with workspace folders * instead of starting separate servers per project root. * Heavy LSP servers like TypeScript and rust-analyzer should use this. */ useWorkspaceFolders?: boolean; } export interface RootUriContext { uri?: string; file?: AcodeFile; view?: EditorView; languageId?: string; rootUri?: string; } export type RegistryEventType = "register" | "unregister" | "update"; export type RegistryEventListener = ( event: RegistryEventType, server: LspServerDefinition, ) => void; // ============================================================================ // Client Manager Types // ============================================================================ export interface FileMetadata { uri: string; languageId?: string; languageName?: string; view?: EditorView; file?: AcodeFile; rootUri?: string; } export interface FormattingOptions { tabSize?: number; insertSpaces?: boolean; [key: string]: unknown; } export interface ClientManagerOptions { diagnosticsUiExtension?: Extension | Extension[]; clientExtensions?: Extension | Extension[]; resolveRoot?: (context: RootUriContext) => Promise; displayFile?: (uri: string) => Promise; openFile?: (uri: string) => Promise; resolveLanguageId?: (uri: string) => string | null; onClientIdle?: (info: ClientIdleInfo) => void; } export interface ClientIdleInfo { server: LspServerDefinition; client: LSPClient; rootUri: string | null; } export interface ClientState { server: LspServerDefinition; client: LSPClient; transport: TransportHandle; rootUri: string | null; attach: (uri: string, view: EditorView, aliases?: string[]) => void; detach: (uri: string, view?: EditorView) => void; dispose: () => Promise; } export interface NormalizedRootUri { normalizedRootUri: string | null; originalRootUri: string | null; } // ============================================================================ // Server Launcher Types // ============================================================================ export interface ManagedServerEntry { uuid: string; command: string; startedAt: number; /** Port number for the axs proxy (for stats endpoint) */ port?: number; } export type InstallStatus = "present" | "declined" | "failed"; export interface InstallCheckResult { status: "present" | "missing" | "failed" | "unknown"; version?: string | null; canInstall: boolean; canUpdate: boolean; message?: string; } /** * Port information from auto-port discovery */ export interface PortInfo { /** The discovered port number */ port: number; /** Path to the port file */ filePath: string; /** Session ID used for the port file */ session: string; } export interface WaitOptions { attempts?: number; delay?: number; probeTimeout?: number; } /** * Result from ensureServerRunning */ export interface EnsureServerResult { uuid: string | null; /** Port discovered from port file (for auto-port discovery) */ discoveredPort?: number; } /** * Stats returned from the axs proxy /status endpoint */ export interface LspServerStats { program: string; processes: Array<{ pid: number; uptime_secs: number; memory_bytes: number; }>; } /** * Formatted stats for UI display */ export interface LspServerStatsFormatted { memoryBytes: number; memoryFormatted: string; uptimeSeconds: number; uptimeFormatted: string; pid: number | null; processCount: number; } // ============================================================================ // Workspace Types // ============================================================================ export interface WorkspaceOptions { displayFile?: (uri: string) => Promise; openFile?: (uri: string) => Promise; resolveLanguageId?: (uri: string) => string | null; } // ============================================================================ // Diagnostics Types // ============================================================================ export interface LspDiagnostic { from: number; to: number; severity: "error" | "warning" | "info" | "hint"; message: string; source?: string; /** Related diagnostic information (e.g., location of declaration for 'unused' errors) */ relatedInformation?: DiagnosticRelatedInformation[]; } /** Related information for a diagnostic (mapped to editor positions) */ export interface DiagnosticRelatedInformation { /** Document URI */ uri: string; /** Start position (offset in document) */ from: number; /** End position (offset in document) */ to: number; /** Message describing the relationship */ message: string; } export interface PublishDiagnosticsParams { uri: string; version?: number; diagnostics: RawDiagnostic[]; } export interface RawDiagnostic { range: Range; severity?: number; code?: number | string; source?: string; message: string; /** Related diagnostic locations from LSP (raw positions) */ relatedInformation?: RawDiagnosticRelatedInformation[]; } /** Raw related information from LSP (before position mapping) */ export interface RawDiagnosticRelatedInformation { location: { uri: string; range: Range; }; message: string; } // ============================================================================ // Formatter Types // ============================================================================ export interface AcodeApi { registerFormatter: ( id: string, extensions: string[], formatter: () => Promise, label: string, ) => void; } /** * Uri utility interface */ export interface ParsedUri { docId?: string; rootUri?: string; isFileUri?: boolean; } /** * Interface representing the LSPPlugin instance API. */ export interface LSPPluginAPI { /** The document URI this plugin is attached to */ uri: string; /** The LSP client instance */ client: LSPClient & { sync: () => void; connected?: boolean }; /** Convert a document offset to an LSP Position */ toPosition: (offset: number) => { line: number; character: number }; /** Convert an LSP Position to a document offset */ fromPosition: ( pos: { line: number; character: number }, doc?: unknown, ) => number; /** The currently synced document state */ syncedDoc: { length: number }; /** Pending changes that haven't been synced yet */ unsyncedChanges: { mapPos: (pos: number, assoc?: number, mode?: MapMode) => number | null; empty: boolean; }; /** Clear pending changes */ clear: () => void; } /** * Interface for workspace file with view access */ export interface WorkspaceFileWithView { version: number; getView: () => EditorView | null; } /** * Interface for workspace with file access */ export interface WorkspaceWithFileAccess { getFile: (uri: string) => WorkspaceFileWithView | null; } /** * LSPClient with workspace access (for type casting in notification handlers) */ export interface LSPClientWithWorkspace { workspace: WorkspaceWithFileAccess; } // Extend the LSPClient with Acode-specific properties declare module "@codemirror/lsp-client" { interface LSPClient { __acodeLoggedInfo?: boolean; } } ================================================ FILE: src/cm/lsp/workspace.ts ================================================ import type { WorkspaceFile } from "@codemirror/lsp-client"; import { LSPPlugin, Workspace } from "@codemirror/lsp-client"; import type { Text, TransactionSpec } from "@codemirror/state"; import type { EditorView } from "@codemirror/view"; import { getModeForPath } from "cm/modelist"; import type { WorkspaceFileUpdate, WorkspaceOptions } from "./types"; class AcodeWorkspaceFile implements WorkspaceFile { uri: string; languageId: string; version: number; doc: Text; views: Set; constructor( uri: string, languageId: string, version: number, doc: Text, view?: EditorView, ) { this.uri = uri; this.languageId = languageId; this.version = version; this.doc = doc; this.views = new Set(); if (view) this.views.add(view); } getView(preferred?: EditorView): EditorView | null { if (preferred && this.views.has(preferred)) return preferred; const iterator = this.views.values(); const next = iterator.next(); return next.done ? null : next.value; } } export default class AcodeWorkspace extends Workspace { files: AcodeWorkspaceFile[]; options: WorkspaceOptions; #fileMap: Map; #versions: Record; #workspaceFolders: Set; constructor( client: ConstructorParameters[0], options: WorkspaceOptions = {}, ) { super(client); this.files = []; this.#fileMap = new Map(); this.#versions = Object.create(null) as Record; this.#workspaceFolders = new Set(); this.options = options; } #getOrCreateFile( uri: string, languageId: string, view: EditorView, ): AcodeWorkspaceFile { let file = this.#fileMap.get(uri); if (!file) { const doc = view.state?.doc; if (!doc) { throw new Error( `Cannot create workspace file without document: ${uri}`, ); } file = new AcodeWorkspaceFile( uri, languageId, this.#nextFileVersion(uri), doc, view, ); this.#fileMap.set(uri, file); this.files.push(file); this.client.didOpen(file); } file.views.add(view); return file; } #getFileEntry(uri: string): AcodeWorkspaceFile | null { return this.#fileMap.get(uri) ?? null; } #removeFileEntry(file: AcodeWorkspaceFile): void { this.#fileMap.delete(file.uri); this.files = this.files.filter((candidate) => candidate !== file); } #nextFileVersion(uri: string): number { const current = this.#versions[uri] ?? -1; const next = current + 1; this.#versions[uri] = next; return next; } #resolveLanguageIdForUri(uri: string): string { if (typeof this.options.resolveLanguageId === "function") { const resolved = this.options.resolveLanguageId(uri); if (resolved) return resolved; } try { const mode = getModeForPath(uri); if (mode?.name) { return String(mode.name).toLowerCase(); } } catch (error) { console.warn( `[LSP:Workspace] Failed to resolve language id for ${uri}`, error, ); } return "plaintext"; } syncFiles(): readonly WorkspaceFileUpdate[] { const updates: WorkspaceFileUpdate[] = []; for (const file of this.files) { const view = file.getView(); if (!view) continue; const plugin = LSPPlugin.get(view); if (!plugin) continue; const { unsyncedChanges } = plugin; if (unsyncedChanges.empty) continue; updates.push({ file, prevDoc: file.doc, changes: unsyncedChanges }); file.doc = view.state.doc; file.version = this.#nextFileVersion(file.uri); plugin.clear(); } return updates; } openFile(uri: string, languageId: string, view: EditorView): void { if (!view) return; this.#getOrCreateFile(uri, languageId, view); } closeFile(uri: string, view?: EditorView): void { const file = this.#getFileEntry(uri); if (!file) return; if (view && file.views.has(view)) { file.views.delete(view); } if (!file.views.size) { this.client.didClose(uri); this.#removeFileEntry(file); } } getFile(uri: string): AcodeWorkspaceFile | null { return this.#getFileEntry(uri); } requestFile(uri: string): Promise { return Promise.resolve(this.#getFileEntry(uri)); } connected(): void { for (const file of this.files) { this.client.didOpen(file); } } updateFile(uri: string, update: TransactionSpec): void { const file = this.#getFileEntry(uri); if (file) { const view = file.getView(); if (view) { view.dispatch(update); return; } } // File is not open - try to open it and apply the update this.#applyUpdateToClosedFile(uri, update).catch((error) => { console.warn(`[LSP:Workspace] Failed to apply update: ${uri}`, error); }); } async #applyUpdateToClosedFile( uri: string, update: TransactionSpec, ): Promise { if (typeof this.options.displayFile !== "function") return; try { const view = await this.options.displayFile(uri); if (!view?.state?.doc) return; const languageId = this.#resolveLanguageIdForUri(uri); const file = this.#getOrCreateFile(uri, languageId, view); const fileView = file.getView(); if (fileView) { fileView.dispatch(update); } } catch (error) { console.error(`[LSP:Workspace] Failed to apply update: ${uri}`, error); } } async displayFile(uri: string): Promise { if (typeof this.options.displayFile === "function") { try { return await this.options.displayFile(uri); } catch (error) { console.error("[LSP:Workspace] Failed to display file", error); } } return null; } // ======================================================================== // Workspace Folders Support // ======================================================================== #getFolderName(uri: string): string { const parts = uri.replace(/\/$/, "").split("/"); return parts[parts.length - 1] || uri; } #sendNotification(method: string, params: unknown): void { // Access the client's transport to send raw JSON-RPC notification const client = this.client as unknown as { connected: boolean; transport?: { send: (message: string) => void }; }; if (!client.connected || !client.transport) { console.warn(`[LSP:Workspace] Cannot send notification: not connected`); return; } const message = JSON.stringify({ jsonrpc: "2.0", method, params, }); client.transport.send(message); } hasWorkspaceFolder(uri: string): boolean { return this.#workspaceFolders.has(uri); } getWorkspaceFolders(): string[] { return Array.from(this.#workspaceFolders); } addWorkspaceFolder(uri: string): boolean { if (this.#workspaceFolders.has(uri)) { return false; } this.#workspaceFolders.add(uri); this.#sendNotification("workspace/didChangeWorkspaceFolders", { event: { added: [{ uri, name: this.#getFolderName(uri) }], removed: [], }, }); console.info(`[LSP:Workspace] Added workspace folder: ${uri}`); return true; } removeWorkspaceFolder(uri: string): boolean { if (!this.#workspaceFolders.has(uri)) { return false; } this.#workspaceFolders.delete(uri); this.#sendNotification("workspace/didChangeWorkspaceFolders", { event: { added: [], removed: [{ uri, name: this.#getFolderName(uri) }], }, }); console.info(`[LSP:Workspace] Removed workspace folder: ${uri}`); return true; } } ================================================ FILE: src/cm/mainEditorExtensions.ts ================================================ import type { Extension } from "@codemirror/state"; import { EditorView, scrollPastEnd } from "@codemirror/view"; interface MainEditorExtensionOptions { emmetExtensions?: Extension[]; baseExtensions?: Extension[]; commandKeymapExtension?: Extension; themeExtension?: Extension; pointerCursorVisibilityExtension?: Extension; shiftClickSelectionExtension?: Extension; touchSelectionUpdateExtension?: Extension; searchExtension?: Extension; readOnlyExtension?: Extension; optionExtensions?: Extension[]; } function pushExtension(target: Extension[], extension?: Extension): void { if (extension == null) return; target.push(extension); } export const fixedHeightTheme = EditorView.theme({ "&": { height: "100%" }, ".cm-scroller": { height: "100%", overflow: "auto" }, }); export function createMainEditorExtensions( options: MainEditorExtensionOptions = {}, ): Extension[] { const extensions: Extension[] = []; if (options.emmetExtensions?.length) { extensions.push(...options.emmetExtensions); } if (options.baseExtensions?.length) { extensions.push(...options.baseExtensions); } pushExtension(extensions, options.commandKeymapExtension); pushExtension(extensions, options.themeExtension); extensions.push(fixedHeightTheme); extensions.push(scrollPastEnd()); pushExtension(extensions, options.pointerCursorVisibilityExtension); pushExtension(extensions, options.shiftClickSelectionExtension); pushExtension(extensions, options.touchSelectionUpdateExtension); pushExtension(extensions, options.searchExtension); pushExtension(extensions, options.readOnlyExtension); if (options.optionExtensions?.length) { extensions.push(...options.optionExtensions); } return extensions; } export default createMainEditorExtensions; ================================================ FILE: src/cm/modelist.ts ================================================ import type { Extension } from "@codemirror/state"; export type LanguageExtensionProvider = () => Extension | Promise; export interface AddModeOptions { aliases?: string[]; filenameMatchers?: RegExp[]; } export interface ModesByName { [name: string]: Mode; } const modesByName: ModesByName = {}; const modes: Mode[] = []; function normalizeModeKey(value: string): string { return String(value ?? "") .trim() .toLowerCase(); } function normalizeAliases(aliases: string[] = [], name: string): string[] { const normalized = new Set(); for (const alias of aliases) { const key = normalizeModeKey(alias); if (!key || key === name) continue; normalized.add(key); } return [...normalized]; } function escapeRegExp(value: string): string { return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Initialize CodeMirror mode list functionality */ export function initModes(): void { // CodeMirror modes don't need the same ace.define wrapper // but we maintain the same API structure for compatibility } /** * Add language mode to CodeMirror editor */ export function addMode( name: string, extensions: string | string[], caption?: string, languageExtension: LanguageExtensionProvider | null = null, options: AddModeOptions = {}, ): void { const filename = normalizeModeKey(name); const mode = new Mode( filename, caption, extensions, languageExtension, options, ); modesByName[filename] = mode; mode.aliases.forEach((alias) => { if (!modesByName[alias]) { modesByName[alias] = mode; } }); modes.push(mode); } /** * Remove language mode from CodeMirror editor */ export function removeMode(name: string): void { const filename = normalizeModeKey(name); const mode = modesByName[filename]; if (!mode) return; delete modesByName[mode.name]; mode.aliases.forEach((alias) => { if (modesByName[alias] === mode) { delete modesByName[alias]; } }); const modeIndex = modes.findIndex( (registeredMode) => registeredMode === mode, ); if (modeIndex >= 0) { modes.splice(modeIndex, 1); } } /** * Get mode for file path */ export function getModeForPath(path: string): Mode { let mode = modesByName.text; const fileName = path.split(/[/\\]/).pop() || ""; // Sort modes by specificity (descending) to check most specific first const sortedModes = [...modes].sort((a, b) => { return getModeSpecificityScore(b) - getModeSpecificityScore(a); }); for (const iMode of sortedModes) { if (iMode.supportsFile?.(fileName)) { mode = iMode; break; } } return mode; } /** * Calculates a specificity score for a mode. * Higher score means more specific. * - Anchored patterns (e.g., "^Dockerfile") get a base score of 1000. * - Non-anchored patterns (extensions) are scored by length. */ function getModeSpecificityScore(modeInstance: Mode): number { const extensionsStr = modeInstance.extensions; let maxScore = 0; if (extensionsStr) { const patterns = extensionsStr.split("|"); for (const pattern of patterns) { let currentScore = 0; if (pattern.startsWith("^")) { // Exact filename match or anchored pattern currentScore = 1000 + (pattern.length - 1); // Subtract 1 for '^' } else { // Extension match currentScore = pattern.length; } if (currentScore > maxScore) { maxScore = currentScore; } } } for (const matcher of modeInstance.filenameMatchers) { const score = 1000 + matcher.source.length; if (score > maxScore) { maxScore = score; } } return maxScore; } /** * Get all modes by name */ export function getModesByName(): ModesByName { return modesByName; } /** * Get all modes array */ export function getModes(): Mode[] { return modes; } export function getMode(name: string): Mode | null { return modesByName[normalizeModeKey(name)] || null; } export class Mode { extensions: string; caption: string; name: string; mode: string; aliases: string[]; extRe: RegExp | null; filenameMatchers: RegExp[]; languageExtension: LanguageExtensionProvider | null; constructor( name: string, caption: string | undefined, extensions: string | string[], languageExtension: LanguageExtensionProvider | null = null, options: AddModeOptions = {}, ) { if (Array.isArray(extensions)) { extensions = extensions.join("|"); } this.name = name; this.mode = name; // CodeMirror uses different mode naming this.extensions = extensions; this.caption = caption || this.name.replace(/_/g, " "); this.aliases = normalizeAliases(options.aliases, this.name); this.filenameMatchers = Array.isArray(options.filenameMatchers) ? options.filenameMatchers.filter((matcher) => matcher instanceof RegExp) : []; this.languageExtension = languageExtension; let re = ""; if (!extensions) { this.extRe = null; return; } const patterns = extensions .split("|") .map((pattern) => pattern.trim()) .filter(Boolean); const filenamePatterns = patterns .filter((pattern) => pattern.startsWith("^")) .map((pattern) => `^${escapeRegExp(pattern.slice(1))}$`); const extensionPatterns = patterns .filter((pattern) => !pattern.startsWith("^")) .map((pattern) => escapeRegExp(pattern)); const regexParts: string[] = []; if (extensionPatterns.length) { regexParts.push(`^.*\\.(${extensionPatterns.join("|")})$`); } regexParts.push(...filenamePatterns); if (!regexParts.length) { this.extRe = null; return; } re = regexParts.length === 1 ? regexParts[0] : `(?:${regexParts.join("|")})`; this.extRe = new RegExp(re, "i"); } supportsFile(filename: string): boolean { if (this.extRe?.test(filename)) return true; return this.filenameMatchers.some((matcher) => { matcher.lastIndex = 0; return matcher.test(filename); }); } /** * Get the CodeMirror language extension */ getExtension(): LanguageExtensionProvider | null { return this.languageExtension; } /** * Check if the language extension is available (loaded) */ isAvailable(): boolean { return this.languageExtension !== null; } } ================================================ FILE: src/cm/modes/luau/index.ts ================================================ import { IndentContext, LanguageSupport, StreamLanguage, StringStream, } from "@codemirror/language"; type Tokenizer = (stream: StringStream, state: LuauState) => string | null; interface LuauState { basecol: number; indentDepth: number; cur: Tokenizer; stack: Tokenizer[]; expectFunctionName: boolean; afterFunctionName: boolean; expectTypeName: boolean; afterTypeName: boolean; afterTypeIdentifier: boolean; inType: boolean; typeDepth: number; genericDepth: number; interpolationBraceDepth: number; afterPropertyAccess: boolean; lastIdentifierWasStandard: boolean; docCommentExpectParamName: boolean; docCommentExpectType: boolean; } const controlKeywords = new Set([ "break", "continue", "do", "else", "elseif", "end", "for", "function", "if", "in", "repeat", "return", "then", "type", "until", "while", ]); const modifierKeywords = new Set(["export", "local"]); const logicalKeywords = new Set(["and", "not", "or"]); const typePrimitives = new Set([ "any", "boolean", "buffer", "never", "nil", "number", "string", "symbol", "thread", "unknown", "userdata", "vector", ]); const standardFunctions = new Set([ "assert", "collectgarbage", "delay", "error", "gcinfo", "getfenv", "getmetatable", "ipairs", "loadstring", "newproxy", "next", "pairs", "pcall", "print", "printidentity", "rawequal", "rawset", "require", "select", "setfenv", "setmetatable", "settings", "spawn", "stats", "tick", "time", "tonumber", "tostring", "type", "typeof", "unpack", "UserSettings", "version", "wait", "warn", ]); const standardNamespaces = new Set([ "bit32", "buffer", "coroutine", "debug", "math", "os", "string", "table", "task", "utf8", "vector", "Enum", ]); const standardVariables = new Set([ "_G", "_VERSION", "DebuggerManager", "PluginManager", "game", "plugin", "script", "shared", "workspace", ]); const metamethods = new Set([ "__add", "__call", "__concat", "__div", "__eq", "__idiv", "__index", "__iter", "__le", "__len", "__lt", "__metatable", "__mod", "__mode", "__mul", "__newindex", "__pow", "__sub", "__tostring", "__unm", ]); const typeTerminators = new Set([ "break", "continue", "do", "else", "elseif", "end", "for", "if", "in", "local", "repeat", "return", "then", "until", "while", ]); const indentTokens = new Set(["do", "function", "if", "repeat", "(", "{"]); const dedentTokens = new Set(["end", "until", ")", "}"]); const dedentPartial = /^(?:end|until|\)|}|else|elseif)\b/; function pushTokenizer(state: LuauState, tokenizer: Tokenizer) { state.stack.push(state.cur); state.cur = tokenizer; } function popTokenizer(state: LuauState) { state.cur = state.stack.pop() || normal; } function enterTypeContext(state: LuauState, depth = 0) { state.inType = true; state.typeDepth = depth; } function exitTypeContext(state: LuauState) { state.inType = false; state.typeDepth = 0; state.genericDepth = 0; state.afterTypeIdentifier = false; } function isWordStart(char: string) { return /[A-Za-z_]/.test(char); } function isWord(char: string) { return /[A-Za-z0-9_]/.test(char); } function isUpperConstant(word: string) { return /^[A-Z_][A-Z0-9_]*$/.test(word); } function isStandardWord(word: string) { return ( standardFunctions.has(word) || standardNamespaces.has(word) || standardVariables.has(word) ); } function looksLikeMethodSeparator(stream: StringStream) { return /^\s*[A-Za-z_][A-Za-z0-9_]*\s*\(/.test( stream.string.slice(stream.pos), ); } function readLongBracket(stream: StringStream) { let level = 0; while (stream.eat("=")) level++; return stream.eat("[") ? level : -1; } function bracketed(level: number, style: string): Tokenizer { return (stream, state) => { let seenEquals: number | null = null; while (true) { const char = stream.next(); if (char == null) break; if (seenEquals == null) { if (char === "]") seenEquals = 0; } else if (char === "=") { seenEquals++; } else if (char === "]" && seenEquals === level) { popTokenizer(state); break; } else { seenEquals = null; } } return style; }; } function quotedString(quote: string): Tokenizer { return (stream, state) => { let escaped = false; while (true) { const char = stream.next(); if (char == null) break; if (char === quote && !escaped) { popTokenizer(state); break; } escaped = !escaped && char === "\\"; } return "string"; }; } const interpolatedString: Tokenizer = (stream, state) => { while (true) { const char = stream.next(); if (char == null) break; if (char === "\\") { stream.next(); continue; } if (char === "{") { if (stream.pos - stream.start > 1) { stream.backUp(1); return "string"; } state.interpolationBraceDepth = 0; pushTokenizer(state, interpolatedExpression); return "punctuation"; } if (char === "`") { popTokenizer(state); break; } } return "string"; }; const interpolatedExpression: Tokenizer = (stream, state) => { if (stream.eatSpace()) return null; if (stream.peek() === "}" && state.interpolationBraceDepth === 0) { stream.next(); popTokenizer(state); return "punctuation"; } const style = normal(stream, state); const token = stream.current(); if (state.cur === interpolatedExpression) { if (token === "{") { state.interpolationBraceDepth++; } else if (token === "}" && state.interpolationBraceDepth > 0) { state.interpolationBraceDepth--; } } return style; }; const docCommentLine: Tokenizer = (stream, state) => { if (stream.sol()) { popTokenizer(state); return normal(stream, state); } if (stream.eatSpace()) return null; const peek = stream.peek(); if (!peek) { state.docCommentExpectParamName = false; state.docCommentExpectType = false; return null; } if (stream.match(/(?:\\|@)[A-Za-z_][A-Za-z0-9_]*/)) { const tag = stream.current(); state.docCommentExpectParamName = /(?:\\|@)param$/.test(tag); state.docCommentExpectType = false; return "attributeName"; } if (state.docCommentExpectParamName && isWordStart(peek)) { stream.next(); stream.eatWhile(isWord); state.docCommentExpectParamName = false; state.docCommentExpectType = true; return "variableName"; } if ( state.docCommentExpectType && (isWordStart(peek) || peek === "{" || peek === "(" || peek === "[" || peek === "?" || peek === "." || peek === "|") ) { stream.next(); stream.eatWhile(/[^\s,;]+/); state.docCommentExpectType = false; return "typeName"; } stream.next(); stream.eatWhile((char) => !/\s/.test(char)); return "comment"; }; function readNumber(stream: StringStream, firstChar: string) { const next = stream.peek(); if (firstChar === "0" && next && /[xX]/.test(next)) { stream.next(); stream.eatWhile(/[0-9a-fA-F_]/); return; } stream.eatWhile(/[\d_]/); if (stream.peek() === "." && stream.string.charAt(stream.pos + 1) !== ".") { stream.next(); stream.eatWhile(/[\d_]/); } const exponent = stream.peek(); if (exponent && /[eE]/.test(exponent)) { stream.next(); stream.eat(/[+-]/); stream.eatWhile(/[\d_]/); } } function classifyIdentifier(word: string, state: LuauState) { if (state.expectFunctionName && isWordStart(word)) { state.expectFunctionName = false; state.afterFunctionName = true; state.afterPropertyAccess = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return metamethods.has(word) ? "variableName.function.definition.special" : "variableName.function.definition"; } if (state.expectTypeName && word !== "function") { state.expectTypeName = false; state.afterTypeName = true; state.afterTypeIdentifier = true; state.afterFunctionName = false; state.lastIdentifierWasStandard = false; return "typeName.definition"; } if (state.afterPropertyAccess) { state.afterPropertyAccess = false; const isStandardProperty = state.lastIdentifierWasStandard; const isStandardMember = isStandardProperty || isStandardWord(word); state.lastIdentifierWasStandard = isStandardMember; state.afterFunctionName = false; state.afterTypeIdentifier = false; if (metamethods.has(word)) return "propertyName.special"; return isStandardMember ? "propertyName.standard" : "propertyName"; } if (logicalKeywords.has(word)) { state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "operatorKeyword"; } if (modifierKeywords.has(word)) { state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "modifier"; } if (word === "type") { state.expectTypeName = true; state.afterTypeName = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return "definitionKeyword"; } if (word === "function") { if (!state.expectTypeName) state.expectFunctionName = true; state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return "controlKeyword"; } if (word === "self") { state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "variableName.special"; } if (word === "true" || word === "false") { state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "bool"; } if (word === "nil") { state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "null"; } if (controlKeywords.has(word)) { if (state.inType && state.typeDepth === 0 && typeTerminators.has(word)) { exitTypeContext(state); } state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "controlKeyword"; } if (state.inType) { state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = true; if (word === "typeof") return "variableName.function.standard"; if (typePrimitives.has(word) || isUpperConstant(word)) return "typeName"; return "typeName"; } if (standardNamespaces.has(word)) { state.lastIdentifierWasStandard = true; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "namespace"; } if (standardVariables.has(word)) { state.lastIdentifierWasStandard = true; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "variableName.standard"; } if (standardFunctions.has(word)) { state.lastIdentifierWasStandard = true; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "variableName.function.standard"; } if (isUpperConstant(word)) { state.lastIdentifierWasStandard = false; state.afterFunctionName = false; state.afterTypeIdentifier = false; return "variableName.constant"; } state.lastIdentifierWasStandard = isStandardWord(word); state.afterFunctionName = false; state.afterTypeIdentifier = false; return "variableName"; } const normal: Tokenizer = (stream, state) => { const char = stream.next(); if (!char) return null; if (char === "-" && stream.eat("-")) { if (stream.eat("-")) { state.docCommentExpectParamName = false; state.docCommentExpectType = false; pushTokenizer(state, docCommentLine); return "comment"; } if (stream.eat("[")) { const longBracketStart = stream.pos; const level = readLongBracket(stream); if (level >= 0) { pushTokenizer(state, bracketed(level, "comment")); return state.cur(stream, state); } stream.backUp(stream.pos - longBracketStart); } stream.skipToEnd(); return "comment"; } if (char === '"' || char === "'") { pushTokenizer(state, quotedString(char)); return state.cur(stream, state); } if (char === "`") { pushTokenizer(state, interpolatedString); return state.cur(stream, state); } if (char === "[") { const longBracketStart = stream.pos; const level = readLongBracket(stream); if (level >= 0) { pushTokenizer(state, bracketed(level, "string")); return state.cur(stream, state); } stream.backUp(stream.pos - longBracketStart); } if (char === "@" && isWordStart(stream.peek() || "")) { stream.eatWhile(isWord); state.lastIdentifierWasStandard = false; return "attributeName"; } if (/\d/.test(char) || (char === "." && /\d/.test(stream.peek() || ""))) { readNumber(stream, char); state.lastIdentifierWasStandard = false; return "number"; } if (isWordStart(char)) { stream.eatWhile(isWord); return classifyIdentifier(stream.current(), state); } if (char === "." || char === ":") { if (char === "." && stream.eat(".")) { state.afterFunctionName = false; state.afterTypeIdentifier = false; if (stream.eat(".")) { state.lastIdentifierWasStandard = false; return "keyword"; } stream.eat("="); state.lastIdentifierWasStandard = false; return "operator"; } if (char === ":" && stream.eat(":")) { enterTypeContext(state); state.lastIdentifierWasStandard = false; return "operator"; } if ( char === ":" && !state.expectFunctionName && !looksLikeMethodSeparator(stream) ) { enterTypeContext(state); state.lastIdentifierWasStandard = false; return "operator"; } state.afterPropertyAccess = true; return "punctuation"; } if (char === "-" && stream.eat(">")) { enterTypeContext(state); state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return "operator"; } if ( char === "<" && (state.afterTypeName || state.afterFunctionName || state.afterTypeIdentifier) ) { enterTypeContext(state); state.genericDepth++; state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return "operator"; } if ( (char === "|" || char === "&" || char === "?") && (state.inType || char === "?") ) { state.lastIdentifierWasStandard = false; return "operator"; } if ( char === "+" || char === "-" || char === "*" || char === "/" || char === "%" || char === "^" || char === "#" || char === "=" || char === "<" || char === ">" || char === "~" || char === "!" ) { stream.eat("="); if (char === ">" && state.genericDepth > 0) { state.genericDepth--; if (state.genericDepth === 0 && state.typeDepth === 0) { state.inType = false; } state.afterTypeIdentifier = true; state.lastIdentifierWasStandard = false; return "operator"; } if (char === "/" && stream.eat("/")) stream.eat("="); if (char === "=" && state.afterTypeName && state.genericDepth === 0) { state.afterTypeName = false; enterTypeContext(state); } state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return "operator"; } if (char === "(" || char === "{" || char === "[") { if (char === "(" && state.expectFunctionName) { state.expectFunctionName = false; } if (char === "(") { state.expectTypeName = false; } if (state.inType) state.typeDepth++; state.lastIdentifierWasStandard = false; if (state.afterTypeName && char === "(") { state.afterTypeName = false; enterTypeContext(state, 1); } state.afterFunctionName = false; state.afterTypeIdentifier = false; return "punctuation"; } if (char === ")" || char === "}" || char === "]") { if (state.inType) { if (state.typeDepth > 0) { state.typeDepth--; } else if ( char === ")" && /^\s*->/.test(stream.string.slice(stream.pos)) ) { enterTypeContext(state); } else { exitTypeContext(state); } } state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return "punctuation"; } if (char === "," || char === ";") { if (state.inType && state.typeDepth === 0) exitTypeContext(state); state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return "punctuation"; } state.afterFunctionName = false; state.afterTypeIdentifier = false; state.lastIdentifierWasStandard = false; return null; }; const luauLanguage = StreamLanguage.define({ name: "luau", startState() { return { basecol: 0, indentDepth: 0, cur: normal, stack: [], expectFunctionName: false, afterFunctionName: false, expectTypeName: false, afterTypeName: false, afterTypeIdentifier: false, inType: false, typeDepth: 0, genericDepth: 0, interpolationBraceDepth: 0, afterPropertyAccess: false, lastIdentifierWasStandard: false, docCommentExpectParamName: false, docCommentExpectType: false, }; }, copyState(state) { return { ...state, stack: state.stack.slice(), }; }, token(stream, state) { if (stream.sol()) state.basecol = stream.indentation(); if (stream.eatSpace()) return null; const style = state.cur(stream, state); const word = stream.current(); if (style !== "comment" && style !== "string") { if (indentTokens.has(word)) state.indentDepth++; if (dedentTokens.has(word)) state.indentDepth--; } return style; }, indent(state, textAfter, context: IndentContext) { const closing = dedentPartial.test(textAfter); return ( state.basecol + context.unit * (state.indentDepth - (closing ? 1 : 0)) ); }, languageData: { commentTokens: { line: "--", block: { open: "--[[", close: "]]" } }, closeBrackets: { brackets: ["(", "[", "{", '"', "'", "`"] }, indentOnInput: /^\s*(?:end|until|else|elseif|\)|\})$/, }, }); export function luau() { return new LanguageSupport(luauLanguage); } export { luauLanguage }; ================================================ FILE: src/cm/rainbowBrackets.ts ================================================ import { syntaxTree } from "@codemirror/language"; import { RangeSetBuilder } from "@codemirror/state"; import type { DecorationSet, ViewUpdate } from "@codemirror/view"; import { Decoration, EditorView, ViewPlugin } from "@codemirror/view"; import type { SyntaxNode } from "@lezer/common"; const DEFAULT_DARK_COLORS = [ "#e5c07b", "#c678dd", "#56b6c2", "#61afef", "#98c379", "#d19a66", ]; const DEFAULT_LIGHT_COLORS = [ "#795e26", "#af00db", "#005cc5", "#008000", "#b15c00", "#267f99", ]; const BLOCK_SIZE = 2048; const MAX_BLOCK_CACHE_ENTRIES = 192; const CONTEXT_SIGNATURE_DEPTH = 4; const MIN_LOOK_BEHIND = 4000; const MAX_LOOK_BEHIND = 24000; const DEFAULT_EXACT_SCAN_LIMIT = 24000; const SKIP_CONTEXTS = new Set([ "String", "TemplateString", "Comment", "LineComment", "BlockComment", "RegExp", ]); const CLOSING_TO_OPENING = { ")": "(", "]": "[", "}": "{", } as const; type ClosingBracket = keyof typeof CLOSING_TO_OPENING; export interface RainbowBracketThemeConfig { dark?: boolean; keyword?: string; type?: string; class?: string; function?: string; string?: string; number?: string; constant?: string; variable?: string; foreground?: string; } export interface RainbowBracketsOptions { colors?: readonly string[]; exactScanLimit?: number; lookBehind?: number; } interface BracketInfo { char: string; colorIndex: number; } interface BracketToken { offset: number; char: string; } interface BlockCacheEntry { carrySkipChars: number; tokens: readonly BracketToken[]; } function normalizeHexColor(value: unknown): string | null { if (typeof value !== "string") return null; const color = value.trim().toLowerCase(); if (/^#([\da-f]{3}|[\da-f]{6})$/.test(color)) return color; return null; } function alignToBlockStart(pos: number): number { return pos - (pos % BLOCK_SIZE); } function clampLookBehind(value: number | undefined): number { if (!Number.isFinite(value)) return MAX_LOOK_BEHIND; return Math.max( MIN_LOOK_BEHIND, Math.min(MAX_LOOK_BEHIND, Math.floor(value || 0)), ); } function getScanStart( view: EditorView, lookBehind: number, exactScanLimit: number, ): number { const ranges = view.visibleRanges; if (!ranges.length) return 0; const firstVisibleFrom = ranges[0].from; const lastVisibleTo = ranges[ranges.length - 1].to; const docLength = view.state.doc.length; if (docLength <= exactScanLimit || firstVisibleFrom <= exactScanLimit) { return 0; } const visibleSpan = Math.max(1, lastVisibleTo - firstVisibleFrom); const dynamicLookBehind = Math.max( MIN_LOOK_BEHIND, Math.min(MAX_LOOK_BEHIND, visibleSpan * 3), ); return Math.max( 0, firstVisibleFrom - Math.max(lookBehind, dynamicLookBehind), ); } function isBracketCode(code: number): boolean { return ( code === 40 || code === 41 || code === 91 || code === 93 || code === 123 || code === 125 ); } function isOpeningBracket(char: string): boolean { return char === "(" || char === "[" || char === "{"; } function getSkipContextEnd( tree: ReturnType, pos: number, ): number { let node: SyntaxNode | null = tree.resolveInner(pos, 1); while (node) { if (SKIP_CONTEXTS.has(node.name)) return node.to; node = node.parent; } return -1; } function getContextChainSignature( tree: ReturnType, pos: number, ): string { if (tree.length <= 0) return ""; const clampedPos = Math.max(0, Math.min(tree.length - 1, pos)); let node: SyntaxNode | null = tree.resolveInner(clampedPos, 1); const parts: string[] = []; for (let depth = 0; node && depth < CONTEXT_SIGNATURE_DEPTH; depth++) { parts.push(node.name); node = node.parent; } return parts.join(">"); } function getBlockContextSignature( tree: ReturnType, blockStart: number, blockEnd: number, ): string { if (blockEnd <= blockStart) return ""; const endPos = Math.max(blockStart, blockEnd - 1); return `${getContextChainSignature(tree, blockStart)}|${getContextChainSignature(tree, endPos)}`; } function getBlockCacheKey( blockText: string, initialSkipChars: number, contextSignature: string, ): string { return `${initialSkipChars}\u0000${contextSignature}\u0000${blockText}`; } function tokenizeBlock( tree: ReturnType, blockText: string, blockStart: number, initialSkipChars: number, ): BlockCacheEntry { const tokens: BracketToken[] = []; let skipUntilOffset = Math.max(0, initialSkipChars); if (!blockText.length) { return { carrySkipChars: skipUntilOffset, tokens }; } if (skipUntilOffset >= blockText.length) { return { carrySkipChars: skipUntilOffset - blockText.length, tokens }; } for (let offset = 0; offset < blockText.length; offset++) { if (offset < skipUntilOffset) continue; const code = blockText.charCodeAt(offset); if (!isBracketCode(code)) continue; const pos = blockStart + offset; const skipContextEnd = getSkipContextEnd(tree, pos); if (skipContextEnd > pos) { skipUntilOffset = Math.max(skipUntilOffset, skipContextEnd - blockStart); continue; } tokens.push({ offset, char: blockText[offset] }); } return { carrySkipChars: Math.max(0, skipUntilOffset - blockText.length), tokens, }; } function isVisiblePosition( pos: number, ranges: readonly { from: number; to: number }[], cursor: { index: number }, ): boolean { while (cursor.index < ranges.length && pos >= ranges[cursor.index].to) { cursor.index++; } const range = ranges[cursor.index]; return !!range && pos >= range.from && pos < range.to; } function buildTheme(colors: readonly string[]) { const themeSpec: Record = {}; colors.forEach((color, index) => { const selector = `.cm-rainbowBracket-${index}`; themeSpec[selector] = { color: `${color} !important` }; themeSpec[`${selector} span`] = { color: `${color} !important` }; }); return EditorView.baseTheme(themeSpec); } export function getRainbowBracketColors( themeConfig: RainbowBracketThemeConfig = {}, ): string[] { const fallback = themeConfig.dark ? DEFAULT_DARK_COLORS : DEFAULT_LIGHT_COLORS; const colors: string[] = []; const seen = new Set(); for (const candidate of [ themeConfig.keyword, themeConfig.type, themeConfig.class, themeConfig.function, themeConfig.string, themeConfig.number, themeConfig.constant, themeConfig.variable, themeConfig.foreground, ]) { const color = normalizeHexColor(candidate); if (!color || seen.has(color)) continue; seen.add(color); colors.push(color); if (colors.length === fallback.length) break; } if (colors.length < 4) { return [...fallback]; } for (const fallbackColor of fallback) { if (colors.length === fallback.length) break; if (seen.has(fallbackColor)) continue; colors.push(fallbackColor); } return colors; } export function rainbowBrackets(options: RainbowBracketsOptions = {}) { const colors = options.colors != null && options.colors.length > 0 ? [...options.colors] : getRainbowBracketColors(); const exactScanLimit = Math.max( MIN_LOOK_BEHIND, Math.floor(options.exactScanLimit || DEFAULT_EXACT_SCAN_LIMIT), ); const lookBehind = clampLookBehind(options.lookBehind); const theme = buildTheme(colors); const marks = colors.map((_, index) => Decoration.mark({ class: `cm-rainbowBracket-${index}` }), ); const rainbowBracketsPlugin = ViewPlugin.fromClass( class { decorations: DecorationSet; blockCache = new Map(); raf = 0; pendingView: EditorView | null = null; constructor(view: EditorView) { this.decorations = this.buildDecorations(view); } update(update: ViewUpdate) { if (!update.docChanged && !update.viewportChanged) return; this.scheduleBuild(update.view); } scheduleBuild(view: EditorView): void { this.pendingView = view; if (this.raf) return; // Bracket recoloring is cosmetic. Collapse bursts of edits/scroll // events into a single frame so large pastes don't block repeatedly. this.raf = requestAnimationFrame(() => { this.raf = 0; const pendingView = this.pendingView; this.pendingView = null; if (!pendingView) return; this.decorations = this.buildDecorations(pendingView); }); } buildDecorations(view: EditorView): DecorationSet { const visibleRanges = view.visibleRanges; if (!visibleRanges.length || !marks.length) return Decoration.none; const tree = syntaxTree(view.state); const scanStart = alignToBlockStart( getScanStart(view, lookBehind, exactScanLimit), ); const scanEnd = visibleRanges[visibleRanges.length - 1].to; const visibleCursor = { index: 0 }; const openBrackets: BracketInfo[] = []; let carrySkipChars = 0; const builder = new RangeSetBuilder(); for ( let blockStart = scanStart; blockStart < scanEnd; blockStart += BLOCK_SIZE ) { const blockEnd = Math.min(scanEnd, blockStart + BLOCK_SIZE); const blockText = view.state.doc.sliceString(blockStart, blockEnd); const cacheKey = getBlockCacheKey( blockText, carrySkipChars, getBlockContextSignature(tree, blockStart, blockEnd), ); let cachedBlock = this.getCachedBlock(cacheKey); if (!cachedBlock) { cachedBlock = tokenizeBlock( tree, blockText, blockStart, carrySkipChars, ); this.setCachedBlock(cacheKey, cachedBlock); } for (const token of cachedBlock.tokens) { const pos = blockStart + token.offset; if (isOpeningBracket(token.char)) { const colorIndex = openBrackets.length % marks.length; if (isVisiblePosition(pos, visibleRanges, visibleCursor)) { builder.add(pos, pos + 1, marks[colorIndex]); } openBrackets.push({ char: token.char, colorIndex }); continue; } const matchingOpen = CLOSING_TO_OPENING[token.char as ClosingBracket]; if (!matchingOpen) continue; for (let index = openBrackets.length - 1; index >= 0; index--) { if (openBrackets[index].char !== matchingOpen) continue; if (isVisiblePosition(pos, visibleRanges, visibleCursor)) { builder.add( pos, pos + 1, marks[openBrackets[index].colorIndex], ); } openBrackets.length = index; break; } } carrySkipChars = cachedBlock.carrySkipChars; } return builder.finish(); } getCachedBlock(key: string): BlockCacheEntry | null { const cached = this.blockCache.get(key); if (!cached) return null; this.blockCache.delete(key); this.blockCache.set(key, cached); return cached; } setCachedBlock(key: string, value: BlockCacheEntry): void { if (this.blockCache.has(key)) { this.blockCache.delete(key); } this.blockCache.set(key, value); if (this.blockCache.size <= MAX_BLOCK_CACHE_ENTRIES) return; const oldestKey = this.blockCache.keys().next().value; if (oldestKey !== undefined) { this.blockCache.delete(oldestKey); } } destroy(): void { if (this.raf) { cancelAnimationFrame(this.raf); this.raf = 0; } this.pendingView = null; this.blockCache.clear(); } }, { decorations: (value) => value.decorations, }, ); return [rainbowBracketsPlugin, theme]; } export default rainbowBrackets; ================================================ FILE: src/cm/supportedModes.ts ================================================ import { languages } from "@codemirror/language-data"; import type { Extension } from "@codemirror/state"; import { addMode } from "./modelist"; type FilenameMatcher = string | RegExp; interface LanguageDescription { name?: string; alias?: readonly string[]; extensions?: readonly string[]; filenames?: readonly FilenameMatcher[]; filename?: FilenameMatcher; load?: () => Promise; } function normalizeModeKey(value: string): string { return String(value ?? "") .trim() .toLowerCase(); } function isSafeModeId(value: string): boolean { return /^[a-z0-9][a-z0-9._-]*$/.test(value); } function slugifyModeId(value: string): string { return normalizeModeKey(value) .replace(/\+\+/g, "pp") .replace(/#/g, "sharp") .replace(/&/g, "and") .replace(/[^a-z0-9._-]+/g, "-") .replace(/^-+|-+$/g, ""); } function collectAliases( name: string, aliases: readonly string[] | undefined, ): string[] { return [ ...new Set( [name, ...(aliases || [])].map(normalizeModeKey).filter(Boolean), ), ]; } function getModeId(name: string, aliases: string[]): string { const normalizedName = normalizeModeKey(name); if (isSafeModeId(normalizedName)) return normalizedName; const safeAlias = aliases.find( (alias) => alias !== normalizedName && isSafeModeId(alias), ); return safeAlias || slugifyModeId(name) || normalizedName || "text"; } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // 1) Always register a plain text fallback addMode("Text", "txt|text|log|plain", "Plain Text", () => []); // 2) Register all languages provided by @codemirror/language-data // We convert extensions like [".js", ".mjs"] into a modelist pattern: "js|mjs" // and preserve aliases and filename regexes for languages like C++ and Dockerfile. for (const lang of languages as readonly LanguageDescription[]) { try { const name = String(lang?.name || "").trim(); if (!name) continue; const aliases = collectAliases(name, lang.alias); const modeId = getModeId(name, aliases); const parts: string[] = []; const filenameMatchers: RegExp[] = []; // File extensions if (Array.isArray(lang.extensions)) { for (const e of lang.extensions) { if (typeof e !== "string") continue; const cleaned = e.replace(/^\./, "").trim(); if (cleaned) parts.push(cleaned); } } // Exact filenames / filename regexes (Dockerfile, PKGBUILD, nginx*.conf, etc.) const filenames = Array.isArray(lang.filenames) ? lang.filenames : lang.filename ? [lang.filename] : []; for (const fn of filenames) { if (typeof fn === "string") { const cleaned = fn.trim(); if (cleaned) { filenameMatchers.push(new RegExp(`^${escapeRegExp(cleaned)}$`, "i")); } continue; } if (fn instanceof RegExp) { filenameMatchers.push(new RegExp(fn.source, fn.flags)); } } const pattern = parts.join("|"); // Wrap language-data loader as our modelist language provider // lang.load() returns a Promise; we let the editor handle async loading const loader = typeof lang.load === "function" ? () => lang.load!() : null; addMode(modeId, pattern, name, loader, { aliases, filenameMatchers, }); } catch (_) { // Ignore faulty entries to avoid breaking the whole registration } } // Luau isn't bundled in @codemirror/language-data, so register it explicitly. addMode("Luau", "luau", "Luau", async () => { const { luau } = await import("./modes/luau"); return luau(); }); ================================================ FILE: src/cm/themes/aura.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; // Aura theme configuration and extensions export const config = { name: "aura", dark: true, background: "#21202e", foreground: "#edecee", selection: "#3d375e7f", cursor: "#a277ff", dropdownBackground: "#21202e", dropdownBorder: "#3b334b", activeLine: "#4d4b6622", lineNumber: "#a394f033", lineNumberActive: "#cdccce", matchingBracket: "#a394f033", keyword: "#a277ff", storage: "#a277ff", variable: "#edecee", parameter: "#edecee", function: "#ffca85", string: "#61ffca", constant: "#61ffca", type: "#82e2ff", class: "#82e2ff", number: "#61ffca", comment: "#6d6d6d", heading: "#a277ff", invalid: "#ff6767", regexp: "#61ffca", }; export const auraTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection, }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const auraHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function aura() { return [auraTheme, syntaxHighlighting(auraHighlightStyle)]; } export default aura; ================================================ FILE: src/cm/themes/dracula.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView, lineNumbers } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "dracula", dark: true, background: "#282A36", foreground: "#F8F8F2", selection: "#44475A", cursor: "#F8F8F2", dropdownBackground: "#282A36", dropdownBorder: "#191A21", activeLine: "#53576c22", lineNumber: "#6272A4", lineNumberActive: "#F8F8F2", matchingBracket: "#44475A", keyword: "#FF79C6", storage: "#FF79C6", variable: "#F8F8F2", parameter: "#F8F8F2", function: "#50FA7B", string: "#F1FA8C", constant: "#BD93F9", type: "#8BE9FD", class: "#8BE9FD", number: "#BD93F9", comment: "#6272A4", heading: "#BD93F9", invalid: "#FF5555", regexp: "#F1FA8C", }; export const draculaTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const draculaHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type, fontStyle: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function dracula() { return [draculaTheme, syntaxHighlighting(draculaHighlightStyle)]; } export default dracula; ================================================ FILE: src/cm/themes/githubDark.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView, lineNumbers } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "githubDark", dark: true, background: "#24292e", foreground: "#d1d5da", selection: "#3392FF44", cursor: "#c8e1ff", dropdownBackground: "#24292e", dropdownBorder: "#1b1f23", activeLine: "#4d566022", lineNumber: "#444d56", lineNumberActive: "#e1e4e8", matchingBracket: "#17E5E650", keyword: "#f97583", storage: "#f97583", variable: "#ffab70", parameter: "#e1e4e8", function: "#79b8ff", string: "#9ecbff", constant: "#79b8ff", type: "#79b8ff", class: "#b392f0", number: "#79b8ff", comment: "#6a737d", heading: "#79b8ff", invalid: "#f97583", regexp: "#9ecbff", }; export const githubDarkTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const githubDarkHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type, fontStyle: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function githubDark() { return [githubDarkTheme, syntaxHighlighting(githubDarkHighlightStyle)]; } export default githubDark; ================================================ FILE: src/cm/themes/githubLight.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView, lineNumbers } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "githubLight", dark: false, background: "#fff", foreground: "#444d56", selection: "#0366d625", cursor: "#044289", dropdownBackground: "#fff", dropdownBorder: "#e1e4e8", activeLine: "#c6c6c622", lineNumber: "#1b1f234d", lineNumberActive: "#24292e", matchingBracket: "#34d05840", keyword: "#d73a49", storage: "#d73a49", variable: "#e36209", parameter: "#24292e", function: "#005cc5", string: "#032f62", constant: "#005cc5", type: "#005cc5", class: "#6f42c1", number: "#005cc5", comment: "#6a737d", heading: "#005cc5", invalid: "#cb2431", regexp: "#032f62", }; export const githubLightTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const githubLightHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type, fontStyle: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function githubLight() { return [githubLightTheme, syntaxHighlighting(githubLightHighlightStyle)]; } export default githubLight; ================================================ FILE: src/cm/themes/index.js ================================================ import { EditorState } from "@codemirror/state"; import { oneDark } from "@codemirror/theme-one-dark"; import aura, { config as auraConfig } from "./aura"; import dracula, { config as draculaConfig } from "./dracula"; import githubDark, { config as githubDarkConfig } from "./githubDark"; import githubLight, { config as githubLightConfig } from "./githubLight"; import monokai, { config as monokaiConfig } from "./monokai"; import noctisLilac, { config as noctisLilacConfig } from "./noctisLilac"; import solarizedDark, { config as solarizedDarkConfig } from "./solarizedDark"; import solarizedLight, { config as solarizedLightConfig, } from "./solarizedLight"; import tokyoNight, { config as tokyoNightConfig } from "./tokyoNight"; import tokyoNightDay, { config as tokyoNightDayConfig } from "./tokyoNightDay"; import tomorrowNight, { config as tomorrowNightConfig } from "./tomorrowNight"; import tomorrowNightBright, { config as tomorrowNightBrightConfig, } from "./tomorrowNightBright"; import vscodeDark, { config as vscodeDarkConfig } from "./vscodeDark"; const oneDarkConfig = { name: "one_dark", dark: true, background: "#282c34", foreground: "#abb2bf", keyword: "#c678dd", string: "#98c379", number: "#d19a66", comment: "#5c6370", function: "#61afef", variable: "#e06c75", type: "#e5c07b", class: "#e5c07b", constant: "#d19a66", operator: "#56b6c2", invalid: "#ff6b6b", }; const themes = new Map(); const warnedInvalidThemes = new Set(); function normalizeExtensions(value, target = []) { if (Array.isArray(value)) { value.forEach((item) => normalizeExtensions(item, target)); return target; } if (value !== null && value !== undefined) { target.push(value); } return target; } function toExtensionGetter(getExtension) { if (typeof getExtension === "function") { return () => normalizeExtensions(getExtension()); } return () => normalizeExtensions(getExtension); } function logInvalidThemeOnce(themeId, error, reason = "") { if (warnedInvalidThemes.has(themeId)) return; warnedInvalidThemes.add(themeId); const message = reason ? `[editorThemes] Theme '${themeId}' is invalid: ${reason}` : `[editorThemes] Theme '${themeId}' is invalid.`; console.error(message, error); } function validateThemeExtensions(themeId, extensions) { if (!extensions.length) { logInvalidThemeOnce(themeId, null, "no extensions were returned"); return false; } try { // Validate against Acode's own CodeMirror instance. EditorState.create({ doc: "", extensions }); return true; } catch (error) { logInvalidThemeOnce(themeId, error); return false; } } function resolveThemeEntryExtensions(theme, fallbackExtensions) { const fallback = fallbackExtensions.length ? [...fallbackExtensions] : [oneDark]; if (!theme) return fallback; try { const resolved = normalizeExtensions(theme.getExtension?.()); if (!validateThemeExtensions(theme.id, resolved)) { return fallback; } return resolved; } catch (error) { logInvalidThemeOnce(theme.id, error); return fallback; } } export function addTheme(id, caption, isDark, getExtension, config = null) { const key = String(id || "") .trim() .toLowerCase(); if (!key || themes.has(key)) return false; const theme = { id: key, caption: caption || id, isDark: !!isDark, getExtension: toExtensionGetter(getExtension), config: config || null, }; if (!validateThemeExtensions(key, theme.getExtension())) { return false; } themes.set(key, theme); return true; } export function getThemes() { return Array.from(themes.values()); } export function getThemeById(id) { if (!id) return null; return themes.get(String(id).toLowerCase()) || null; } export function getThemeConfig(id) { if (!id) return oneDarkConfig; const theme = themes.get(String(id).toLowerCase()); return theme?.config || oneDarkConfig; } export function getThemeExtensions(id, fallback = [oneDark]) { const fallbackExtensions = normalizeExtensions(fallback); const theme = getThemeById(id) || getThemeById(String(id || "").replace(/-/g, "_")); return resolveThemeEntryExtensions(theme, fallbackExtensions); } export function removeTheme(id) { if (!id) return; themes.delete(String(id).toLowerCase()); } addTheme("one_dark", "One Dark", true, () => [oneDark], oneDarkConfig); addTheme(auraConfig.name, "Aura", !!auraConfig.dark, () => aura(), auraConfig); addTheme( noctisLilacConfig.name, noctisLilacConfig.caption || "Noctis Lilac", !!noctisLilacConfig.dark, () => noctisLilac(), noctisLilacConfig, ); addTheme( draculaConfig.name, "Dracula", !!draculaConfig.dark, () => dracula(), draculaConfig, ); addTheme( githubDarkConfig.name, "GitHub Dark", !!githubDarkConfig.dark, () => githubDark(), githubDarkConfig, ); addTheme( githubLightConfig.name, "GitHub Light", !!githubLightConfig.dark, () => githubLight(), githubLightConfig, ); addTheme( solarizedDarkConfig.name, "Solarized Dark", !!solarizedDarkConfig.dark, () => solarizedDark(), solarizedDarkConfig, ); addTheme( solarizedLightConfig.name, "Solarized Light", !!solarizedLightConfig.dark, () => solarizedLight(), solarizedLightConfig, ); addTheme( tokyoNightDayConfig.name, "Tokyo Night Day", !!tokyoNightDayConfig.dark, () => tokyoNightDay(), tokyoNightDayConfig, ); addTheme( tokyoNightConfig.name, "Tokyo Night", !!tokyoNightConfig.dark, () => tokyoNight(), tokyoNightConfig, ); addTheme( tomorrowNightConfig.name, "Tomorrow Night", !!tomorrowNightConfig.dark, () => tomorrowNight(), tomorrowNightConfig, ); addTheme( tomorrowNightBrightConfig.name, "Tomorrow Night Bright", !!tomorrowNightBrightConfig.dark, () => tomorrowNightBright(), tomorrowNightBrightConfig, ); addTheme( monokaiConfig.name, "Monokai", !!monokaiConfig.dark, () => monokai(), monokaiConfig, ); addTheme( vscodeDarkConfig.name, "VS Code Dark", !!vscodeDarkConfig.dark, () => vscodeDark(), vscodeDarkConfig, ); export default { getThemes, getThemeById, getThemeConfig, getThemeExtensions, addTheme, removeTheme, }; ================================================ FILE: src/cm/themes/monokai.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "monokai", dark: true, background: "#272822", foreground: "#f8f8f2", selection: "#4a4a76", cursor: "#f8f8f0", dropdownBackground: "#414339", dropdownBorder: "#3e3d32", activeLine: "#3e3d3257", lineNumber: "#f8f8f270", lineNumberActive: "#f8f8f2", matchingBracket: "#3e3d32", keyword: "#F92672", storage: "#F92672", variable: "#FD971F", parameter: "#FD971F", function: "#66D9EF", string: "#E6DB74", constant: "#AE81FF", type: "#66D9EF", class: "#A6E22E", number: "#AE81FF", comment: "#88846f", heading: "#A6E22E", invalid: "#F44747", regexp: "#E6DB74", tag: "#F92672", }; export const monokaiTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection, }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: `1px solid ${config.dropdownBorder}`, }, ".cm-panels.cm-panels-bottom": { borderTop: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const monokaiHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.tagName, color: config.tag }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function monokai() { return [monokaiTheme, syntaxHighlighting(monokaiHighlightStyle)]; } export default monokai; ================================================ FILE: src/cm/themes/noctisLilac.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "noctisLilac", dark: false, background: "#f2f1f8", foreground: "#0c006b", selection: "#d5d1f2", cursor: "#5c49e9", dropdownBackground: "#f2f1f8", dropdownBorder: "#e1def3", activeLine: "#e1def3", lineNumber: "#0c006b70", lineNumberActive: "#0c006b", matchingBracket: "#d5d1f2", keyword: "#ff5792", storage: "#ff5792", variable: "#0c006b", parameter: "#0c006b", function: "#0095a8", string: "#00b368", constant: "#5842ff", type: "#b3694d", class: "#0094f0", number: "#5842ff", comment: "#9995b7", heading: "#0094f0", invalid: "#ff5792", regexp: "#00b368", }; export const noctisLilacTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection, }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const noctisLilacHighlightStyle = HighlightStyle.define([ { tag: t.comment, color: config.comment }, { tag: t.keyword, color: config.keyword, fontWeight: "bold" }, { tag: [t.definitionKeyword, t.modifier], color: config.keyword }, { tag: [t.className, t.tagName, t.definition(t.typeName)], color: config.class, }, { tag: [t.number, t.bool, t.null, t.special(t.brace)], color: config.number }, { tag: [t.definition(t.propertyName), t.function(t.variableName)], color: config.function, }, { tag: t.typeName, color: config.type }, { tag: [t.propertyName, t.variableName], color: "#fa8900" }, { tag: t.operator, color: config.keyword }, { tag: t.self, color: "#e64100" }, { tag: [t.string, t.regexp], color: config.string }, { tag: [t.paren, t.bracket], color: "#0431fa" }, { tag: t.labelName, color: "#00bdd6" }, { tag: t.attributeName, color: "#e64100" }, { tag: t.angleBracket, color: config.comment }, ]); export function noctisLilac() { return [noctisLilacTheme, syntaxHighlighting(noctisLilacHighlightStyle)]; } export default noctisLilac; ================================================ FILE: src/cm/themes/solarizedDark.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView, lineNumbers } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "solarizedDark", dark: true, background: "#002B36", foreground: "#93A1A1", selection: "#274642", cursor: "#D30102", dropdownBackground: "#002B36", dropdownBorder: "#2AA19899", activeLine: "#005b7022", lineNumber: "#93A1A1", lineNumberActive: "#949494", matchingBracket: "#073642", keyword: "#859900", storage: "#93A1A1", variable: "#268BD2", parameter: "#268BD2", function: "#268BD2", string: "#2AA198", constant: "#CB4B16", type: "#CB4B16", class: "#CB4B16", number: "#D33682", comment: "#586E75", heading: "#268BD2", invalid: "#DC322F", regexp: "#DC322F", }; export const solarizedDarkTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const solarizedDarkHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type, fontStyle: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function solarizedDark() { return [solarizedDarkTheme, syntaxHighlighting(solarizedDarkHighlightStyle)]; } export default solarizedDark; ================================================ FILE: src/cm/themes/solarizedLight.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView, lineNumbers } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "solarizedLight", dark: false, background: "#FDF6E3", foreground: "#586E75", selection: "#EEE8D5", cursor: "#657B83", dropdownBackground: "#FDF6E3", dropdownBorder: "#D3AF86", activeLine: "#d5bd5c22", lineNumber: "#586E75", lineNumberActive: "#567983", matchingBracket: "#EEE8D5", keyword: "#859900", storage: "#586E75", variable: "#268BD2", parameter: "#268BD2", function: "#268BD2", string: "#2AA198", constant: "#CB4B16", type: "#CB4B16", class: "#CB4B16", number: "#D33682", comment: "#93A1A1", heading: "#268BD2", invalid: "#DC322F", regexp: "#DC322F", }; export const solarizedLightTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const solarizedLightHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type, fontStyle: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function solarizedLight() { return [ solarizedLightTheme, syntaxHighlighting(solarizedLightHighlightStyle), ]; } export default solarizedLight; ================================================ FILE: src/cm/themes/tokyoNight.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView, lineNumbers } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "tokyoNight", dark: true, background: "#1a1b26", foreground: "#787c99", selection: "#515c7e40", cursor: "#c0caf5", dropdownBackground: "#1a1b26", dropdownBorder: "#787c99", activeLine: "#43455c22", lineNumber: "#363b54", lineNumberActive: "#737aa2", matchingBracket: "#16161e", keyword: "#bb9af7", storage: "#bb9af7", variable: "#c0caf5", parameter: "#c0caf5", function: "#7aa2f7", string: "#9ece6a", constant: "#bb9af7", type: "#0db9d7", class: "#c0caf5", number: "#ff9e64", comment: "#444b6a", heading: "#89ddff", invalid: "#ff5370", regexp: "#b4f9f8", }; export const tokyoNightTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const tokyoNightHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type, fontStyle: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function tokyoNight() { return [tokyoNightTheme, syntaxHighlighting(tokyoNightHighlightStyle)]; } export default tokyoNight; ================================================ FILE: src/cm/themes/tokyoNightDay.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView, lineNumbers } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "tokyoNightDay", dark: false, background: "#e1e2e7", foreground: "#6a6f8e", selection: "#8591b840", cursor: "#3760bf", dropdownBackground: "#e1e2e7", dropdownBorder: "#6a6f8e", activeLine: "#a7aaba22", lineNumber: "#b3b6cd", lineNumberActive: "#68709a", matchingBracket: "#e9e9ec", keyword: "#9854f1", storage: "#9854f1", variable: "#3760bf", parameter: "#3760bf", function: "#2e7de9", string: "#587539", constant: "#9854f1", type: "#07879d", class: "#3760bf", number: "#b15c00", comment: "#9da3c2", heading: "#006a83", invalid: "#ff3e64", regexp: "#2e5857", }; export const tokyoNightDayTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const tokyoNightDayHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type, fontStyle: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.keyword }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function tokyoNightDay() { return [tokyoNightDayTheme, syntaxHighlighting(tokyoNightDayHighlightStyle)]; } export default tokyoNightDay; ================================================ FILE: src/cm/themes/tomorrowNight.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; // Palette adapted from Tomorrow Night (Chris Kempson) export const config = { name: "tomorrowNight", dark: true, background: "#1D1F21", foreground: "#C5C8C6", selection: "#373B41", cursor: "#AEAFAD", dropdownBackground: "#1D1F21", dropdownBorder: "#4B4E55", activeLine: "#282A2E", lineNumber: "#4B4E55", lineNumberActive: "#C5C8C6", matchingBracket: "#282A2E", keyword: "#B294BB", storage: "#B294BB", variable: "#CC6666", parameter: "#DE935F", function: "#81A2BE", string: "#B5BD68", constant: "#DE935F", type: "#F0C674", class: "#F0C674", number: "#DE935F", comment: "#969896", heading: "#81A2BE", invalid: "#DF5F5F", regexp: "#CC6666", operator: "#8ABEB7", tag: "#CC6666", }; export const tomorrowNightTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: `1px solid ${config.dropdownBorder}`, }, ".cm-panels.cm-panels-bottom": { borderTop: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const tomorrowNightHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.operator }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.tagName, color: config.tag }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function tomorrowNight() { return [tomorrowNightTheme, syntaxHighlighting(tomorrowNightHighlightStyle)]; } export default tomorrowNight; ================================================ FILE: src/cm/themes/tomorrowNightBright.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; // Palette adapted from Tomorrow Night Bright (Chris Kempson) export const config = { name: "tomorrowNightBright", dark: true, background: "#000000", foreground: "#DEDEDE", selection: "#424242", cursor: "#9F9F9F", dropdownBackground: "#000000", dropdownBorder: "#343434", activeLine: "#2A2A2A", lineNumber: "#343434", lineNumberActive: "#DEDEDE", matchingBracket: "#2A2A2A", keyword: "#C397D8", storage: "#C397D8", variable: "#D54E53", parameter: "#E78C45", function: "#7AA6DA", string: "#B9CA4A", constant: "#E78C45", type: "#E7C547", class: "#E7C547", number: "#E78C45", comment: "#969896", heading: "#7AA6DA", invalid: "#DF5F5F", regexp: "#D54E53", operator: "#70C0B1", tag: "#D54E53", }; export const tomorrowNightBrightTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: `1px solid ${config.dropdownBorder}`, }, ".cm-panels.cm-panels-bottom": { borderTop: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selection, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selection }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.foreground, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selection, color: config.foreground, }, }, }, { dark: config.dark }, ); export const tomorrowNightBrightHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: config.keyword }, { tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable, }, { tag: [t.propertyName], color: config.function }, { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string, }, { tag: [t.function(t.variableName), t.labelName], color: config.function }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant, }, { tag: [t.definition(t.name), t.separator], color: config.variable }, { tag: [t.className], color: config.class }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number, }, { tag: [t.typeName], color: config.type }, { tag: [t.operator, t.operatorKeyword], color: config.operator }, { tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.tagName, color: config.tag }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.link, textDecoration: "underline" }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable }, { tag: t.invalid, color: config.invalid }, { tag: t.strikethrough, textDecoration: "line-through" }, ]); export function tomorrowNightBright() { return [ tomorrowNightBrightTheme, syntaxHighlighting(tomorrowNightBrightHighlightStyle), ]; } export default tomorrowNightBright; ================================================ FILE: src/cm/themes/vscodeDark.js ================================================ import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; import { EditorView } from "@codemirror/view"; import { tags as t } from "@lezer/highlight"; export const config = { name: "vscodeDark", dark: true, background: "#1e1e1e", foreground: "#9cdcfe", selection: "#6199ff2f", selectionMatch: "#72a1ff59", cursor: "#c6c6c6", dropdownBackground: "#1e1e1e", dropdownBorder: "#3c3c3c", activeLine: "#ffffff0f", lineNumber: "#838383", lineNumberActive: "#ffffff", matchingBracket: "#515c6a", keyword: "#569cd6", variable: "#9cdcfe", parameter: "#9cdcfe", function: "#dcdcaa", string: "#ce9178", constant: "#569cd6", type: "#4ec9b0", class: "#4ec9b0", number: "#b5cea8", comment: "#6a9955", heading: "#9cdcfe", invalid: "#ff0000", regexp: "#d16969", tag: "#4ec9b0", operator: "#d4d4d4", angleBracket: "#808080", }; export const vscodeDarkTheme = EditorView.theme( { "&": { color: config.foreground, backgroundColor: config.background, }, ".cm-content": { caretColor: config.cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: config.cursor }, "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: config.selection, }, ".cm-panels": { backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-panels.cm-panels-top": { borderBottom: `1px solid ${config.dropdownBorder}`, }, ".cm-panels.cm-panels-bottom": { borderTop: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch": { backgroundColor: config.dropdownBackground, outline: `1px solid ${config.dropdownBorder}`, }, ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: config.selectionMatch, }, ".cm-activeLine": { backgroundColor: config.activeLine }, ".cm-selectionMatch": { backgroundColor: config.selectionMatch }, "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { backgroundColor: config.matchingBracket, outline: "none", }, ".cm-gutters": { backgroundColor: config.background, color: config.lineNumber, border: "none", }, ".cm-activeLineGutter": { backgroundColor: config.background }, ".cm-lineNumbers .cm-gutterElement": { color: config.lineNumber }, ".cm-lineNumbers .cm-activeLineGutter": { color: config.lineNumberActive }, ".cm-foldPlaceholder": { backgroundColor: "transparent", border: "none", color: config.foreground, }, ".cm-tooltip": { border: `1px solid ${config.dropdownBorder}`, backgroundColor: config.dropdownBackground, color: config.foreground, }, ".cm-tooltip .cm-tooltip-arrow:before": { borderTopColor: "transparent", borderBottomColor: "transparent", }, ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: config.foreground, borderBottomColor: config.foreground, }, ".cm-tooltip-autocomplete": { "& > ul > li[aria-selected]": { background: config.selectionMatch, color: config.foreground, }, }, }, { dark: config.dark }, ); export const vscodeDarkHighlightStyle = HighlightStyle.define([ { tag: [ t.keyword, t.operatorKeyword, t.modifier, t.color, t.constant(t.name), t.standard(t.name), t.standard(t.tagName), t.special(t.brace), t.atom, t.bool, t.special(t.variableName), ], color: config.keyword, }, { tag: [t.controlKeyword, t.moduleKeyword], color: "#c586c0" }, { tag: [ t.name, t.deleted, t.character, t.macroName, t.propertyName, t.variableName, t.labelName, t.definition(t.name), ], color: config.variable, }, { tag: t.heading, fontWeight: "bold", color: config.heading }, { tag: [ t.typeName, t.className, t.tagName, t.number, t.changed, t.annotation, t.self, t.namespace, ], color: config.type, }, { tag: [t.function(t.variableName), t.function(t.propertyName)], color: config.function, }, { tag: [t.number], color: config.number }, { tag: [t.operator, t.punctuation, t.separator, t.url, t.escape, t.regexp], color: config.operator, }, { tag: [t.regexp], color: config.regexp }, { tag: [t.special(t.string), t.processingInstruction, t.string, t.inserted], color: config.string, }, { tag: [t.angleBracket], color: config.angleBracket }, { tag: t.strong, fontWeight: "bold" }, { tag: t.emphasis, fontStyle: "italic" }, { tag: t.strikethrough, textDecoration: "line-through" }, { tag: [t.meta, t.comment], color: config.comment }, { tag: t.link, color: config.comment, textDecoration: "underline" }, { tag: t.invalid, color: config.invalid }, ]); export function vscodeDark() { return [vscodeDarkTheme, syntaxHighlighting(vscodeDarkHighlightStyle)]; } export default vscodeDark; ================================================ FILE: src/cm/touchSelectionMenu.js ================================================ import { EditorSelection } from "@codemirror/state"; import selectionMenu from "lib/selectionMenu"; const TAP_MAX_DELAY = 500; const TAP_MAX_DISTANCE = 20; const EDGE_SCROLL_GAP = 40; const MENU_MARGIN = 10; const MENU_SHOW_DELAY = 120; const MENU_CARET_GAP = 10; const MENU_SELECTION_GAP = 12; const MENU_HANDLE_CLEARANCE = 28; const TAP_MAX_COLUMN_DELTA = 2; const TAP_MAX_POS_DELTA = 2; /** * Classify taps into single/double/triple tap buckets. * @param {{x:number,y:number,time:number,count:number}|null} previousTap * @param {{x:number,y:number,time:number}} tap * @returns {{x:number,y:number,time:number,count:number}} */ export function classifyTap(previousTap, tap) { if (!previousTap) { return { ...tap, count: 1 }; } const dt = tap.time - previousTap.time; const dx = tap.x - previousTap.x; const dy = tap.y - previousTap.y; const distance = Math.hypot(dx, dy); const sameTextZone = tap.line != null && previousTap.line != null && tap.line === previousTap.line && Math.abs((tap.column ?? 0) - (previousTap.column ?? 0)) <= TAP_MAX_COLUMN_DELTA; const nearSamePos = tap.pos != null && previousTap.pos != null && Math.abs(tap.pos - previousTap.pos) <= TAP_MAX_POS_DELTA; if ( dt <= TAP_MAX_DELAY && (distance <= TAP_MAX_DISTANCE || sameTextZone || nearSamePos) ) { return { ...tap, count: Math.min(previousTap.count + 1, 3), }; } return { ...tap, count: 1 }; } /** * Clamp menu coordinates so it stays within the container bounds. * @param {{left:number, top:number, width:number, height:number}} menuRect * @param {{left:number, top:number, width:number, height:number}} containerRect * @returns {{left:number, top:number}} */ export function clampMenuPosition(menuRect, containerRect) { const maxLeft = Math.max( containerRect.left + MENU_MARGIN, containerRect.left + containerRect.width - menuRect.width - MENU_MARGIN, ); const maxTop = Math.max( containerRect.top + MENU_MARGIN, containerRect.top + containerRect.height - menuRect.height - MENU_MARGIN, ); return { left: clamp(menuRect.left, containerRect.left + MENU_MARGIN, maxLeft), top: clamp(menuRect.top, containerRect.top + MENU_MARGIN, maxTop), }; } /** * Filter menu items using Ace-compatible rules. * @param {ReturnType} items * @param {{readOnly:boolean,hasSelection:boolean}} options */ export function filterSelectionMenuItems(items, options) { const { readOnly, hasSelection } = options; return items.filter((item) => { if (readOnly && !item.readOnly) return false; if (hasSelection && !["selected", "all"].includes(item.mode)) return false; if (!hasSelection && item.mode === "selected") return false; return true; }); } /** * Detect which edge(s) should trigger drag auto-scroll. * @param {{ * x:number, * y:number, * rect:{left:number,right:number,top:number,bottom:number}, * allowHorizontal?:boolean, * gap?:number, * }} options * @returns {{horizontal:number, vertical:number}} */ export function getEdgeScrollDirections(options) { const { x, y, rect, allowHorizontal = true, gap = EDGE_SCROLL_GAP } = options; let horizontal = 0; let vertical = 0; if (allowHorizontal) { if (x < rect.left + gap) horizontal = -1; else if (x > rect.right - gap) horizontal = 1; } if (y < rect.top + gap) vertical = -1; else if (y > rect.bottom - gap) vertical = 1; return { horizontal, vertical }; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } export default function createTouchSelectionMenu(view, options = {}) { return new TouchSelectionMenuController(view, options); } class TouchSelectionMenuController { #view; #container; #getActiveFile; #isShiftSelectionActive; #stateSyncRaf = 0; #isScrolling = false; #isPointerInteracting = false; #shiftSelectionSession = null; #pendingShiftSelectionClick = null; #menuActive = false; #menuRequested = false; #enabled = true; #handlingMenuAction = false; #menuShowTimer = null; constructor(view, options = {}) { this.#view = view; this.#container = options.container || view.dom.closest(".editor-container") || view.dom; this.#getActiveFile = options.getActiveFile || (() => null); this.#isShiftSelectionActive = options.isShiftSelectionActive || (() => false); this.$menu = document.createElement("menu"); this.$menu.className = "cursor-menu"; this.#bindEvents(); } #bindEvents() { const root = this.#view.dom; root.addEventListener("contextmenu", this.#onContextMenu, true); document.addEventListener("pointerdown", this.#onGlobalPointerDown, true); document.addEventListener("pointerup", this.#onGlobalPointerUp, true); document.addEventListener("pointercancel", this.#onGlobalPointerUp, true); } destroy() { const root = this.#view.dom; root.removeEventListener("contextmenu", this.#onContextMenu, true); document.removeEventListener( "pointerdown", this.#onGlobalPointerDown, true, ); document.removeEventListener("pointerup", this.#onGlobalPointerUp, true); document.removeEventListener( "pointercancel", this.#onGlobalPointerUp, true, ); this.#clearMenuShowTimer(); cancelAnimationFrame(this.#stateSyncRaf); this.#stateSyncRaf = 0; this.#shiftSelectionSession = null; this.#pendingShiftSelectionClick = null; this.#hideMenu(true); } setEnabled(enabled) { this.#enabled = !!enabled; if (this.#enabled) return; this.#shiftSelectionSession = null; this.#pendingShiftSelectionClick = null; this.#menuRequested = false; this.#isPointerInteracting = false; this.#isScrolling = false; this.#clearMenuShowTimer(); cancelAnimationFrame(this.#stateSyncRaf); this.#stateSyncRaf = 0; this.#hideMenu(true); } setSelection(value) { if (!this.#enabled) return; if (value) { this.#menuRequested = true; } this.onStateChanged({ pointerTriggered: !!value, selectionChanged: true, }); } setMenu(value) { this.#menuRequested = !!value; if (!this.#enabled) return; if (!value) { this.#clearMenuShowTimer(); this.#hideMenu(); return; } this.#scheduleMenuShow(MENU_SHOW_DELAY); } isMenuVisible() { return this.#menuActive && this.$menu.isConnected; } onScrollStart() { if (!this.#enabled) return; if (this.#isScrolling) return; this.#clearMenuShowTimer(); this.#isScrolling = true; this.#hideMenu(); } onScrollEnd() { if (!this.#enabled || !this.#isScrolling) return; this.#isScrolling = false; if (this.#shouldShowMenu()) this.#scheduleMenuShow(MENU_SHOW_DELAY); } onStateChanged(meta = {}) { if (!this.#enabled) return; if (this.#handlingMenuAction) return; if (meta.selectionChanged && this.#menuActive) { this.#hideMenu(); } if (!this.#shouldShowMenu()) { if (!this.#hasSelection()) { this.#menuRequested = false; } this.#clearMenuShowTimer(); this.#hideMenu(); return; } const delay = meta.pointerTriggered || meta.selectionChanged ? MENU_SHOW_DELAY : 0; this.#scheduleMenuShow(delay); } onSessionChanged() { if (!this.#enabled) return; this.#shiftSelectionSession = null; this.#pendingShiftSelectionClick = null; this.#menuRequested = false; this.#isPointerInteracting = false; this.#isScrolling = false; this.#clearMenuShowTimer(); this.#hideMenu(true); } #onContextMenu = (event) => { if (!this.#enabled) return; if (this.#isIgnoredPointerTarget(event.target)) return; event.preventDefault(); event.stopPropagation(); this.#menuRequested = true; this.#scheduleMenuShow(MENU_SHOW_DELAY); }; #onGlobalPointerDown = (event) => { const target = event.target; if (this.$menu.contains(target)) return; if (this.#isIgnoredPointerTarget(target)) { this.#shiftSelectionSession = null; return; } if (target instanceof Node && this.#view.dom.contains(target)) { this.#captureShiftSelection(event); this.#isPointerInteracting = true; this.#clearMenuShowTimer(); this.#hideMenu(); return; } this.#shiftSelectionSession = null; this.#isPointerInteracting = false; this.#menuRequested = false; this.#hideMenu(); }; #onGlobalPointerUp = (event) => { if (event.type === "pointerup") { this.#commitShiftSelection(event); } else { this.#shiftSelectionSession = null; } if (!this.#isPointerInteracting) return; this.#isPointerInteracting = false; if (!this.#enabled) return; if (this.#shouldShowMenu()) { this.#scheduleMenuShow(MENU_SHOW_DELAY); return; } if (!this.#hasSelection()) { this.#menuRequested = false; } this.#hideMenu(); }; #captureShiftSelection(event) { if (!this.#canExtendSelection(event)) { this.#shiftSelectionSession = null; return; } this.#shiftSelectionSession = { pointerId: event.pointerId, anchor: this.#view.state.selection.main.anchor, x: event.clientX, y: event.clientY, }; } #commitShiftSelection(event) { const session = this.#shiftSelectionSession; this.#shiftSelectionSession = null; if (!session) return; if (!this.#canExtendSelection(event)) return; if (event.pointerId !== session.pointerId) return; if ( Math.hypot(event.clientX - session.x, event.clientY - session.y) > TAP_MAX_DISTANCE ) { return; } const target = event.target; if (!(target instanceof Node) || !this.#view.dom.contains(target)) return; if (this.#isIgnoredPointerTarget(target)) return; // Rely on pointer coordinates instead of click events so touch selection // keeps working when the browser/native path owns the actual tap. const head = this.#view.posAtCoords( { x: event.clientX, y: event.clientY }, false, ); this.#view.dispatch({ selection: EditorSelection.range(session.anchor, head), userEvent: "select.extend", }); this.#pendingShiftSelectionClick = { x: event.clientX, y: event.clientY, timeStamp: event.timeStamp, }; event.preventDefault(); } #canExtendSelection(event) { if (!this.#enabled) return false; if (!(event.isTrusted && event.isPrimary)) return false; if (typeof event.button === "number" && event.button !== 0) return false; return !!this.#isShiftSelectionActive(event); } consumePendingShiftSelectionClick(event) { const pending = this.#pendingShiftSelectionClick; this.#pendingShiftSelectionClick = null; if (!pending || !this.#enabled) return false; if (event.timeStamp - pending.timeStamp > TAP_MAX_DELAY) return false; if ( Math.hypot(event.clientX - pending.x, event.clientY - pending.y) > TAP_MAX_DISTANCE ) { return false; } const target = event.target; if (!(target instanceof Node) || !this.#view.dom.contains(target)) return false; if (this.#isIgnoredPointerTarget(target)) return false; return true; } #shouldShowMenu() { if (this.#isScrolling || this.#isPointerInteracting || !this.#view.hasFocus) return false; return this.#hasSelection() || this.#menuRequested; } #scheduleMenuShow(delay = 0) { this.#clearMenuShowTimer(); if (!this.#enabled || this.#isScrolling) return; this.#menuShowTimer = setTimeout(() => { this.#menuShowTimer = null; if (!this.#enabled || this.#isScrolling) return; if (!this.#shouldShowMenu()) { if (!this.#hasSelection()) { this.#menuRequested = false; } this.#hideMenu(); return; } cancelAnimationFrame(this.#stateSyncRaf); this.#stateSyncRaf = requestAnimationFrame(() => { this.#stateSyncRaf = 0; this.#showMenuDeferred(); }); }, delay); } #safeCoordsAtPos(view, pos) { try { return view.coordsAtPos(pos); } catch { return null; } } #getMenuAnchor(selection = this.#hasSelection()) { const range = this.#view.state.selection.main; if (!selection) { const caret = this.#safeCoordsAtPos(this.#view, range.head); if (!caret) return null; return { x: (caret.left + caret.right) / 2, top: caret.top, bottom: caret.bottom, hasSelection: false, }; } const start = this.#safeCoordsAtPos(this.#view, range.from); const end = this.#safeCoordsAtPos(this.#view, range.to); const primary = start || end; if (!primary) return null; const secondary = end || start || primary; return { x: ((start?.left ?? primary.left) + (end?.left ?? secondary.left)) / 2, top: Math.min(primary.top, secondary.top), bottom: Math.max(primary.bottom, secondary.bottom), hasSelection: true, }; } #showMenu(anchor) { const hasSelection = this.#hasSelection(); const items = filterSelectionMenuItems(selectionMenu(), { readOnly: this.#isReadOnly(), hasSelection, }); this.$menu.innerHTML = ""; if (!items.length) { this.#menuRequested = false; this.#hideMenu(true); return; } items.forEach(({ onclick, text }) => { const $item = document.createElement("div"); if (typeof text === "string") { $item.textContent = text; } else if (text instanceof Node) { $item.append(text.cloneNode(true)); } let handled = false; const runAction = (event) => { if (handled) return; handled = true; event.preventDefault(); event.stopPropagation(); this.#handlingMenuAction = true; try { onclick?.(); } finally { this.#handlingMenuAction = false; this.#menuRequested = false; this.#hideMenu(); this.#view.focus(); } }; $item.addEventListener("pointerdown", runAction); $item.addEventListener("click", runAction); this.$menu.append($item); }); if (!this.$menu.isConnected) { this.#container.append(this.$menu); } const containerRect = this.#container.getBoundingClientRect(); this.$menu.style.left = "0px"; this.$menu.style.top = "0px"; this.$menu.style.visibility = "hidden"; const menuRect = this.$menu.getBoundingClientRect(); const preferredLeft = anchor.x - menuRect.width / 2; const aboveGap = anchor.hasSelection ? MENU_SELECTION_GAP : MENU_CARET_GAP; const belowGap = anchor.hasSelection ? MENU_HANDLE_CLEARANCE : MENU_CARET_GAP; const topAbove = anchor.top - menuRect.height - aboveGap; const topBelow = anchor.bottom + belowGap; const minTop = containerRect.top + MENU_MARGIN; const maxTop = containerRect.top + containerRect.height - menuRect.height - MENU_MARGIN; const fitsAbove = topAbove >= minTop; const fitsBelow = topBelow <= maxTop; const clamped = clampMenuPosition( { left: preferredLeft, top: fitsAbove || !fitsBelow ? topAbove : topBelow, width: menuRect.width, height: menuRect.height, }, { left: containerRect.left, top: containerRect.top, width: containerRect.width, height: containerRect.height, }, ); this.$menu.style.left = `${clamped.left - containerRect.left}px`; this.$menu.style.top = `${clamped.top - containerRect.top}px`; this.$menu.style.visibility = ""; this.#menuActive = true; this.#menuRequested = false; } #showMenuDeferred() { if (!this.#enabled || this.#isScrolling || !this.#shouldShowMenu()) return; const useSelectionAnchor = this.#hasSelection(); this.#view.requestMeasure({ read: () => this.#getMenuAnchor(useSelectionAnchor), write: (anchor) => { if (!this.#enabled || this.#isScrolling || !this.#shouldShowMenu()) { this.#hideMenu(); return; } if (!anchor) { this.#hideMenu(true); return; } this.#showMenu(anchor); }, }); } #hideMenu(force = false) { if (!force && !this.#menuActive && !this.$menu.isConnected) return; if (this.$menu.isConnected) { this.$menu.remove(); } this.#menuActive = false; } #clearMenuShowTimer() { clearTimeout(this.#menuShowTimer); this.#menuShowTimer = null; } #isReadOnly() { const activeFile = this.#getActiveFile(); if (activeFile?.type === "editor") { return !activeFile.editable || !!activeFile.loading; } return !!this.#view.state?.readOnly; } #isIgnoredPointerTarget(target) { let element = null; if (target instanceof Element) { element = target; } else if (target instanceof Node) { element = target.parentElement; } if (!element) return false; if (element.closest(".cm-tooltip, .cm-panel")) return true; const editorContent = element.closest(".cm-content"); if (editorContent && this.#view.dom.contains(editorContent)) { return false; } if ( element.closest( 'input, textarea, select, button, a, [contenteditable], [role="button"]', ) ) { return true; } return false; } #hasSelection() { const selection = this.#view.state.selection.main; return selection.from !== selection.to; } } ================================================ FILE: src/components/WebComponents/index.js ================================================ import "@ungap/custom-elements"; import WcPage from "./wcPage"; customElements.define("wc-page", WcPage); ================================================ FILE: src/components/WebComponents/wcPage.js ================================================ import tile from "../tile"; export default class WCPage extends HTMLElement { #leadBtn; #header; #on = { hide: [], show: [], willconnect: [], willdisconnect: [], }; #append; handler; onhide; onconnect; ondisconnect; onwillconnect; onwilldisconnect; constructor() { super(); const title = this.getAttribute("data-title"); this.handler = new PageHandler(this); this.#append = super.append.bind(this); this.append = this.appendBody.bind(this); this.hide = this.hide.bind(this); this.settitle = this.settitle.bind(this); this.on = this.on.bind(this); this.off = this.off.bind(this); this.handler.onReplace = () => { if (typeof this.onwilldisconnect === "function") { this.onwilldisconnect(); } this.#on.willdisconnect.forEach((cb) => cb.call(this)); }; this.handler.onRestore = () => { if (typeof this.onwillconnect === "function") { this.onwillconnect(); } this.#on.willconnect.forEach((cb) => cb.call(this)); }; this.#leadBtn = ( this.hide.call(this)} attr-action="go-back" > ); this.#header = tile({ type: "header", text: title || "Page", lead: this.#leadBtn, }); } appendBody(...$els) { let $main = this.body; if (!$main) return; for (const $el of $els) { $main.append($el); } } appendOuter(...$els) { this.#append(...$els); } attributeChangedCallback(name, oldValue, newValue) { if (name === "data-title") { this.settitle = newValue; } } connectedCallback() { this.classList.remove("hide"); if (typeof this.onconnect === "function") this.onconnect(); this.#on.show.forEach((cb) => cb.call(this)); } disconnectedCallback() { if (typeof this.ondisconnect === "function") this.ondisconnect(); this.#on.hide.forEach((cb) => cb.call(this)); } /** * Adds event listener to the page * @param {'hide' | 'show'} event * @param {function(this: WCPage):void} cb */ on(event, cb) { if (event in this.#on) { this.#on[event].push(cb); } } /** * Removes event listener from the page * @param {'hide' | 'show'} event * @param {function(this: WCPage):void} cb */ off(event, cb) { if (event in this.#on) { this.#on[event] = this.#on[event].filter((fn) => fn !== cb); } } /** * Sets the title of the page * @param {string} title */ settitle(title) { this.header.text = title; } hide() { this.classList.add("hide"); if (typeof this.onhide === "function") this.onhide(); setTimeout(() => { this.remove(); this.handler.remove(); }, 150); } get body() { return this.get(".main") || this.get("main"); } set body($el) { if (this.body) this.replaceChild($el, this.body); const headerAdjacent = this.header.nextElementSibling; if (headerAdjacent) { this.insertBefore($el, headerAdjacent); return; } this.appendChild($el); } get innerHTML() { return this.body?.innerHTML; } set innerHTML(html) { if (this.body) this.body.innerHTML = html; } get textContent() { return this.body?.textContent; } set textContent(text) { if (this.body) this.body.textContent = text; } get lead() { return this.#leadBtn; } set lead($el) { this.header.replaceChild($el, this.#leadBtn); this.#leadBtn = $el; } get header() { return this.#header; } set header($el) { this.#header.replaceChild($el, this.#header); this.#header = $el; } initializeIfNotAlreadyInitialized() { if (!this.#header.isConnected) { this.#addHeaderOrAssignHeader(); } } #addHeaderOrAssignHeader() { if (!this.classList.contains("primary")) { this.#append(this.#header); this.#append(
); } else { this.#header = this.get("header"); if (this.#header) { this.#leadBtn = this.#header.firstChild; } } } } class PageHandler { $el; $replacement; onRestore; onReplace; /** * * @param {HTMLElement} $el */ constructor($el) { this.$el = $el; this.onhide = this.onhide.bind(this); this.onshow = this.onshow.bind(this); this.$replacement = ; this.$replacement.handler = this; this.$el.on("hide", this.onhide); this.$el.on("show", this.onshow); } /** * Replace current element with a replacement element */ replaceEl() { this.$el.off("hide", this.onhide); if (!this.$el.isConnected || this.$replacement.isConnected) return; if (typeof this.onReplace === "function") this.onReplace(); this.$el.parentElement.replaceChild(this.$replacement, this.$el); this.$el.classList.add("no-transition"); } /** * Restore current element from a replacement element */ restoreEl() { if (this.$el.isConnected || !this.$replacement.isConnected) return; if (typeof this.onRestore === "function") this.onRestore(); this.$el.off("hide", this.onhide); this.$replacement.parentElement.replaceChild(this.$el, this.$replacement); this.$el.on("hide", this.onhide); } onhide() { this.$el.off("hide", this.onhide); handlePagesForSmoothExperienceBack(); } onshow() { this.$el.off("show", this.onshow); handlePagesForSmoothExperience(); } remove() { this.$replacement.remove(); } } /** * Remove invisible pages from DOM and add them to the stack */ function handlePagesForSmoothExperience() { const $pages = [...tag.getAll("wc-page")]; for (let $page of $pages.slice(0, -1)) { $page.handler.replaceEl(); } } function handlePagesForSmoothExperienceBack() { [...tag.getAll(".page-replacement")].pop()?.handler.restoreEl(); } ================================================ FILE: src/components/audioPlayer/index.js ================================================ import "./style.scss"; import Ref from "html-tag-js/ref"; export default class AudioPlayer { constructor(container) { this.container = container; this.audio = new Audio(); this.isPlaying = false; this.elements = { playBtn: Ref(), playIcon: Ref(), timeline: Ref(), progress: Ref(), progressHandle: Ref(), timeDisplay: Ref(), duration: Ref(), volumeBtn: Ref(), }; this.initializeUI(); this.initializeEvents(); this.cleanup = this.cleanup.bind(this); } initializeUI() { const audioPlayer = (
0:00
); this.container.appendChild(audioPlayer); this.elements.volumeBtn.el.innerHTML = ` `; } initializeEvents() { // Play/Pause this.elements.playBtn.el.addEventListener("click", () => this.togglePlay()); // Timeline this.elements.timeline.el.addEventListener("click", (e) => this.seek(e)); this.elements.timeline.el.addEventListener("touchstart", (e) => this.seek(e), ); // Volume this.elements.volumeBtn.el.addEventListener("click", () => this.toggleMute(), ); // Audio events this.audio.addEventListener("timeupdate", () => this.updateProgress()); this.audio.addEventListener("ended", () => this.audioEnded()); } togglePlay() { if (this.isPlaying) { this.audio.pause(); this.elements.playIcon.el.classList.remove("pause"); this.elements.playIcon.el.classList.add("play_arrow"); } else { this.audio.play(); this.elements.playIcon.el.classList.remove("play_arrow"); this.elements.playIcon.el.classList.add("pause"); } this.isPlaying = !this.isPlaying; } seek(e) { const rect = this.elements.timeline.el.getBoundingClientRect(); const pos = (e.type.includes("touch") ? e.touches[0].clientX : e.clientX) - rect.left; const percentage = pos / rect.width; this.audio.currentTime = percentage * this.audio.duration; } updateProgress() { const percentage = (this.audio.currentTime / this.audio.duration) * 100; this.elements.progress.el.style.width = `${percentage}%`; this.elements.progressHandle.el.style.left = `${percentage}%`; this.elements.timeDisplay.el.textContent = this.formatTime( this.audio.currentTime, ); } formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; } toggleMute() { this.audio.muted = !this.audio.muted; if (this.audio.muted) { this.elements.volumeBtn.el.innerHTML = ''; } else { this.elements.volumeBtn.el.innerHTML = ''; } } audioEnded() { this.isPlaying = false; this.elements.playIcon.el.classList.remove("pause"); this.elements.playIcon.el.classList.add("play_arrow"); } loadTrack(src) { this.audio.src = src; this.audio.load(); } cleanup() { this.audio.pause(); this.audio.currentTime = 0; this.isPlaying = false; this.elements.playBtn.el.removeEventListener("click", () => this.togglePlay(), ); this.elements.timeline.el.removeEventListener("click", (e) => this.seek(e)); this.elements.timeline.el.removeEventListener("touchstart", (e) => this.seek(e), ); this.elements.volumeBtn.el.removeEventListener("click", () => this.toggleMute(), ); this.audio.removeEventListener("timeupdate", () => this.updateProgress()); this.audio.removeEventListener("ended", () => this.audioEnded()); const audioSrc = this.audio.src; this.audio.src = ""; this.audio.load(); if (audioSrc.startsWith("blob:")) { URL.revokeObjectURL(audioSrc); } } } ================================================ FILE: src/components/audioPlayer/style.scss ================================================ .audio-player { background: var(--primary-color, #1e1e1e); border-radius: 10px; padding: 15px; display: flex; align-items: center; gap: 15px; width: 100%; max-width: 400px; user-select: none; .play-btn { background: transparent; border: none; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; span { font-size: 20px; color: var(--primary-text-color); } &:hover { background: color-mix(in srgb, var(--secondary-color) 30%, transparent); } } .timeline { flex: 1; height: 4px; background: color-mix(in srgb, var(--secondary-color) 60%, transparent); border-radius: 2px; position: relative; cursor: pointer; &:hover .progress-handle { opacity: 1; } } .progress { background: var(--primary-text-color, #fff); width: 0%; height: 100%; border-radius: 2px; transition: width 0.1s linear; } .progress-handle { width: 12px; height: 12px; background: var(--primary-text-color, #fff); border-radius: 50%; position: absolute; top: 50%; transform: translate(-50%, -50%); pointer-events: none; opacity: 0; transition: opacity 0.2s; } .time { color: var(--primary-text-color, #fff); font-family: monospace; font-size: 12px; min-width: 45px; } .volume-control { display: flex; align-items: center; gap: 8px; } .volume-btn { background: transparent; border: none; cursor: pointer; svg { width: 20px; height: 20px; fill: var(--primary-text-color, #fff); } } } ================================================ FILE: src/components/checkbox/index.js ================================================ import "./styles.scss"; import Ref from "html-tag-js/ref"; /** * @typedef {Object} Checkbox * @property {string} text * @property {Ref} ref * @property {boolean} checked * @property {string} [name] * @property {string} [id] * @property {string} [size] * @property {"checkbox"|"radio"} [type] */ /** * Create a checkbox * @param {string | Checkbox} text Checkbox label * @param {Boolean} checked Whether checkbox is checked or not * @param {string} [name] Name of checkbox * @param {string} [id] Id of checkbox * @param {"checkbox"|"radio"} [type] Type of checkbox * @param {Ref} [ref] A reference to the input element * @param {string} [size] Size of checkbox * @returns {Checkbox & HTMLLabelElement} */ function Checkbox(text, checked, name, id, type, ref, size) { if (typeof text === "object") { ({ text, checked, name, id, type, ref, size } = text); } size = size || "1rem"; const $input = ref || Ref(); const $checkbox = ( ); Object.defineProperties($checkbox, { checked: { get() { return !!$input.el.checked; }, set(value) { $input.el.checked = value; }, }, onclick: { get() { return $input.el.onclick; }, set(onclick) { $input.el.onclick = onclick; }, }, onchange: { get() { return $input.el.onchange; }, set(onchange) { $input.el.onchange = onchange; }, }, value: { get() { return this.checked; }, set(value) { this.checked = value; }, }, toggle: { value() { this.checked = !this.checked; }, }, }); return $checkbox; } export default Checkbox; ================================================ FILE: src/components/checkbox/styles.scss ================================================ .input-checkbox { display: inline-flex; align-items: center; input { display: none; } .box { display: flex; height: 1.2rem; width: 1.2rem; border-radius: 4px; border: solid 1px #252525; border: solid 1px var(--secondary-text-color); margin: 0 5px; &::after { content: ''; display: block; height: 80%; width: 80%; background-color: #3399ff; background-color: var(--button-background-color); margin: auto; border-radius: 2px; } } input:checked { &+.box::after { opacity: 1; } } input:not(:checked) { &+.box::after { opacity: 0; } } } ================================================ FILE: src/components/collapsableList.js ================================================ import tag from "html-tag-js"; import tile from "./tile"; /** * @typedef {object} CollapsibleBase * @property {HTMLElement} $title * @property {HTMLUListElement} $ul * @property {function(void):void} ontoggle * @property {function(void):void} collapse * @property {function(void):void} expand * @property {boolean} collapsed * @property {boolean} unclasped */ /** * @typedef {CollapsibleBase & HTMLElement} Collapsible */ /** * Create a collapsible list * @param {string} titleText Title of the list * @param {boolean} hidden If true, the list will be hidden * @param {"indicator"|"folder"} type Type of the list toggle indicator * @param {object} [options] Configuration options * @param {HTMLElement} [options.tail] Tail element of the title * @param {string} [options.type] Type of the list element * @param {boolean} [options.allCaps] If true, the title will be in all caps * @param {function(this:Collapsible):void} [options.ontoggle] Called when the list is toggled * @returns {Collapsible} */ export default function collapsableList( titleText, type = "indicator", options = {}, ) { let onscroll = null; const $ul = tag("ul", { className: "scroll", onscroll: onUlScroll, }); const $collapseIndicator = tag("span", { className: `icon ${type}`, }); const $title = tile({ lead: $collapseIndicator, type: "div", text: options.allCaps ? titleText.toUpperCase() : titleText, tail: options.tail, }); const $mainWrapper = tag(options.type || "div", { className: "list collapsible hidden", children: [$title, $ul], }); let collapse = () => { $mainWrapper.classList.add("hidden"); if ($mainWrapper.ontoggle) $mainWrapper.ontoggle.call($mainWrapper); delete $ul.dataset.scrollTop; }; let expand = () => { $mainWrapper.classList.remove("hidden"); if ($mainWrapper.ontoggle) $mainWrapper.ontoggle.call($mainWrapper); }; $title.classList.add("light"); $title.addEventListener("click", toggle); [$title, $mainWrapper].forEach(defineProperties); $mainWrapper.dataset.id = `${Math.random().toString(36).substring(2, 15)}`; return $mainWrapper; function onUlScroll() { if (onscroll) onscroll.call($ul); $ul.dataset.scrollTop = $ul.scrollTop; } function toggle() { if ($title.collapsed) { expand(); } else { collapse(); } } function defineProperties($el) { Object.defineProperties($el, { $title: { get() { return $title; }, }, $ul: { get() { return $ul; }, }, ontoggle: { get() { return options.ontoggle; }, set(fun) { if (typeof fun === "function") options.ontoggle = fun; }, }, collapse: { get() { return collapse || (() => {}); }, set(fun) { if (typeof fun === "function") collapse = fun; }, }, expand: { get() { return expand || (() => {}); }, set(fun) { if (typeof fun === "function") expand = fun; }, }, collapsed: { get() { return $mainWrapper.classList.contains("hidden"); }, }, unclasped: { get() { return !this.collapsed; }, }, onscroll: { get() { return onscroll; }, set(fun) { if (typeof fun === "function") { onscroll = fun; } }, }, scrollTop: { get() { return $ul.dataset.scrollTop || 0; }, set(val) { $ul.dataset.scrollTop = val; $ul.scrollTop = val; }, }, }); } } ================================================ FILE: src/components/contextmenu/index.js ================================================ import "./style.scss"; import actionStack from "lib/actionStack"; /** * @typedef {object} ContextMenuObj * @extends HTMLElement * @property {function():void} hide hides the menu * @property {function():void} show shows the page * @property {function():void} destroy destroys the menu */ /** * @typedef {object} ContextMenuOptions * @property {number} left * @property {number} top * @property {number} bottom * @property {number} right * @property {string} transformOrigin * @property {HTMLElement} toggler * @property {function} onshow * @property {function} onhide * @property {Array<[string, string]>} items Array of [text, action] pairs * @property {(this:HTMLElement, event:MouseEvent)=>void} onclick Called when an item is clicked * @property {(item:string) => void} onselect Called when an item is selected * @property {(this:HTMLElement) => string} innerHTML Called when the menu is shown */ /** * Create a context menu * @param {string|ContextMenuOptions} content Context menu content or options * @param {ContextMenuOptions} [options] Options * @returns {ContextMenuObj} */ export default function Contextmenu(content, options) { if (!options && typeof content === "object") { options = content; content = null; } else if (!options) { options = {}; } const $el = tag("ul", { className: "context-menu scroll", innerHTML: content || "", onclick(e) { if (options.onclick) options.onclick.call(this, e); if (options.onselect) { const $target = e.target; const { action } = $target.dataset; if (!action) return; hide(); options.onselect.call(this, action); } }, style: { top: options.top || "auto", left: options.left || "auto", right: options.right || "auto", bottom: options.bottom || "auto", transformOrigin: options.transformOrigin, }, }); const $mask = tag("span", { className: "mask", ontouchstart: hide, onmousedown: hide, }); if (Array.isArray(options.items)) { options.items.forEach(([text, action]) => { $el.append(
  • {text}
  • ); }); } if (!options.innerHTML) addTabindex(); function show() { actionStack.push({ id: "main-menu", action: hide, }); $el.onshow(); $el.classList.remove("hide"); if (options.innerHTML) { $el.innerHTML = options.innerHTML.call($el); addTabindex(); } if (options.toggler) { const client = options.toggler.getBoundingClientRect(); if (!options.top && !options.bottom) { $el.style.top = client.top + "px"; } if (!options.left && !options.right) { $el.style.right = innerWidth - client.right + "px"; } } app.append($el, $mask); const $firstChild = $el.firstChild; if ($firstChild && $firstChild.focus) $firstChild.focus(); } function hide() { actionStack.remove("main-menu"); $el.onhide(); $el.classList.add("hide"); setTimeout(() => { $mask.remove(); $el.remove(); }, 100); } function toggle() { if ($el.parentElement) return hide(); show(); } function addTabindex() { /**@type {Array} */ const children = [...$el.children]; for (let $el of children) $el.tabIndex = "0"; } function destroy() { $el.remove(); $mask.remove(); options.toggler?.removeEventListener("click", toggle); } if (options.toggler) { options.toggler.addEventListener("click", toggle); } $el.hide = hide; $el.show = show; $el.destroy = destroy; $el.onshow = options.onshow || (() => {}); $el.onhide = options.onhide || (() => {}); return $el; } ================================================ FILE: src/components/contextmenu/style.scss ================================================ body.no-animation { .context-menu { border: solid 1px rgba($color: #000000, $alpha: 0.2); } } .context-menu { z-index: 111; box-sizing: border-box; position: fixed; width: fit-content; min-width: 220px; max-width: 320px; min-height: 40px; max-height: calc(100vh - 38px); overflow-y: auto; user-select: none; background-color: rgb(255, 255, 255); background-color: var(--popup-background-color); box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); box-shadow: 0 0 4px var(--box-shadow-color); border-radius: var(--popup-border-radius); transform-origin: top center; animation: menu-grow 100ms ease 1; list-style: none; padding: 0; border: solid 1px transparent; border: solid 1px var(--popup-border-color); &+.mask { z-index: 110; } &.disabled { li { pointer-events: none; opacity: 0.5; } } hr { border: none; border-bottom: solid 1px rgba(122, 122, 122, 0.227); border-bottom: solid 1px var(--border-color); } li { display: flex; height: 50px; align-items: center; padding: 0 10px; user-select: none; &.notice::after { content: "•"; color: rgb(255, 218, 12); font-size: 1.2em; margin-left: 2.5px; text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5); } &:focus { background-color: rgba($color: #000000, $alpha: 0.1); } * { pointer-events: none; } [data-action], [action] { pointer-events: all !important; } .text { flex: 1; color: rgb(37, 37, 37); color: var(--popup-text-color); .value { display: block; opacity: 0.6; font-size: 0.8em; } } .icon { color: rgb(153, 153, 255); color: var(--popup-icon-color); } &.disabled { pointer-events: none; opacity: 0.5; } } &.hide { transition: all 100ms ease; opacity: 0; } } ================================================ FILE: src/components/fileTree/index.js ================================================ import "./style.scss"; import tile from "components/tile"; import VirtualList from "components/virtualList"; import tag from "html-tag-js"; import helpers from "utils/helpers"; import Path from "utils/Path"; const VIRTUALIZATION_THRESHOLD = Number.POSITIVE_INFINITY; // FIX: temporary due to some scrolling issues in VirtualList const ITEM_HEIGHT = 30; /** * @typedef {object} FileTreeOptions * @property {function(string): Promise} getEntries - Function to get directory entries * @property {function(string, string): void} [onFileClick] - File click handler * @property {function(string, string, string, HTMLElement): void} [onContextMenu] - Context menu handler * @property {Object} [expandedState] - Map of expanded folder URLs * @property {function(string, boolean): void} [onExpandedChange] - Called when folder expanded state changes */ /** * FileTree component for rendering folder contents with virtual scrolling */ export default class FileTree { /** * @param {HTMLElement} container * @param {FileTreeOptions} options */ constructor(container, options = {}) { this.container = container; this.container.classList.add("file-tree"); this.options = options; this.virtualList = null; this.entries = []; this.isLoading = false; this.childTrees = new Map(); // Track child trees for cleanup this.depth = options._depth || 0; // Internal: nesting depth } /** * Load and render entries for a directory * @param {string} url - Directory URL */ async load(url) { if (this.isLoading) return; this.isLoading = true; this.currentUrl = url; try { this.clear(); const entries = await this.options.getEntries(url); this.entries = helpers.sortDir(entries, { sortByName: true, showHiddenFiles: true, }); if (this.entries.length > VIRTUALIZATION_THRESHOLD) { this.renderVirtualized(); } else { this.renderWithFragment(); } } finally { this.isLoading = false; } } /** * Render using DocumentFragment for batch DOM updates (small folders) */ renderWithFragment() { const fragment = document.createDocumentFragment(); for (const entry of this.entries) { const $el = this.createEntryElement(entry); fragment.appendChild($el); } this.container.appendChild(fragment); } /** * Render using virtual scrolling (large folders) */ renderVirtualized() { this.container.classList.add("virtual-scroll"); this.virtualList = new VirtualList(this.container, { itemHeight: ITEM_HEIGHT, buffer: 15, renderItem: (entry, recycledEl) => this.createEntryElement(entry, recycledEl), }); this.virtualList.setItems(this.entries); } /** * Create DOM element for a file/folder entry * @param {object} entry * @param {HTMLElement} [recycledEl] - Optional recycled element for reuse * @returns {HTMLElement} */ createEntryElement(entry, recycledEl) { const name = entry.name || Path.basename(entry.url); if (entry.isDirectory) { return this.createFolderElement(name, entry.url, recycledEl); } else { return this.createFileElement(name, entry.url, recycledEl); } } /** * Create folder element (collapsible) * @param {string} name * @param {string} url * @param {HTMLElement} [recycledEl] - Optional recycled element for reuse * @returns {HTMLElement} */ createFolderElement(name, url, recycledEl) { // Try to recycle existing folder element if (recycledEl && recycledEl.classList.contains("collapsible")) { const $title = recycledEl.$title; if ($title) { $title.dataset.url = url; $title.dataset.name = name; const textEl = $title.querySelector(".text"); if (textEl) textEl.textContent = name; // Collapse if expanded and clear children if (!recycledEl.classList.contains("hidden")) { recycledEl.classList.add("hidden"); const childTree = this.childTrees.get(recycledEl._folderUrl); if (childTree) { childTree.destroy(); this.childTrees.delete(recycledEl._folderUrl); } recycledEl.$ul.innerHTML = ""; } recycledEl._folderUrl = url; return recycledEl; } } const $wrapper = tag("div", { className: "list collapsible hidden", }); $wrapper._folderUrl = url; const $indicator = tag("span", { className: "icon folder" }); const $title = tile({ lead: $indicator, type: "div", text: name, }); $title.classList.add("light"); $title.dataset.url = url; $title.dataset.name = name; $title.dataset.type = "dir"; const $content = tag("ul", { className: "scroll folder-content" }); $wrapper.append($title, $content); // Child file tree for nested folders let childTree = null; $content._fileTree = null; const toggle = async () => { const isExpanded = !$wrapper.classList.contains("hidden"); if (isExpanded) { // Collapse $wrapper.classList.add("hidden"); if (childTree) { childTree.destroy(); this.childTrees.delete(url); childTree = null; $content._fileTree = null; } this.options.onExpandedChange?.(url, false); } else { // Expand $wrapper.classList.remove("hidden"); $title.classList.add("loading"); // Create child tree with incremented depth childTree = new FileTree($content, { ...this.options, _depth: this.depth + 1, }); this.childTrees.set(url, childTree); $content._fileTree = childTree; try { await childTree.load(url); } finally { $title.classList.remove("loading"); } this.options.onExpandedChange?.(url, true); } }; $title.addEventListener("click", (e) => { e.stopPropagation(); toggle(); }); $title.addEventListener("contextmenu", (e) => { e.stopPropagation(); this.options.onContextMenu?.("dir", url, name, $title); }); // Check if folder should be expanded from saved state if (this.options.expandedState?.[url]) { queueMicrotask(() => toggle()); } const defineCollapsibleAccessors = ($el, { includeTitle = true } = {}) => { const properties = { collapsed: { get: () => $wrapper.classList.contains("hidden") }, expanded: { get: () => !$wrapper.classList.contains("hidden") }, unclasped: { get: () => !$wrapper.classList.contains("hidden") }, // Legacy compatibility $ul: { get: () => $content }, fileTree: { get: () => childTree }, refresh: { value: () => childTree?.refresh(), }, expand: { value: () => !$wrapper.classList.contains("hidden") || toggle(), }, collapse: { value: () => $wrapper.classList.contains("hidden") || toggle(), }, }; if (includeTitle) { properties.$title = { get: () => $title }; } Object.defineProperties($el, properties); }; // Keep nested folders compatible with the legacy collapsableList API. defineCollapsibleAccessors($wrapper); defineCollapsibleAccessors($title, { includeTitle: false }); return $wrapper; } /** * Create file element (tile) * @param {string} name * @param {string} url * @param {HTMLElement} [recycledEl] - Optional recycled element for reuse * @returns {HTMLElement} */ createFileElement(name, url, recycledEl) { const iconClass = helpers.getIconForFile(name); // Try to recycle existing element if (recycledEl && recycledEl.dataset.type === "file") { recycledEl.dataset.url = url; recycledEl.dataset.name = name; const textEl = recycledEl.querySelector(".text"); const iconEl = recycledEl.querySelector("span:first-child"); if (textEl) textEl.textContent = name; if (iconEl) iconEl.className = iconClass; return recycledEl; } const $tile = tile({ lead: tag("span", { className: iconClass }), text: name, }); $tile.dataset.url = url; $tile.dataset.name = name; $tile.dataset.type = "file"; $tile.addEventListener("click", (e) => { e.stopPropagation(); this.options.onFileClick?.(url, name); }); $tile.addEventListener("contextmenu", (e) => { e.stopPropagation(); this.options.onContextMenu?.("file", url, name, $tile); }); return $tile; } /** * Clear all rendered content */ clear() { this.destroyChildTrees(); if (this.virtualList) { this.virtualList.destroy(); this.virtualList = null; } this.container.innerHTML = ""; this.container.classList.remove("virtual-scroll"); this.entries = []; } /** * Destroy the file tree and cleanup */ destroy() { this.clear(); this.container.classList.remove("file-tree"); } /** * Find an entry element by URL * @param {string} url * @returns {HTMLElement|null} */ findElement(url) { return this.container.querySelector(`[data-url="${CSS.escape(url)}"]`); } /** * Refresh the current directory */ async refresh() { if (this.currentUrl) { await this.load(this.currentUrl); } } /** * Destroy all expanded child trees and clear their references. */ destroyChildTrees() { for (const childTree of this.childTrees.values()) { childTree.destroy(); } this.childTrees.clear(); } /** * Append a new entry to the tree * @param {string} name * @param {string} url * @param {boolean} isDirectory */ appendEntry(name, url, isDirectory) { const entry = { name, url, isDirectory, isFile: !isDirectory }; // Insert in sorted position if (isDirectory) { // Find first file or end of dirs const insertIndex = this.entries.findIndex((e) => !e.isDirectory); if (insertIndex === -1) { this.entries.push(entry); } else { this.entries.splice(insertIndex, 0, entry); } } else { this.entries.push(entry); } // Re-sort entries this.entries = helpers.sortDir(this.entries, { sortByName: true, showHiddenFiles: true, }); // Update rendering based on mode if (this.virtualList) { // Virtual list mode: update items this.virtualList.setItems(this.entries); } else { // Fragment mode: re-render this.destroyChildTrees(); this.container.innerHTML = ""; this.renderWithFragment(); } } /** * Remove an entry from the tree * @param {string} url */ removeEntry(url) { // Update data first const index = this.entries.findIndex((e) => e.url === url); if (index === -1) return; // Clean up child tree if folder const entry = this.entries[index]; if (entry.isDirectory && this.childTrees.has(url)) { this.childTrees.get(url).destroy(); this.childTrees.delete(url); } // Remove from entries this.entries.splice(index, 1); // Update rendering based on mode if (this.virtualList) { // Virtual list mode: update items this.virtualList.setItems(this.entries); } else { // Fragment mode: remove element directly const $el = this.findElement(url); if ($el) { if ($el.dataset.type === "dir") { $el.closest(".list.collapsible")?.remove(); } else { $el.remove(); } } } } } ================================================ FILE: src/components/fileTree/style.scss ================================================ @use "../../styles/mixins.scss"; .file-tree { --file-tree-indent: 24px; min-width: 100%; width: max-content; &.virtual-scroll { position: relative; overflow-y: auto; will-change: transform; } // Collapsible folder structure .list.collapsible { &.hidden>ul { display: none; } >.tile { cursor: pointer; &.loading { @include mixins.linear-loader(30%, 2px); } } } // Folder content with indent ul.folder-content { padding-left: var(--file-tree-indent); position: relative; margin-left: 0; min-width: 100%; width: max-content; overflow-x: visible !important; max-width: none; } // Indent guides &.show-indent-guide { >.list.collapsible>ul.folder-content { &::before { content: ""; position: absolute; left: calc(var(--file-tree-indent) / 2); top: 0; height: 100%; width: 1px; background: var(--border-color); z-index: 0; } } } .tile { min-width: 100%; width: max-content; >.text { white-space: nowrap !important; overflow: visible !important; width: max-content !important; text-overflow: clip !important; } } // File items li, [data-type="file"] { min-width: 100%; width: max-content; } } ================================================ FILE: src/components/inputhints/index.js ================================================ import "./style.scss"; /** * @typedef {Object} HintObj * @property {string} value * @property {string} text * @property {boolean} [active] */ /** * @typedef {HintObj|string} Hint */ /** * @typedef {Object} HintModification * @property {(hint:Hint, index:number)=>void} add * @property {(hint:Hint)=>void} remove * @property {(index:number)=>void} removeIndex */ /** * @typedef {(setHints:(hints:Array)=>void, modification: HintModification) => void} HintCallback */ /** * Generate a list of hints for an input field * @param {HTMLInputElement} $input Input field * @param {Array|HintCallback} hints Hints or a callback to generate hints * @param {(value: string) => void} onSelect Callback to call when a hint is selected * @returns {{getSelected: ()=>HTMLLIElement, container: HTMLUListElement}} */ export default function inputhints($input, hints, onSelect) { /**@type {HTMLUListElement} */ const $ul =
      ; const LIMIT = 100; let preventUpdate = false; let updateUlTimeout; let pages = 0; let currentHints = []; $input.addEventListener("focus", onfocus); if (typeof hints === "function") { const cb = hints; hints = []; $ul.content = []; cb(setHints, hintModification()); } else { setHints(hints); } /** * Retain the focus on the input field */ function handleMouseDown() { preventUpdate = true; } function handleMouseUp() { $input.focus(); preventUpdate = false; } /** * Handle click event * @param {MouseEvent} e Event */ function handleClick(e) { const $el = e.target; const action = $el.getAttribute("action"); if (action !== "hint") return; const value = $el.getAttribute("value"); if (!value) { onblur(); return; } $input.value = $el.textContent; if (onSelect) onSelect(value); preventUpdate = false; onblur(); } /** * Handle keypress event * @param {KeyboardEvent} e Event */ function handleKeypress(e) { if (e.key !== "Enter") return; e.preventDefault(); e.stopPropagation(); const activeHint = $ul.get(".active"); if (!activeHint) return; const value = activeHint.getAttribute("value"); if (onSelect) onSelect(value); else $input.value = value; } /** * Handle keydown event * @param {KeyboardEvent} e Event */ function handleKeydown(e) { const code = e.key; if (code === "ArrowUp" || code === "ArrowDown") { e.preventDefault(); e.stopPropagation(); } updateHintFocus(code); } /** * Moves the active hint up or down * @param {"ArrowDown" | "ArrowUp"} key Direction to move */ function updateHintFocus(key) { let nextHint; let activeHint = $ul.get(".active"); if (!activeHint) activeHint = $ul.firstChild; if (key === "ArrowDown") { nextHint = activeHint.nextElementSibling; if (!nextHint) nextHint = $ul.firstElementChild; } else if (key === "ArrowUp") { nextHint = activeHint.previousElementSibling; if (!nextHint) nextHint = $ul.lastElementChild; } if (nextHint) { activeHint.classList.remove("active"); nextHint.classList.add("active"); nextHint.scrollIntoView(); } } /** * @this {HTMLInputElement} */ function oninput() { const { value: toTest } = this; const matched = []; const regexp = new RegExp(escapeRegExp(toTest), "i"); hints.forEach((hint) => { const { value, text } = hint; if (regexp.test(value) || regexp.test(text)) { matched.push(hint); } }); updateUl(matched); } function onfocus() { if (preventUpdate) return; $input.addEventListener("keypress", handleKeypress); $input.addEventListener("keydown", handleKeydown); $input.addEventListener("blur", onblur); $input.addEventListener("input", oninput); window.addEventListener("resize", position); ulAddEventListeners(); app.append($ul); position(); } /** * Event listener for blur * @returns */ function onblur() { if (preventUpdate) return; clearTimeout(updateUlTimeout); $input.removeEventListener("keypress", handleKeypress); $input.removeEventListener("keydown", handleKeydown); $input.removeEventListener("blur", onblur); $input.removeEventListener("input", oninput); window.removeEventListener("resize", position); ulRemoveEventListeners(); $ul.remove(); } /** * Update the position of the hint list * @param {boolean} append Append the list to the body or not */ function position() { const activeHint = $ul.get(".active"); const { firstElementChild } = $ul; if (!activeHint && firstElementChild) firstElementChild.classList.add("active"); const client = $input.getBoundingClientRect(); const inputTop = client.top - 5; const inputBottom = client.bottom + 5; const inputLeft = client.left; const bottomHeight = window.innerHeight - inputBottom; const mid = window.innerHeight / 2; if (bottomHeight >= mid) { $ul.classList.remove("bottom"); $ul.style.top = `${inputBottom}px`; $ul.style.bottom = "auto"; } else { $ul.classList.add("bottom"); $ul.style.top = "auto"; $ul.style.bottom = `${inputTop}px`; } $ul.style.left = `${inputLeft}px`; $ul.style.width = `${client.width}px`; } /** * Set hint items * @param {Array} list Hint items */ function setHints(list) { if (Array.isArray(list)) { hints = list; } else { hints = []; } updateUl(hints); $ul.classList.remove("loading"); } function hintModification() { return { add(item, index) { if (index) { hints.splice(index, 0, item); const child = $ul.children[index]; if (child) { $ul.insertBefore(child, $ul.children[index]); } return; } hints.push(item); }, remove(item) { const index = hints.indexOf(item); if (index > -1) { hints.splice(index, 1); } }, removeIndex(index) { hints.splice(index, 1); }, }; } function ulAddEventListeners() { window.addEventListener("resize", position); $ul.addEventListener("click", handleClick); $ul.addEventListener("mousedown", handleMouseDown); $ul.addEventListener("mouseup", handleMouseUp); $ul.addEventListener("touchstart", handleMouseDown); $ul.addEventListener("touchend", handleMouseUp); $ul.addEventListener("scroll", updatePage); } function ulRemoveEventListeners() { window.removeEventListener("resize", position); $ul.removeEventListener("click", handleClick); $ul.removeEventListener("mousedown", handleMouseDown); $ul.removeEventListener("mouseup", handleMouseUp); $ul.removeEventListener("touchstart", handleMouseDown); $ul.removeEventListener("touchend", handleMouseUp); $ul.removeEventListener("scroll", updatePage); } function updatePage() { const offset = (pages + 1) * LIMIT; const hasMore = offset < currentHints.length; // if the scroll is at the bottom if ($ul.scrollTop + $ul.clientHeight >= $ul.scrollHeight && hasMore) { pages++; updateUlNow(currentHints, pages); } } /** * First time updates the hint instantly, then debounce * @param {Array} hints */ function updateUl(hints) { updateUlNow(hints); updateUl = updateUlDebounce; } /** * Update the hint list after a delay * @param {Array} hints */ function updateUlDebounce(hints) { clearTimeout(updateUlTimeout); updateUlTimeout = setTimeout(updateUlNow, 300, hints); } /** * Update the hint list instantly * @param {Array} hints * @param {number} page */ function updateUlNow(hints, page = 0) { // render only first 500 hints currentHints = hints; const offset = page * LIMIT; const end = offset + LIMIT; const list = hints.slice(offset, end); let scrollTop = $ul.scrollTop; //if (!list.length) return; $ul.remove(); if (!hints.length) { $ul.content = []; } else if (!list.length) { // No more hints to load return; } else { if (!page) { scrollTop = 0; $ul.content = list.map((hint) => ); } else { $ul.append(...list.map((hint) => )); } } app.append($ul); $ul.scrollTop = scrollTop; position(); // Update the position of the new list } return { getSelected() { $ul.get(".active"); }, get container() { return $ul; }, }; } /** * Create a hint item * @param {object} param0 Hint item * @param {HintObj} param0.hint Hint item * @returns {HTMLLIElement} */ function Hint({ hint }) { let value = ""; let text = ""; let active = false; if (typeof hint === "string") { value = hint; text = hint; } else { value = hint.value; text = hint.text; active = !!hint.active; } return (
    • ); } /** * Create a hint list * @param {object} param0 Attributes * @param {Array} param0.hints Hint items * @returns {HTMLUListElement} */ function Ul({ hints = [] }) { return (
        {hints.map((hint) => ( ))}
      ); } function escapeRegExp(value) { return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } ================================================ FILE: src/components/inputhints/style.scss ================================================ @use "../../styles/mixins.scss"; #hints { position: fixed; top: 0; left: 0; padding: 0; margin: 0; border-radius: 0 0 4px 4px; background-color: rgb(255, 255, 255); background-color: var(--secondary-color); color: rgb(37, 37, 37); color: var(--secondary-text-color); box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); box-shadow: 0 0 4px var(--box-shadow-color); border: solid 1px transparent; border: solid 1px var(--popup-border-color); height: fit-content; min-height: 30px; max-height: 70vh; z-index: 999; overflow-y: scroll; &.bottom { border-radius: 4px 4px 0 0; } &.all { border-radius: 4px; } [data-action="hint"], [action="hint"] { font-size: 0.9rem; min-height: 30px; height: fit-content; display: flex; align-items: center; box-sizing: border-box; padding: 5px; overflow-x: hidden; text-overflow: ellipsis; * { pointer-events: none; } [data-str]::after { content: attr(data-str); font-size: 0.6rem; opacity: 0.5; margin-left: 10px; } small:not(:empty) { margin-left: auto; color: rgb(51, 153, 255); color: var(--active-color); padding: 2px; border-radius: 2px; font-size: 0.6rem; } &.active { background-color: rgb(51, 153, 255); background-color: var(--active-color); color: rgb(27, 26, 26); color: var(--primary-text-color); small { color: rgb(255, 215, 0); color: var(--active-text-color); } } &:hover { background-color: rgb(107, 168, 229); background-color: var(--primary-color); color: rgb(27, 26, 26); color: var(--secondary-text-color); } } &.loading { @include mixins.loader(18px); } &:empty { display: none; } } ================================================ FILE: src/components/logo/index.js ================================================ import "./style.scss"; export default function Logo() { return
      ; } ================================================ FILE: src/components/logo/style.scss ================================================ .logo { display: block; width: 150px; height: 150px; position: relative; margin: 0 auto; &::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-image: url(./logo.png); background-size: 80px; background-repeat: no-repeat; background-position: center; } &::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgb(68, 153, 254, 0.5), rgb(68, 153, 254, 0.1), rgb(68, 153, 254, 0), rgb(68, 153, 254, 0)); overflow: visible; } } ================================================ FILE: src/components/lspInfoDialog/index.js ================================================ import "./styles.scss"; import lspClientManager from "cm/lsp/clientManager"; import { getServerStats } from "cm/lsp/serverLauncher"; import serverRegistry from "cm/lsp/serverRegistry"; import toast from "components/toast"; import actionStack from "lib/actionStack"; import restoreTheme from "lib/restoreTheme"; let dialogInstance = null; const lspLogs = new Map(); const MAX_LOGS = 200; const logListeners = new Set(); const IGNORED_LOG_PATTERNS = [ /\$\/progress\b/i, /\bProgress:/i, /\bwindow\/workDoneProgress\/create\b/i, /\bAuto-responded to window\/workDoneProgress\/create\b/i, ]; function shouldIgnoreLog(message) { if (typeof message !== "string") return false; return IGNORED_LOG_PATTERNS.some((pattern) => pattern.test(message)); } function addLspLog(serverId, level, message, details = null) { if (shouldIgnoreLog(message)) { return; } if (!lspLogs.has(serverId)) { lspLogs.set(serverId, []); } const logs = lspLogs.get(serverId); const entry = { timestamp: new Date(), level, message, details, }; logs.push(entry); if (logs.length > MAX_LOGS) { logs.shift(); } logListeners.forEach((fn) => fn(serverId, entry)); } function getLspLogs(serverId) { return lspLogs.get(serverId) || []; } function clearLspLogs(serverId) { lspLogs.delete(serverId); } const originalConsoleInfo = console.info; const originalConsoleWarn = console.warn; const originalConsoleError = console.error; function stripAnsi(str) { if (typeof str !== "string") return str; return str.replace(/\x1b\[[0-9;]*m/g, ""); } function extractServerId(message) { const cleaned = stripAnsi(message); // Match [LSP:serverId] format const lspMatch = cleaned?.match?.(/\[LSP:([^\]]+)\]/); if (lspMatch) return lspMatch[1]; // Match [LSP-STDERR:program] format from axs proxy const stderrMatch = cleaned?.match?.(/\[LSP-STDERR:([^\]]+)\]/); if (stderrMatch) { const program = stderrMatch[1]; return program; } return null; } function extractLogMessage(message) { const cleaned = stripAnsi(message); // Strip [LSP:...] and [LSP-STDERR:...] prefixes // Strip ISO timestamps like 2026-02-05T08:26:24.745443Z // Strip log levels like INFO, WARN, ERROR and the source like axs::lsp: return ( cleaned ?.replace?.(/\[LSP:[^\]]+\]\s*/, "") ?.replace?.(/\[LSP-STDERR:[^\]]+\]\s*/, "") ?.replace?.(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\s*/g, "") ?.replace?.(/\s*(INFO|WARN|ERROR|DEBUG|TRACE)\s+/gi, "") ?.replace?.(/[a-z_]+::[a-z_]+:\s*/gi, "") ?.trim() || cleaned ); } console.info = function (...args) { originalConsoleInfo.apply(console, args); const msg = args[0]; if ( typeof msg === "string" && (msg.includes("[LSP:") || msg.includes("[LSP-STDERR:")) ) { const serverId = extractServerId(msg); if (serverId) { addLspLog(serverId, "info", extractLogMessage(msg)); } } }; console.warn = function (...args) { originalConsoleWarn.apply(console, args); const msg = args[0]; if ( typeof msg === "string" && (msg.includes("[LSP:") || msg.includes("[LSP-STDERR:")) ) { const serverId = extractServerId(msg); if (serverId) { // stderr from axs is logged as warn, mark it appropriately const isStderr = msg.includes("[LSP-STDERR:"); addLspLog(serverId, isStderr ? "stderr" : "warn", extractLogMessage(msg)); } } }; console.error = function (...args) { originalConsoleError.apply(console, args); const msg = args[0]; if ( typeof msg === "string" && (msg.includes("[LSP:") || msg.includes("[LSP-STDERR:")) ) { const serverId = extractServerId(msg); if (serverId) { addLspLog(serverId, "error", extractLogMessage(msg)); } } }; function getActiveClients() { try { return lspClientManager.getActiveClients(); } catch { return []; } } function getCurrentFileLanguage() { try { const file = window.editorManager?.activeFile; if (!file || file.type !== "editor") return null; return file.currentMode?.toLowerCase() || null; } catch { return null; } } function getServersForCurrentFile() { const language = getCurrentFileLanguage(); if (!language) return []; try { return serverRegistry.getServersForLanguage(language); } catch { return []; } } function getServerStatus(serverId) { const activeClients = getActiveClients(); const client = activeClients.find((c) => c.server?.id === serverId); if (!client) return "stopped"; try { return client.client?.connected !== false ? "active" : "connecting"; } catch { return "stopped"; } } function getClientState(serverId) { const activeClients = getActiveClients(); return activeClients.find((c) => c.server?.id === serverId) || null; } function getStatusColor(status) { switch (status) { case "active": return "var(--lsp-status-active, #22c55e)"; case "connecting": return "var(--lsp-status-connecting, #f59e0b)"; default: return "var(--lsp-status-stopped, #6b7280)"; } } function copyLogsToClipboard(serverId, serverLabel) { const logs = getLspLogs(serverId); if (logs.length === 0) { toast("No logs to copy"); return; } const text = logs .map((log) => { const time = log.timestamp.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); return `[${time}] [${log.level.toUpperCase()}] ${log.message}`; }) .join("\n"); const header = `=== ${serverLabel} LSP Logs ===\n`; if (navigator.clipboard?.writeText) { navigator.clipboard .writeText(header + text) .then(() => { toast("Logs copied"); }) .catch(() => { toast("Failed to copy"); }); } else if (cordova?.plugins?.clipboard) { cordova.plugins.clipboard.copy(header + text); toast("Logs copied"); } else { toast("Clipboard not available"); } } async function restartServer(serverId) { addLspLog(serverId, "info", "Restart requested by user"); toast("Restarting server..."); try { const clientState = getClientState(serverId); if (clientState) { await clientState.dispose(); } const { stopManagedServer } = await import("cm/lsp/serverLauncher"); stopManagedServer(serverId); window.editorManager?.restartLsp?.(); addLspLog(serverId, "info", "Server restarted successfully"); toast("Server restarted"); } catch (err) { addLspLog(serverId, "error", `Restart failed: ${err.message}`); toast("Restart failed"); } } async function stopServer(serverId) { addLspLog(serverId, "info", "Stop requested by user"); toast("Stopping..."); try { const clientState = getClientState(serverId); if (clientState) { await clientState.dispose(); } const { stopManagedServer } = await import("cm/lsp/serverLauncher"); stopManagedServer(serverId); addLspLog(serverId, "info", "Server stopped"); toast("Server stopped"); } catch (err) { addLspLog(serverId, "error", `Stop failed: ${err.message}`); toast("Failed to stop"); } } async function startAllServers() { toast("Starting LSP servers..."); try { window.editorManager?.restartLsp?.(); toast("Servers started"); } catch (err) { toast("Failed to start servers"); } } async function restartAllServers() { const activeClients = getActiveClients(); if (!activeClients.length) { await startAllServers(); return; } const count = activeClients.length; toast(`Restarting ${count} LSP server${count > 1 ? "s" : ""}...`); try { await lspClientManager.dispose(); window.editorManager?.restartLsp?.(); toast("All servers restarted"); } catch (err) { toast("Failed to restart servers"); } } async function stopAllServers() { const activeClients = getActiveClients(); if (!activeClients.length) { toast("No LSP servers are currently running"); return; } const count = activeClients.length; try { await lspClientManager.dispose(); toast(`Stopped ${count} LSP server${count > 1 ? "s" : ""}`); } catch (err) { toast("Failed to stop servers"); } } function showLspInfoDialog() { if (dialogInstance) { dialogInstance.hide(); return; } const relevantServers = getServersForCurrentFile(); const currentLanguage = getCurrentFileLanguage(); let currentView = "list"; let selectedServer = null; const $mask = ; const $dialog = (
      Language Servers
      ); const $body = $dialog.querySelector(".lsp-dialog-body"); function renderList() { $body.innerHTML = ""; if (relevantServers.length === 0) { $body.appendChild(

      No language servers for{" "} {currentLanguage || "this file"}

      , ); return; } const $list =
        ; const runningServers = relevantServers.filter( (s) => getServerStatus(s.id) !== "stopped", ); const hasRunning = runningServers.length > 0; const $actions = (
        {hasRunning && ( )}
        ); $body.appendChild($actions); for (const server of relevantServers) { const status = getServerStatus(server.id); const statusColor = getStatusColor(status); const logs = getLspLogs(server.id); const errorCount = logs.filter((l) => l.level === "error").length; const $item = (
      • { selectedServer = server; currentView = "details"; renderDetails(); }} >
        {server.label} {status}
        {errorCount > 0 && ( {errorCount} )}
      • ); $list.appendChild($item); } $body.appendChild($list); } function renderDetails() { if (!selectedServer) return; $body.innerHTML = ""; const server = selectedServer; const status = getServerStatus(server.id); const clientState = getClientState(server.id); const isRunning = status !== "stopped"; const capabilities = []; const hasCapabilities = clientState?.client?.serverCapabilities; if (hasCapabilities) { const caps = clientState.client.serverCapabilities; if (caps.completionProvider) capabilities.push("Completion"); if (caps.hoverProvider) capabilities.push("Hover"); if (caps.definitionProvider) capabilities.push("Go to Definition"); if (caps.referencesProvider) capabilities.push("Find References"); if (caps.renameProvider) capabilities.push("Rename"); if (caps.documentFormattingProvider) capabilities.push("Format"); if (caps.signatureHelpProvider) capabilities.push("Signature Help"); if (caps.inlayHintProvider) capabilities.push("Inlay Hints"); if (caps.codeActionProvider) capabilities.push("Code Actions"); if (caps.diagnosticProvider) capabilities.push("Diagnostics"); } if (isRunning && capabilities.length === 0 && hasCapabilities) { capabilities.push("Diagnostics"); } const logs = getLspLogs(server.id); const $details = (
        {server.label}
        {isRunning && ( )}
        {isRunning && (
        Capabilities
        {capabilities.length > 0 ? capabilities.map((cap) => ( {cap} )) : !hasCapabilities && ( Initializing... )}
        )}
        Supported
        {server.languages.map((lang) => ( .{lang} ))}
        {isRunning && (
        Project
        {clientState?.rootUri || "(workspace folders mode)"}
        )} {isRunning && (
        Resources
        Memory
        Uptime
        PID
        )}
        ); $body.appendChild($details); // Create simple collapsible logs section const $logsSection = (
        { const section = e.currentTarget.closest(".lsp-logs-section"); if (section) { section.classList.toggle("collapsed"); if (!section.classList.contains("collapsed")) { const container = section.querySelector(".lsp-logs-container"); if (container) container.scrollTop = container.scrollHeight; } } }} >
        LSP Logs {logs.length > 0 && ( ({logs.length}) )}
        {logs.length === 0 ? (
        No logs yet
        ) : ( logs.slice(-50).map((log) => { const time = log.timestamp.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", }); return (
        {time} {log.message}
        ); }) )}
        ); $body.appendChild($logsSection); // Fetch and update stats asynchronously if (isRunning) { getServerStats(server.id).then((stats) => { if (!stats) return; const $mem = document.getElementById(`lsp-mem-${server.id}`); const $uptime = document.getElementById(`lsp-uptime-${server.id}`); const $pid = document.getElementById(`lsp-pid-${server.id}`); if ($mem) $mem.textContent = stats.memoryFormatted; if ($uptime) $uptime.textContent = stats.uptimeFormatted; if ($pid) $pid.textContent = stats.pid ? String(stats.pid) : "—"; }); } } function hide() { $dialog.classList.add("hide"); restoreTheme(); actionStack.remove("lsp-info-dialog"); setTimeout(() => { $dialog.remove(); $mask.remove(); dialogInstance = null; }, 200); } dialogInstance = { hide, element: $dialog }; actionStack.push({ id: "lsp-info-dialog", action: hide, }); restoreTheme(true); document.body.appendChild($dialog); document.body.appendChild($mask); if (currentView === "list") { renderList(); } } function hasConnectedServers() { const relevantServers = getServersForCurrentFile(); return relevantServers.length > 0; } export { addLspLog, getLspLogs, hasConnectedServers, showLspInfoDialog }; export default showLspInfoDialog; ================================================ FILE: src/components/lspInfoDialog/styles.scss ================================================ :root { --lsp-status-active: #22c55e; --lsp-status-connecting: #f59e0b; --lsp-status-stopped: #6b7280; } .lsp-info-dialog { max-width: 360px; min-width: 300px; .title { display: flex; align-items: center; text-transform: none; font-weight: 500; } } .lsp-dialog-body { overflow-y: auto; max-height: 65vh; &::-webkit-scrollbar { width: 4px; } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: var(--scrollbar-color); border-radius: 2px; } } .lsp-empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; color: var(--popup-text-color); text-align: center; .icon { font-size: 2.5rem; margin-bottom: 16px; opacity: 0.3; } p { font-size: 0.9rem; margin: 0; opacity: 0.6; strong { opacity: 1; } } } .lsp-list-actions { display: flex; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border-color); } .lsp-action-btn { display: flex; align-items: center; gap: 6px; flex: 1; justify-content: center; padding: 8px 12px; background-color: var(--box-shadow-color); border: none; color: var(--popup-text-color); cursor: pointer; border-radius: 6px; font-size: 0.75rem; font-weight: 500; transition: all 0.12s ease; .icon { font-size: 0.9rem; } &:hover { background-color: var(--active-icon-color); } &.danger { color: var(--danger-color); &:hover { background-color: color-mix(in srgb, var(--danger-color) 15%, transparent); } } } .lsp-server-list { list-style: none; padding: 8px; margin: 0; } .lsp-server-item { display: flex; align-items: center; gap: 12px; padding: 14px 12px; border-radius: 6px; cursor: pointer; transition: background-color 0.12s ease; color: var(--popup-text-color); &:hover { background-color: var(--box-shadow-color); } &:active { background-color: var(--active-icon-color); } &:not(:last-child) { border-bottom: 1px solid var(--border-color); } } .lsp-arrow { font-size: 1.3rem; opacity: 0.35; margin-left: auto; } .lsp-status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .lsp-server-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .lsp-server-name { font-size: 0.95rem; font-weight: 500; color: var(--popup-text-color); } .lsp-server-status { font-size: 0.72rem; color: var(--popup-text-color); opacity: 0.5; text-transform: capitalize; } .lsp-error-badge { display: flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; background-color: var(--danger-color); color: var(--danger-text-color); font-size: 0.65rem; font-weight: 600; border-radius: 9px; } .lsp-details { display: flex; flex-direction: column; } .lsp-details-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--border-color); } .lsp-header-actions { display: flex; gap: 4px; margin-left: auto; } .lsp-icon-btn { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: transparent; border: none; color: var(--popup-text-color); cursor: pointer; border-radius: 6px; padding: 0; transition: all 0.12s ease; opacity: 0.7; &:hover { background-color: var(--box-shadow-color); opacity: 1; } &:active { background-color: var(--active-icon-color); } .icon { font-size: 1.1rem; } &.small { width: 26px; height: 26px; .icon { font-size: 0.95rem; } } &.danger { color: var(--danger-color); &:hover { background-color: color-mix(in srgb, var(--danger-color) 15%, transparent); } } } .lsp-details-title { display: flex; align-items: center; gap: 10px; font-size: 0.95rem; font-weight: 500; color: var(--popup-text-color); } .lsp-section { padding: 10px 14px; &:last-child { border-bottom: none; } } .lsp-section-label { font-size: 0.65rem; font-weight: 600; color: var(--popup-text-color); opacity: 0.5; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; } .lsp-chip-container { display: flex; flex-wrap: wrap; gap: 5px; } .lsp-chip { display: inline-flex; align-items: center; padding: 4px 10px; background: var(--box-shadow-color); color: var(--popup-text-color); border-radius: 4px; font-size: 0.7rem; font-weight: 500; &.ext { font-family: monospace; font-weight: 600; background: color-mix(in srgb, var(--active-color) 15%, transparent); color: var(--active-color); } } .lsp-project-path { font-size: 0.75rem; font-family: monospace; color: var(--popup-text-color); opacity: 0.75; word-break: break-all; padding: 8px 10px; background-color: var(--box-shadow-color); border-radius: 6px; line-height: 1.3; } .lsp-logs-section { margin: 8px 14px 12px; &.collapsed { .lsp-logs-container { display: none; } .lsp-expand-icon { transform: rotate(-90deg); } .lsp-clear-btn { display: none; } } } .lsp-logs-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; padding: 6px 8px; margin: 0 -8px; } .lsp-logs-title { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; font-weight: 500; color: var(--popup-text-color); opacity: 0.7; } .lsp-expand-icon { font-size: 1.1rem; transition: transform 0.15s ease; } .lsp-log-count { font-size: 0.65rem; opacity: 0.5; margin-left: 2px; } .lsp-logs-actions { display: flex; gap: 2px; } .lsp-logs-container { max-height: 160px; overflow-y: auto; background-color: var(--box-shadow-color); border-radius: 6px; font-family: monospace; font-size: 0.65rem; margin-top: 4px; &::-webkit-scrollbar { width: 3px; } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: var(--scrollbar-color); border-radius: 2px; } } .lsp-logs-empty { padding: 14px; text-align: center; color: var(--popup-text-color); opacity: 0.4; font-size: 0.7rem; font-family: inherit; } .lsp-log { display: flex; gap: 6px; padding: 1px 8px; line-height: 1.3; &:first-child { padding-top: 4px; } &:last-child { padding-bottom: 4px; } &.error .lsp-log-text { color: var(--danger-color); } &.warn .lsp-log-text { color: var(--error-text-color); } &.stderr .lsp-log-text { color: var(--error-text-color); } } .lsp-log-time { flex-shrink: 0; color: var(--popup-text-color); opacity: 0.35; font-size: 0.6rem; } .lsp-log-text { flex: 1; color: var(--popup-text-color); opacity: 0.85; word-break: break-word; } .lsp-stats-container { display: flex; gap: 16px; } .lsp-stat { display: flex; align-items: baseline; gap: 6px; } .lsp-stat-label { font-size: 0.65rem; font-weight: 500; color: var(--popup-text-color); opacity: 0.75; } .lsp-stat-value { font-size: 0.8rem; font-weight: 600; color: var(--popup-text-color); font-family: monospace; } ================================================ FILE: src/components/lspStatusBar/index.js ================================================ import "./style.scss"; /**@type {HTMLElement | null} */ let $statusBar = null; /**@type {number | null} */ let hideTimeout = null; /** * @typedef {Object} ProgressItem * @property {string} title - Task title * @property {string} [message] - Current message * @property {number} [percentage] - Progress percentage (0-100) */ /**@type {Map} */ const activeProgress = new Map(); /**@type {string | null} */ let currentServerId = null; /**@type {string | null} */ let currentServerLabel = null; /** * @typedef {Object} LspStatusOptions * @property {string} message - The status message to display * @property {string} [icon] - Optional icon class name * @property {'info' | 'success' | 'warning' | 'error'} [type='info'] - Status type * @property {number | false} [duration=0] - Duration in ms, 0 for default (5000ms), false for persistent * @property {boolean} [showProgress=false] - Whether to show a progress indicator * @property {number} [progress] - Progress percentage (0-100) * @property {string} [title] - Optional title for the status * @property {string} [id] - Unique identifier for progress tracking */ /** * Ensure the status bar exists * @returns {HTMLElement} */ function ensureStatusBar() { if ($statusBar && document.body.contains($statusBar)) { return $statusBar; } $statusBar = (
        ); // Find the quicktools footer to insert before it const $footer = document.getElementById("quick-tools"); if ($footer && $footer.parentNode) { $footer.parentNode.insertBefore($statusBar, $footer); } else { // Fallback: append to app const $app = document.getElementById("app") || document.body; $app.appendChild($statusBar); } return $statusBar; } /** * Build aggregated message from all active progress items * @returns {{ message: string, avgProgress: number | null, taskCount: number }} */ function buildAggregatedStatus() { const items = Array.from(activeProgress.values()); const taskCount = items.length; if (taskCount === 0) { return { message: "", avgProgress: null, taskCount: 0 }; } // Calculate average progress for items that have percentage const itemsWithProgress = items.filter( (item) => typeof item.percentage === "number", ); const avgProgress = itemsWithProgress.length > 0 ? Math.round( itemsWithProgress.reduce( (sum, item) => sum + (item.percentage || 0), 0, ) / itemsWithProgress.length, ) : null; // Build message if (taskCount === 1) { const item = items[0]; const parts = []; if (item.message) { parts.push(item.message); } else if (item.title) { parts.push(item.title); } return { message: parts.join(" "), avgProgress, taskCount }; } // Multiple tasks - show count and maybe the most recent message const latestWithMessage = items.filter((item) => item.message).pop(); const message = latestWithMessage ? `${taskCount} tasks: ${latestWithMessage.message}` : `${taskCount} tasks running`; return { message, avgProgress, taskCount }; } /** * Update the status bar display */ function updateStatusBarDisplay() { const bar = $statusBar; if (!bar) return; const { message, avgProgress, taskCount } = buildAggregatedStatus(); if (taskCount === 0) { hideStatusBar(); return; } const $title = bar.querySelector(".lsp-status-title"); const $message = bar.querySelector(".lsp-status-message"); const $progressText = bar.querySelector(".lsp-status-progress-text"); const $progressContainer = bar.querySelector(".lsp-status-progress"); const $icon = bar.querySelector(".lsp-status-icon"); if ($title) $title.textContent = currentServerLabel || ""; if ($message) $message.textContent = message; if (avgProgress !== null && $progressText && $progressContainer) { $progressText.textContent = `${avgProgress}%`; $progressContainer.style.display = ""; } else if ($progressContainer) { $progressContainer.style.display = "none"; } // Show spinning icon while progress is active if ($icon) { $icon.className = "lsp-status-icon icon autorenew"; } bar.className = "lsp-status info"; bar.classList.remove("hiding"); } /** * Hide the status bar */ function hideStatusBar() { if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; } if ($statusBar) { $statusBar.classList.add("hiding"); setTimeout(() => { if ($statusBar) { $statusBar.remove(); $statusBar = null; } }, 300); } } /** * Show LSP status notification * @param {LspStatusOptions} options - Status options * @returns {string | undefined} The status ID for later updates/removal */ export function showLspStatus(options) { const { message, icon = "autorenew", type = "info", duration = 0, showProgress = false, progress, title, id, } = options; // Clear any existing hide timeout if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; } // If this is a progress item (has id), track it if (id && id.includes("-progress-")) { // Extract server info from id (format: serverId-progress-token) const serverMatch = id.match(/^(.+?)-progress-/); if (serverMatch) { currentServerId = serverMatch[1]; currentServerLabel = title || currentServerId; } activeProgress.set(id, { title: title || "", message: message || "", percentage: progress, }); ensureStatusBar(); updateStatusBarDisplay(); return id; } // For non-progress messages (errors, warnings, etc.) const bar = ensureStatusBar(); const $title = bar.querySelector(".lsp-status-title"); const $message = bar.querySelector(".lsp-status-message"); const $progressText = bar.querySelector(".lsp-status-progress-text"); const $progressContainer = bar.querySelector(".lsp-status-progress"); const $icon = bar.querySelector(".lsp-status-icon"); if ($title) $title.textContent = title || ""; if ($message) $message.textContent = message; const hasProgress = showProgress && typeof progress === "number"; if (hasProgress && $progressText && $progressContainer) { $progressText.textContent = `${Math.round(progress)}%`; $progressContainer.style.display = ""; } else if ($progressContainer) { $progressContainer.style.display = "none"; } if ($icon) { $icon.className = `lsp-status-icon icon ${icon}`; } bar.className = `lsp-status ${type}`; bar.classList.remove("hiding"); // Auto-hide after duration unless duration is false if (duration !== false) { const ms = duration || 5000; hideTimeout = window.setTimeout(() => { // Only hide if no progress is active if (activeProgress.size === 0) { hideStatusBar(); } }, ms); } return id; } /** * Hide a specific progress item by ID * @param {string} id - The progress ID to hide */ export function hideStatus(id) { if (activeProgress.has(id)) { activeProgress.delete(id); if (activeProgress.size === 0) { // All progress complete - hide after a brief delay hideTimeout = window.setTimeout(() => { hideStatusBar(); }, 500); } else { updateStatusBarDisplay(); } } } /** * Hide the LSP status bar (legacy support - hides all) */ export function hideLspStatus() { activeProgress.clear(); hideStatusBar(); } /** * Update a progress item * @param {Partial & { id?: string }} options - Options to update * @returns {string | null} The status ID */ export function updateLspStatus(options) { const { id, message, progress } = options; if (!id || !activeProgress.has(id)) { return null; } const item = activeProgress.get(id); if (item) { if (message !== undefined) item.message = message; if (progress !== undefined) item.percentage = progress; activeProgress.set(id, item); updateStatusBarDisplay(); } return id; } /** * Check if status bar is currently visible * @returns {boolean} */ export function isLspStatusVisible() { return $statusBar !== null && document.body.contains($statusBar); } /** * Get count of active progress items * @returns {number} */ export function getActiveStatusCount() { return activeProgress.size; } /** * Check if a specific progress item exists * @param {string} id - The progress ID to check * @returns {boolean} */ export function hasStatus(id) { return activeProgress.has(id); } export default { show: showLspStatus, hide: hideLspStatus, hideById: hideStatus, update: updateLspStatus, isVisible: isLspStatusVisible, getActiveCount: getActiveStatusCount, has: hasStatus, }; ================================================ FILE: src/components/lspStatusBar/style.scss ================================================ @use "../../styles/mixins.scss"; #lsp-status-bar { position: fixed; bottom: 50px; left: 0; right: 0; margin: 0 auto; max-width: 95vw; width: fit-content; min-width: 200px; padding: 8px 12px; padding-right: 36px; background-color: var(--secondary-color); color: var(--secondary-text-color); border-radius: 6px; box-shadow: 0 2px 12px var(--box-shadow-color); border: 1px solid var(--border-color); z-index: 97; animation: lspStatusSlideUp 0.3s ease-out; transition: all 0.3s ease; box-sizing: border-box; &.hiding { animation: lspStatusSlideDown 0.3s ease-out forwards; } .lsp-status-content { display: flex; align-items: center; gap: 10px; } .lsp-status-icon { width: 20px; height: 20px; font-size: 1rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0; &.autorenew { animation: lspSpinSlow 2s linear infinite; } } .lsp-status-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; } .lsp-status-title { font-size: 0.75rem; font-weight: 600; color: var(--primary-text-color); text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.8; } .lsp-status-message { font-size: 0.85rem; font-weight: 400; color: var(--secondary-text-color); line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60vw; } .lsp-status-progress { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-shrink: 0; } .lsp-status-progress-text { font-size: 0.8rem; font-weight: 500; color: var(--active-color); min-width: 36px; text-align: right; } .lsp-status-close { position: absolute; top: 50%; right: 6px; transform: translateY(-50%); width: 24px; height: 24px; font-size: 0.9rem; display: flex; align-items: center; justify-content: center; background: transparent; border: none; color: var(--secondary-text-color); cursor: pointer; border-radius: 4px; transition: all 0.2s ease; padding: 0; &:hover, &:active { background-color: rgba(0, 0, 0, 0.1); color: var(--primary-text-color); } } // Status type colors &.info { .lsp-status-icon { color: var(--active-color); } } &.success { .lsp-status-icon { color: #48c158; } .lsp-status-progress-text { color: #48c158; } } &.warning { .lsp-status-icon { color: #f59e0b; } } &.error { .lsp-status-icon { color: var(--error-text-color); } } } // Adjust position when quicktools has different heights wc-page[footer-height="1"] #lsp-status-bar { bottom: 50px; } wc-page[footer-height="2"] #lsp-status-bar { bottom: 90px; } wc-page[footer-height="3"] #lsp-status-bar { bottom: 130px; } // When quicktools is hidden wc-page:not([footer-height]) #lsp-status-bar { bottom: 10px; } @keyframes lspStatusSlideUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes lspStatusSlideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(100%); opacity: 0; } } @keyframes lspSpinSlow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ================================================ FILE: src/components/page.js ================================================ import WCPage from "./WebComponents/wcPage"; /** * * @param {string} title * @param {object} options * @param {HTMLElement} [options.lead] type of page * @param {HTMLElement} [options.tail] type of page * @returns {WCPage} */ function Page(title, options = {}) { let page = ; page.append = page.appendBody; page.initializeIfNotAlreadyInitialized(); page.settitle(title); if (options.tail) { page.header.append(options.tail); } if (options.lead) { page.lead = options.lead; } return page; } export default Page; ================================================ FILE: src/components/palette/index.js ================================================ import "./style.scss"; import inputhints from "components/inputhints"; import keyboardHandler from "handlers/keyboard"; import actionStack from "lib/actionStack"; import restoreTheme from "lib/restoreTheme"; /** * @typedef {import('./inputhints').HintCallback} HintCallback * @typedef {import('./inputhints').HintModification} HintModification */ /* Benchmark to show keyboard When not using keyboardHideStart event; ============================================= Time taken to remove palette: 104 index.js:177 Time taken to show keyboard: 198 index.js:178 Total time taken: 302 When using keyboardHideStart event; ============================================= index.js:150 Time taken to remove palette: 0 index.js:177 Time taken to show keyboard: 187 index.js:178 Total time taken: 188 When not using keyboardHideStart event; ============================================= index.js:150 Time taken to remove palette: 105 index.js:177 Time taken to show keyboard: 203 index.js:178 Total time taken: 310 When using keyboardHideStart event; ============================================= index.js:150 Time taken to remove palette: 0 index.js:177 Time taken to show keyboard: 176 index.js:178 Total time taken: 176 This shows that using keyboardHideStart event is faster than not using it. */ /** * Opens a palette with input and hints * @param {(hints:HintModification)=>string[]} getList Callback to get list of hints * @param {()=>string} onsSelectCb Callback to call when a hint is selected * @param {string} placeholder Placeholder for input * @param {function} onremove Callback to call when palette is removed * @returns {void} */ // Track active palette for chaining let activePalette = null; export default function palette(getList, onsSelectCb, placeholder, onremove) { // Store previous palette if exists const previousPalette = activePalette; const isChained = !!previousPalette; /**@type {HTMLInputElement} */ const $input = ( ); /**@type {HTMLElement} */ const $mask =
        ; /**@type {HTMLDivElement} */ const $palette =
        {$input}
        ; // Create a palette with input and hints inputhints($input, generateHints, onSelect); // Only set the darkened theme when this is not a chained palette if (!isChained) { // Removes the darkened color from status bar and navigation bar restoreTheme(true); } // Remove palette when input is blurred $input.addEventListener("blur", remove); // Don't wait for input to blur when keyboard hides, remove is // as soon as keyboard starts to hide keyboardHandler.on("keyboardHideStart", remove); // Add to DOM app.append($palette, $mask); // If we're in a chained palette, ensure we don't lose focus if (isChained) { // Don't let any blur events from previous palette affect this one setTimeout(() => { $input.focus(); }, 0); } // Focus input to show options $input.focus(); // Trigger input event to show hints immediately $input.dispatchEvent(new Event("input")); // Add to action stack to remove on back button actionStack.push({ id: "palette", action: remove, }); // Store this palette as the active one for chaining activePalette = { remove }; /** * On select callback for inputhints * @param {string} value */ function onSelect(value) { const currentPalette = { remove }; activePalette = currentPalette; onsSelectCb(value); if (activePalette === currentPalette) { remove(); } } /** * Keydown event handler for input * @param {KeyboardEvent} e */ function onkeydown(e) { if (e.key !== "Escape") return; remove(); } /** * Generates hint for inputhints * @param {HintCallback} setHints Set hints callback * @param {HintModification} hintModification Hint modification object */ async function generateHints(setHints, hintModification) { const list = getList(hintModification); const data = list instanceof Promise ? await list : list; setHints(data); } /** * Removes the palette */ function remove() { actionStack.remove("palette"); keyboardHandler.off("keyboardHideStart", remove); $input.removeEventListener("blur", remove); $palette.remove(); $mask.remove(); // Restore previous palette if chained if (isChained && previousPalette) { activePalette = previousPalette; } else { activePalette = null; restoreTheme(); } if (typeof onremove === "function") { onremove(); return; } // If not chained or last in chain, focus the editor if (!isChained) { const { activeFile, editor } = editorManager; if (activeFile.wasFocused) { editor.focus(); } } remove = () => { window.log("warn", "Palette already removed."); }; } } ================================================ FILE: src/components/palette/style.scss ================================================ #palette { position: fixed; top: 0; left: 0; right: 0; margin: auto; height: 40px; width: 90vw; max-width: 600px; z-index: 99; display: flex; align-items: center; background-color: inherit; box-shadow: 0 0 4px black; z-index: 999; &~#hints { width: 90vw; box-sizing: border-box; z-index: 999; } &~.mask { opacity: 0.4; z-index: 998; // pointer-events: none; } input { width: 100%; border: none; color: var(--primary-text-color); } } ================================================ FILE: src/components/quickTools/footer.js ================================================ /** * @typedef {import('html-tag-js/ref')} Ref */ import settings from "lib/settings"; import items, { ref } from "./items"; /** * Create a row with common buttons * @param {object} param0 Attributes * @param {number} [param0.row] Row number */ export const Row = ({ row }) => { const startIndex = (row - 1) * settings.QUICKTOOLS_GROUP_CAPACITY * settings.QUICKTOOLS_GROUPS; return (
        {(() => { const sections = []; for (let i = 0; i < settings.QUICKTOOLS_GROUPS; ++i) { const section = []; for (let j = 0; j < settings.QUICKTOOLS_GROUP_CAPACITY; ++j) { const index = startIndex + (i * settings.QUICKTOOLS_GROUP_CAPACITY + j); const itemIndex = settings.value.quicktoolsItems[index]; // saved item index const item = items[itemIndex]; // item object section.push(); } sections.push(
        {section}
        ); } return sections; })()}
        ); }; /** * Create a search row with search input and buttons * @returns {Element} */ export const SearchRow1 = ({ inputRef }) => (
        ); /** * Create a search row with replace input and buttons * @returns {Element} */ export const SearchRow2 = ({ inputRef, posRef, totalRef }) => (
        0 of 0
        ); /**@type {HTMLElement} */ export const $footer =
        ; /**@type {HTMLElement} */ export const $toggler = ( ); /**@type {HTMLTextAreaElement} */ export const $input = ( ); /** * * @param {RowItem} param0 Attributes * @param {string} param0.id Button id * @param {string} param0.icon Icon name * @param {string} param0.letters Letters to show on button * @param {'insert'|'command'|'key'|'custom'} param0.action Action type * @param {string|Function} param0.value Value of button * @param {Ref} param0.ref Reference to button * @param {boolean} param0.repeat Whether to repeat the action or not * @returns {HTMLButtonElement} */ export function RowItem({ id, icon, letters, action, value, ref, repeat }) { const $item = ( ); if (typeof value === "function") { $item.value = value; } else if (value !== undefined) { $item.dataset.value = value; } return $item; } /** * Create a list of RowItem components * @param {object} param0 Attributes * @param {Array} param0.extras Extra buttons * @returns {Array} */ function Extras({ extras }) { const div =
        ; if (Array.isArray(extras)) { extras.forEach((i) => { if (i instanceof HTMLElement) { div.appendChild(i); return; } div.append(); }); } return div; } /** * @typedef {object} RowItem * @property {string} icon * @property {string} letters * @property {'insert'|'command'|'key'|'custom'} action * @property {string|Function} value * @property {Ref} ref */ ================================================ FILE: src/components/quickTools/index.js ================================================ import "./style.scss"; import Ref from "html-tag-js/ref"; import settings from "lib/settings"; import { $footer, $input, $toggler, Row, SearchRow1, SearchRow2, } from "./footer"; /**@type {HTMLElement} */ let $row1; /**@type {HTMLElement} */ let $row2; /**@type {HTMLElement} */ let $searchRow1; /**@type {HTMLElement} */ let $searchRow2; const $searchInput = Ref(); const $replaceInput = Ref(); const $searchPos = Ref(); const $searchTotal = Ref(); export default { get $footer() { return $footer; }, get $row1() { if ($row1) return $row1; $row1 = ; settings.on("update:quicktoolsItems:after", () => { $row1 = ; }); return $row1; }, get $row2() { if ($row2) return $row2; $row2 = ; settings.on("update:quicktoolsItems:after", () => { $row2 = ; }); return $row2; }, get $searchRow1() { if ($searchRow1) return $searchRow1; $searchRow1 = ; return $searchRow1; }, get $searchRow2() { if ($searchRow2) return $searchRow2; $searchRow2 = ( ); return $searchRow2; }, get $input() { return $input; }, get $toggler() { return $toggler; }, get $searchInput() { return $searchInput; }, get $replaceInput() { return $replaceInput; }, get $searchPos() { return $searchPos; }, get $searchTotal() { return $searchTotal; }, }; ================================================ FILE: src/components/quickTools/items.js ================================================ export default [ item("ctrl-key", "letters", "ctrl", undefined, "ctrl", false), item("tab-key", "keyboard_tab", "key", 9), item("shift-key", "letters", "shift", undefined, "shft", false), item("undo", "undo", "command", "undo"), item("redo", "redo", "command", "redo"), item("search", "search", "search"), item("save", "save", "command", "saveFile", undefined, false), item("esc-key", "letters", "key", 27, "esc"), item("curlybracket", "letters", "insert", "{", "{"), item("curlybracket", "letters", "insert", "}", "}"), item("squarebracket", "letters", "insert", "[", "["), item("squarebracket", "letters", "insert", "]", "]"), item("parentheses", "letters", "insert", "(", "("), item("parentheses", "letters", "insert", ")", ")"), item("anglebracket", "letters", "insert", "<", "<"), item("anglebracket", "letters", "insert", ">", ">"), item("left-arrow-key", "keyboard_arrow_left", "key", 37, undefined, true), item("right-arrow-key", "keyboard_arrow_right", "key", 39, undefined, true), item("up-arrow-key", "keyboard_arrow_up", "key", 38, undefined, true), item("down-arrow-key", "keyboard_arrow_down", "key", 40, undefined, true), item("moveline-up", "moveline-up", "command", "movelinesup"), item("moveline-down", "moveline-down", "command", "movelinesdown"), item("copyline-up", "copyline-up", "command", "copylinesup"), item("copyline-down", "copyline-down", "command", "copylinesdown"), item("semicolon", "letters", "insert", ";", ";"), item("quotation", "letters", "insert", "'", "'"), item("quotation", "letters", "insert", '"', '"'), item("and", "letters", "insert", "&", "&"), item("bar", "letters", "insert", "|", "|"), item("equal", "letters", "insert", "=", "="), item("slash", "letters", "insert", "/", "/"), item("exclamation", "letters", "insert", "!", "!"), item("command-palette", "keyboard_control", "command", "openCommandPalette"), item("alt-key", "letters", "alt", undefined, "alt", false), item("meta-key", "letters", "meta", undefined, "meta", false), item("home-key", "letters", "key", 36, "home", true), item("end-key", "letters", "key", 35, "end", true), item("pageup-key", "letters", "key", 33, "pgup", true), item("pagedown-key", "letters", "key", 34, "pgdn", true), item("delete-key", "letters", "key", 46, "del", true), item("tilde", "letters", "insert", "~", "~"), item("backtick", "letters", "insert", "`", "`"), item("hash", "letters", "insert", "#", "#"), item("dollar", "letters", "insert", "$", "$"), item("modulo", "letters", "insert", "%", "%"), item("caret", "letters", "insert", "^", "^"), item("hyphen", "letters", "insert", "-", "-"), ]; /** * Get description of a button * @param {string} id button id * @returns */ export function description(id) { return strings[`quicktools:${id}`]; } /** * * @param {string} icon * @param {string} action * @param {string|number} value * @param {string} letters * @param {boolean} repeat * @returns */ function item(id, icon, action, value, letters, repeat) { return { id, icon, action, value, letters, repeat, }; } ================================================ FILE: src/components/quickTools/style.scss ================================================ @use '../../styles/mixins.scss'; #quick-tools { .icon.click { @include mixins.active-icon; transition: all 0.3s ease-in-out; transform: scale(1.2); } &[data-alt="true"] { [data-id="alt-key"] { @include mixins.active-icon; } } &[data-shift="true"] { [data-id="shift-key"] { @include mixins.active-icon; } } &[data-ctrl="true"] { [data-id="ctrl-key"] { @include mixins.active-icon; } } &[data-meta="true"] { [data-id="meta-key"] { @include mixins.active-icon; } } &[data-unsaved="true"] { [data-id="save"] { @include mixins.icon-badge; } } } ================================================ FILE: src/components/referencesPanel/index.js ================================================ import "./styles.scss"; import actionStack from "lib/actionStack"; import { openReferencesTab } from "./referencesTab"; import { buildFlatList, clearHighlightCache, createReferenceItem, getReferencesStats, navigateToReference, sanitize, } from "./utils"; let currentPanel = null; function createReferencesPanel() { const state = { visible: false, expanded: false, loading: true, symbolName: "", references: [], collapsedFiles: new Set(), flatItems: [], }; const $mask = ; const $panel =
        ; const $dragHandle =
        ; const $title =
        ; const $subtitle = ; const $content =
        ; const $refList =
        ; const $openTabBtn = ( ); const $closeBtn = ( ); const $header = (
        {$title} {$subtitle}
        {$openTabBtn} {$closeBtn}
        ); $panel.append($dragHandle, $header, $content); $mask.onclick = hide; let startY = 0; let currentY = 0; let isDragging = false; $dragHandle.ontouchstart = onDragStart; $dragHandle.onmousedown = onDragStart; function onDragStart(e) { isDragging = true; startY = e.touches ? e.touches[0].clientY : e.clientY; currentY = startY; $panel.style.transition = "none"; document.addEventListener("touchmove", onDragMove, { passive: false }); document.addEventListener("mousemove", onDragMove); document.addEventListener("touchend", onDragEnd); document.addEventListener("mouseup", onDragEnd); } function onDragMove(e) { if (!isDragging) return; e.preventDefault(); currentY = e.touches ? e.touches[0].clientY : e.clientY; const deltaY = currentY - startY; if (deltaY > 0) { $panel.style.transform = `translateY(${deltaY}px)`; } else if (!state.expanded) { const expansion = Math.min(Math.abs(deltaY), 100); $panel.style.maxHeight = `${60 + (expansion / 100) * 25}vh`; } } function onDragEnd() { isDragging = false; document.removeEventListener("touchmove", onDragMove); document.removeEventListener("mousemove", onDragMove); document.removeEventListener("touchend", onDragEnd); document.removeEventListener("mouseup", onDragEnd); $panel.style.transition = ""; const deltaY = currentY - startY; if (deltaY > 100) { hide(); } else if (deltaY < -50 && !state.expanded) { state.expanded = true; $panel.classList.add("expanded"); $panel.style.transform = ""; $panel.style.maxHeight = ""; } else { $panel.style.transform = ""; $panel.style.maxHeight = ""; } } function setTitle(symbolName) { $title.innerHTML = ""; $title.append( , References to , {sanitize(symbolName)}, ); } function setSubtitle(text) { $subtitle.textContent = text; } function openInTab() { const refs = state.references; const sym = state.symbolName; hide(); openReferencesTab({ symbolName: sym, references: refs, }); } function toggleFile(uri) { if (state.collapsedFiles.has(uri)) { state.collapsedFiles.delete(uri); } else { state.collapsedFiles.add(uri); } renderReferencesList(); } function renderLoading() { $content.innerHTML = ""; $content.append(
        Finding references...
        , ); } function renderEmpty() { $content.innerHTML = ""; $content.append(
        No references found
        , ); } function renderReferencesList() { $refList.innerHTML = ""; const visibleItems = state.flatItems.filter((item) => { if (item.type === "file-header") return true; return !state.collapsedFiles.has(item.uri); }); const fragment = document.createDocumentFragment(); for (const item of visibleItems) { const $el = createReferenceItem(item, { collapsedFiles: state.collapsedFiles, onToggleFile: toggleFile, onNavigate: (ref) => { hide(); navigateToReference(ref); }, }); fragment.appendChild($el); } $refList.appendChild(fragment); $content.innerHTML = ""; $content.appendChild($refList); } async function renderReferences() { $content.innerHTML = ""; $content.append(
        Highlighting code...
        , ); const stats = getReferencesStats(state.references); setSubtitle(stats.text); state.flatItems = await buildFlatList(state.references, state.symbolName); renderReferencesList(); } function show(options = {}) { if (currentPanel && currentPanel !== panelInstance) { currentPanel.hide(); } currentPanel = panelInstance; state.symbolName = options.symbolName || ""; state.references = []; state.loading = true; state.expanded = false; state.collapsedFiles.clear(); state.flatItems = []; clearHighlightCache(); setTitle(state.symbolName); setSubtitle("Searching..."); renderLoading(); document.body.append($mask, $panel); requestAnimationFrame(() => { $mask.classList.add("visible"); $panel.classList.add("visible"); $panel.classList.remove("expanded"); }); state.visible = true; actionStack.push({ id: "references-panel", action: hide, }); } function hide() { if (!state.visible) return; state.visible = false; $mask.classList.remove("visible"); $panel.classList.remove("visible"); actionStack.remove("references-panel"); setTimeout(() => { $mask.remove(); $panel.remove(); }, 250); if (currentPanel === panelInstance) { currentPanel = null; } } function setReferences(references) { state.loading = false; state.references = references || []; if (state.references.length === 0) { setSubtitle("No references found"); renderEmpty(); } else { renderReferences(); } } function setError(message) { state.loading = false; setSubtitle("Error"); $content.innerHTML = ""; $content.append(
        {sanitize(message)}
        , ); } const panelInstance = { show, hide, setReferences, setError, get visible() { return state.visible; }, }; return panelInstance; } let panelSingleton = null; function getPanel() { if (!panelSingleton) { panelSingleton = createReferencesPanel(); } return panelSingleton; } export function showReferencesPanel(options) { const panel = getPanel(); panel.show(options); return panel; } export function hideReferencesPanel() { const panel = getPanel(); panel.hide(); } export { openReferencesTab }; export default { show: showReferencesPanel, hide: hideReferencesPanel, getPanel, openReferencesTab, }; ================================================ FILE: src/components/referencesPanel/referencesTab.js ================================================ import EditorFile from "lib/editorFile"; import { buildFlatList, clearHighlightCache, createReferenceItem, getReferencesStats, navigateToReference, sanitize, } from "./utils"; export function createReferencesTab(options = {}) { const { symbolName = "", references = [], flatItems: prebuiltItems = null, } = options; const collapsedFiles = new Set(); let flatItems = prebuiltItems || []; let isInitialized = false; const $container =
        ; const $listContainer =
        ; const $refList =
        ; const stats = getReferencesStats(references); const $header = (
        References to {sanitize(symbolName)} {stats.text}
        ); const $loadingState = (
        Highlighting code...
        ); $container.append($header, $listContainer); function getVisibleItems() { return flatItems.filter((item) => { if (item.type === "file-header") return true; return !collapsedFiles.has(item.uri); }); } function toggleFile(uri) { if (collapsedFiles.has(uri)) { collapsedFiles.delete(uri); } else { collapsedFiles.add(uri); } renderList(); } function renderList() { $refList.innerHTML = ""; const visibleItems = getVisibleItems(); const fragment = document.createDocumentFragment(); for (const item of visibleItems) { const $el = createReferenceItem(item, { collapsedFiles, onToggleFile: toggleFile, onNavigate: navigateToReference, }); fragment.appendChild($el); } $refList.appendChild(fragment); } async function init() { if (isInitialized) return; isInitialized = true; if (!prebuiltItems || prebuiltItems.length === 0) { $listContainer.appendChild($loadingState); flatItems = await buildFlatList(references, symbolName); $loadingState.remove(); } renderList(); $listContainer.appendChild($refList); } function destroy() { $refList.innerHTML = ""; } return { container: $container, init, destroy, get symbolName() { return symbolName; }, get referenceCount() { return references.length; }, }; } export async function openReferencesTab(options = {}) { const { symbolName = "", references = [] } = options; const tabName = `Refs: ${symbolName}`; const existingFile = editorManager.getFile(tabName, "filename"); if (existingFile) { existingFile.makeActive(); return existingFile; } clearHighlightCache(); const flatItems = await buildFlatList(references, symbolName); const stats = getReferencesStats(references); const tabView = createReferencesTab({ symbolName, references, flatItems }); const file = new EditorFile(tabName, { type: "terminal", content: tabView.container, tabIcon: "icon linkinsert_link", render: true, }); file.setCustomTitle(() => stats.text); tabView.init(); file.on("close", () => { tabView.destroy(); }); return file; } export default { createReferencesTab, openReferencesTab, }; ================================================ FILE: src/components/referencesPanel/styles.scss ================================================ @use "../../styles/mixins.scss"; .references-panel-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.4); z-index: 100; opacity: 0; transition: opacity 200ms ease-out; pointer-events: none; &.visible { opacity: 1; pointer-events: auto; } } .references-panel { position: fixed; left: 0; right: 0; bottom: 0; z-index: 101; background-color: var(--popup-background-color); border-top-left-radius: 12px; border-top-right-radius: 12px; box-shadow: 0 -4px 20px var(--box-shadow-color); transform: translateY(100%); transition: transform 250ms ease-out, max-height 200ms ease-out; display: flex; flex-direction: column; max-height: 60vh; overflow: hidden; &.visible { transform: translateY(0); } &.expanded { max-height: 85vh; } .drag-handle { display: flex; justify-content: center; align-items: center; padding: 8px 0 4px 0; cursor: grab; flex-shrink: 0; &::after { content: ""; width: 36px; height: 4px; background-color: var(--border-color); border-radius: 2px; } &:active { cursor: grabbing; } } .panel-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px 12px 16px; border-bottom: 1px solid var(--border-color); min-height: 44px; flex-shrink: 0; .header-content { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; .header-title { display: flex; align-items: center; gap: 8px; font-size: 1rem; font-weight: 600; color: var(--primary-text-color); .icon { font-size: 1.1em; opacity: 0.7; } .symbol-name { font-family: monospace; background-color: var(--secondary-color); padding: 2px 8px; border-radius: 4px; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } .header-subtitle { font-size: 0.85rem; color: var(--secondary-text-color); opacity: 0.8; } } .header-actions { display: flex; gap: 4px; flex-shrink: 0; } .action-btn { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--secondary-text-color); border-radius: 50%; cursor: pointer; &:active { background-color: var(--active-icon-color); } .icon { font-size: 1.2em; } &.open-tab-btn { color: var(--active-color); } } } .panel-content { flex: 1; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; &::-webkit-scrollbar { width: var(--scrollbar-width, 4px); } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: var(--scrollbar-color, rgba(0, 0, 0, 0.333)); border-radius: calc(var(--scrollbar-width, 4px) / 2); } } .loading-state { display: flex; align-items: center; justify-content: center; padding: 32px 16px; color: var(--secondary-text-color); gap: 12px; .loader { @include mixins.circular-loader(24px); } } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; color: var(--secondary-text-color); text-align: center; .icon { font-size: 2rem; margin-bottom: 8px; opacity: 0.5; } } } .ref-file-header, .ref-item { box-sizing: border-box; display: flex; align-items: center; will-change: transform; } .ref-file-header { gap: 8px; padding: 0 16px; cursor: pointer; user-select: none; background-color: color-mix(in srgb, var(--secondary-color) 50%, var(--primary-color)); &:active { background-color: var(--active-icon-color); } .chevron { font-size: 1rem; color: var(--secondary-text-color); transition: transform 150ms ease; width: 18px; text-align: center; flex-shrink: 0; } &.collapsed .chevron { transform: rotate(-90deg); } .file-icon { font-size: 1rem; flex-shrink: 0; } .file-name { flex: 1; font-size: 0.9rem; font-weight: 500; color: var(--primary-text-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .ref-count { font-size: 0.75rem; color: var(--secondary-text-color); background-color: var(--popup-background-color); padding: 2px 8px; border-radius: 10px; flex-shrink: 0; } } .ref-item { gap: 10px; padding: 0 16px 0 36px; cursor: pointer; &:active { background-color: var(--active-icon-color); } .line-number { font-size: 0.75rem; color: var(--secondary-text-color); min-width: 32px; text-align: right; font-family: monospace; flex-shrink: 0; opacity: 0.7; } .ref-preview { flex: 1; font-size: 0.85rem; font-family: monospace; color: var(--primary-text-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.4; min-width: 0; direction: rtl; text-align: left; span { direction: ltr; unicode-bidi: bidi-override; } .symbol-match { background-color: color-mix(in srgb, var(--active-color) 30%, transparent); border-radius: 2px; padding: 0 2px; } } } .references-tab-container { display: flex; flex-direction: column; height: 100%; background-color: var(--primary-color); color: var(--primary-text-color); .references-tab-header { padding: 16px; border-bottom: 1px solid var(--border-color); background-color: var(--secondary-color); flex-shrink: 0; .header-info { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; .icon { font-size: 1.2em; opacity: 0.7; } .header-title { font-size: 1rem; font-weight: 600; color: var(--primary-text-color); code { font-family: monospace; background-color: var(--popup-background-color); padding: 2px 8px; border-radius: 4px; margin-left: 4px; } } .header-stats { font-size: 0.85rem; color: var(--secondary-text-color); margin-left: auto; } } } .references-list-container { flex: 1; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; &::-webkit-scrollbar { width: var(--scrollbar-width, 4px); } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: var(--scrollbar-color, rgba(0, 0, 0, 0.333)); border-radius: calc(var(--scrollbar-width, 4px) / 2); } .loading-state { display: flex; align-items: center; justify-content: center; padding: 32px 16px; color: var(--secondary-text-color); gap: 12px; .loader { @include mixins.circular-loader(24px); } } } } ================================================ FILE: src/components/referencesPanel/utils.js ================================================ import { EditorView } from "@codemirror/view"; import Sidebar from "components/sidebar"; import DOMPurify from "dompurify"; import openFile from "lib/openFile"; import { clearHighlightCache, highlightLine, sanitize, } from "utils/codeHighlight"; import helpers from "utils/helpers"; export { clearHighlightCache, sanitize }; export function getFilename(uri) { if (!uri) return ""; try { const decoded = decodeURIComponent(uri); const parts = decoded.split("/").filter(Boolean); return parts.pop() || ""; } catch { const parts = uri.split("/").filter(Boolean); return parts.pop() || ""; } } export function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function groupReferencesByFile(references) { const grouped = {}; for (const ref of references) { if (!grouped[ref.uri]) { grouped[ref.uri] = []; } grouped[ref.uri].push(ref); } return grouped; } export async function buildFlatList(references, symbolName) { const grouped = groupReferencesByFile(references); const items = []; for (const [uri, fileRefs] of Object.entries(grouped)) { fileRefs.sort((a, b) => a.range.start.line - b.range.start.line); items.push({ type: "file-header", uri, fileName: getFilename(uri), count: fileRefs.length, }); for (const ref of fileRefs) { const highlightedText = await highlightLine( ref.lineText || "", uri, symbolName, ); items.push({ type: "reference", uri, ref, line: ref.range.start.line + 1, lineText: ref.lineText || "", highlightedText, symbol: symbolName, }); } } return items; } export function createReferenceItem(item, options = {}) { const { collapsedFiles, onToggleFile, onNavigate } = options; if (item.type === "file-header") { const isCollapsed = collapsedFiles?.has(item.uri); const iconClass = helpers.getIconForFile(item.fileName); const $el = (
        onToggleFile?.(item.uri)} > {sanitize(item.fileName)} {item.count}
        ); return $el; } const $el = (
        onNavigate?.(item.ref)}> {item.line}
        ); $el.get(".ref-preview").innerHTML = DOMPurify.sanitize(item.highlightedText); return $el; } export async function navigateToReference(ref) { Sidebar.hide(); try { await openFile(ref.uri, { render: true }); const { editor } = editorManager; if (!editor) return; const doc = editor.state.doc; const startLine = doc.line(ref.range.start.line + 1); const endLine = doc.line(ref.range.end.line + 1); const from = Math.min( startLine.from + ref.range.start.character, startLine.to, ); const to = Math.min(endLine.from + ref.range.end.character, endLine.to); editor.dispatch({ selection: { anchor: from, head: to }, effects: EditorView.scrollIntoView(from, { y: "center" }), }); editor.focus(); } catch (error) { console.error("Failed to navigate to reference:", error); } } export function getReferencesStats(references) { const fileCount = new Set(references.map((r) => r.uri)).size; const refCount = references.length; return { fileCount, refCount, text: `${refCount} reference${refCount !== 1 ? "s" : ""} in ${fileCount} file${fileCount !== 1 ? "s" : ""}`, }; } ================================================ FILE: src/components/scrollbar/index.js ================================================ import "./style.scss"; import tag from "html-tag-js"; /** * @typedef {object} Scrollbar * @property {function():void} destroy * @property {function():void} render * @property {function():void} show * @property {function():void} hide * @property {function():void} resize * @property {function():void} onshow * @property {function():void} onhide * @property {function():void} hideImmediately * @property {number} value * @property {number} size * @property {boolean} visible */ /** * Create a scrollbar * @param {Object} options * @param {HTMLElement} [options.parent] * @param {"top"|"left"|"right"|"bottom"} [options.placement = "right"] * @param {Number} [options.width] * @param {function():void} [options.onscroll] * @param {function():void} [options.onscrollend] * @returns {Scrollbar & HTMLElement} */ export default function ScrollBar(options) { if (!options || !options.parent) { throw new Error("ScrollBar.js: Parent element required."); } const { placement = "right" } = options; const $cursor = tag("span", { className: "scroll-cursor", style: { top: 0, left: 0, }, }); const $thumb = tag("span", { className: "thumb", }); const $container = tag("div", { className: "container", children: [$cursor, $thumb], }); const $scrollbar = tag("div", { className: `scrollbar-container ${placement}`, child: $container, }); const config = { passive: false, }; const TIMEOUT = 2000; const isVertical = placement === "right" || placement === "left"; const observer = new MutationObserver(observerCallback); let scroll = 0; let touchStartValue = { x: 0, y: 0, }; let scrollbarSize = 20; let height; let width; let rect; let scrollbarTimeoutHide; let scrollbarTimeoutRemove; let onshow; let onhide; let touchStarted = false; if (options.width) scrollbarSize = options.width; setWidth(scrollbarSize); $scrollbar.onScroll = options.onscroll; $scrollbar.onScrollEnd = options.onscrollend; $thumb.addEventListener("touchstart", touchStart, config); $thumb.addEventListener("mousedown", touchStart, config); window.addEventListener("resize", resize); observer.observe($cursor, { attributes: true, }); function observerCallback() { $thumb.style.top = $cursor.style.top; $thumb.style.left = $cursor.style.left; } function setWidth(width) { if (isVertical) $scrollbar.style.width = $cursor.style.width = width + "px"; else $scrollbar.style.height = $cursor.style.height = width + "px"; } /** * * @param {TouchEvent|MouseEvent} e */ function touchStart(e) { e.preventDefault(); touchStarted = true; if (!rect) resize(); const touch = e.type === "touchstart" ? e.touches[0] : e; touchStartValue.x = touch.clientX; touchStartValue.y = touch.clientY; $scrollbar.classList.add("active"); document.addEventListener("touchmove", touchMove, config); document.addEventListener("mousemove", touchMove, config); document.addEventListener("touchend", touchEnd, config); document.addEventListener("mouseup", touchEnd, config); document.addEventListener("touchcancel", touchEnd, config); clearTimeout(scrollbarTimeoutHide); } /** * * @param {TouchEvent | MouseEvent} e */ function touchMove(e) { const touch = e.type === "touchmove" ? e.touches[0] : e; const touchDiffX = touchStartValue.x - touch.clientX; const touchDiffY = touchStartValue.y - touch.clientY; touchStartValue.x = touch.clientX; touchStartValue.y = touch.clientY; if (isVertical) { let top = Number.parseFloat($cursor.style.top) - touchDiffY; const currentTopValue = Number.parseFloat($cursor.style.top); if (top < 0) top = 0; else if (top > height) top = height; if (currentTopValue !== top) { $cursor.style.top = top + "px"; scroll = top / height; if (typeof $scrollbar.onScroll === "function") $scrollbar.onScroll(scroll); } } else { let left = Number.parseFloat($cursor.style.left) - touchDiffX; const currentLeftValue = Number.parseFloat($cursor.style.left); if (left < 0) left = 0; else if (left > width) left = width; if (currentLeftValue !== left) { $cursor.style.left = left + "px"; scroll = left / width; if (typeof $scrollbar.onScroll === "function") $scrollbar.onScroll(scroll); } } } /** * * @param {TouchEvent|MouseEvent} e */ function touchEnd(e) { e.preventDefault(); touchStarted = false; $scrollbar.classList.remove("active"); document.removeEventListener("touchmove", touchMove, config); document.removeEventListener("mousemove", touchMove, config); document.removeEventListener("touchend", touchEnd, config); document.removeEventListener("mouseup", touchEnd, config); document.removeEventListener("touchcancel", touchEnd, config); if (typeof $scrollbar.onScrollEnd === "function") $scrollbar.onScrollEnd(); scrollbarTimeoutHide = setTimeout(hide, TIMEOUT); } function resize(render = true) { rect = $scrollbar.getBoundingClientRect(); height = rect.height - 20; width = rect.width - 20; if (height < 0) height = 0; if (width < 0) width = 0; if (render && height && width) setValue(scroll); } function setValue(val) { if (!height || !width) resize(false); //Make sure value is between 0 and 1 if (val < 0) val = 0; else if (val > 1) val = 1; scroll = val; if (isVertical) $cursor.style.top = val * height + "px"; else $cursor.style.left = val * width + "px"; } function destroy() { window.removeEventListener("resize", resize); $thumb.removeEventListener("touchstart", touchStart); observer.disconnect(); if (typeof onhide === "function") onhide(); } function render() { show(); clearTimeout(scrollbarTimeoutHide); scrollbarTimeoutHide = setTimeout(hide, TIMEOUT); } function show() { if ($scrollbar.dataset.hidden === "false") { return; } $scrollbar.dataset.hidden = false; clearTimeout(scrollbarTimeoutHide); clearTimeout(scrollbarTimeoutRemove); $scrollbar.classList.remove("hide"); if (!$scrollbar.isConnected) { options.parent.append($scrollbar); if (typeof onshow === "function") onshow(); } } function hide() { if (touchStarted) return; $scrollbar.dataset.hidden = true; $scrollbar.classList.add("hide"); scrollbarTimeoutRemove = setTimeout(() => $scrollbar.remove(), 300); if (typeof onhide === "function") onhide(); } function hideImmediately() { $scrollbar.dataset.hidden = true; $scrollbar.classList.add("hide"); $scrollbar.remove(); if (typeof onhide === "function") onhide(); } Object.defineProperty($scrollbar, "size", { get: () => scrollbarSize, set: setWidth, }); Object.defineProperty($scrollbar, "resize", { value: resize, }); Object.defineProperty($scrollbar, "value", { get: () => scroll, set: setValue, }); Object.defineProperty($scrollbar, "destroy", { value: destroy, }); Object.defineProperty($scrollbar, "render", { value: render, }); Object.defineProperty($scrollbar, "show", { value: show, }); Object.defineProperty($scrollbar, "hide", { value: hide, }); Object.defineProperty($scrollbar, "visible", { get() { return this.dataset.hidden !== "true"; }, }); Object.defineProperty($scrollbar, "onshow", { set(fun) { onshow = fun; }, get() { return onshow; }, }); Object.defineProperty($scrollbar, "onhide", { set(fun) { onhide = fun; }, get() { return onhide; }, }); Object.defineProperty($scrollbar, "hideImmediately", { value: hideImmediately, }); return $scrollbar; } ================================================ FILE: src/components/scrollbar/style.scss ================================================ .scrollbar-container { position: absolute; background-color: transparent; padding: 10px; opacity: 0.5; z-index: 9999; animation: scrollbar-show 300ms ease 1; transition: all 300ms ease; opacity: 1; &.hide { opacity: 0; } &.active { opacity: 1; .thumb { box-shadow: 0 0 0 4px rgba($color: #fff, $alpha: 0.2); } } .container { height: 100%; width: 100%; position: relative; .thumb, .scroll-cursor { position: absolute; display: block; } .thumb { background-color: #39f; background-color: var(--button-background-color); border-radius: 4px; } } &.right, &.left { height: calc(100% - (50px * 2)); width: 10px; top: 0; bottom: 0; margin: auto; .thumb, .cursor { height: 1px; width: 100%; } .thumb { height: 50px; margin-top: -25px; } } &.right { left: auto; right: 0; padding: 10px 17px 10px 10px; } &.left { left: 0; right: auto; } &.top, &.bottom { width: calc(100% - (50px * 2)); height: 10px; left: 0; right: 0; margin: auto; .thumb, .cursor { height: 100%; width: 1px; } .thumb { width: 50px; margin-left: -25px; } } &.top { top: 0; bottom: auto; } &.bottom { bottom: 0; top: auto; } } ================================================ FILE: src/components/searchbar/index.js ================================================ import "./style.scss"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; /** * Create and activate search bar * @param {HTMLUListElement|HTMLOListElement} $list * @param {(hide:Function)=>void} setHide * @param {()=>void} onhideCb callback to be called when search bar is hidden * @param {(value:string)=>HTMLElement[]} searchFunction */ function searchBar($list, setHide, onhideCb, searchFunction) { let hideOnBlur = true; let timeout = null; const $searchInput = Ref(); /**@type {HTMLDivElement} */ const $container = ( ); /**@type {HTMLElement[]} */ const children = [...$list.children]; if (typeof setHide === "function") { hideOnBlur = false; setHide(hide); } app.appendChild($container); $searchInput.el.oninput = search; $searchInput.el.focus(); $searchInput.el.onblur = () => { if (!hideOnBlur) return; setTimeout(hide, 0); }; actionStack.push({ id: "searchbar", action: hide, }); function hide() { actionStack.remove("searchbar"); if (typeof onhideCb === "function") onhideCb(); $list.content = children; $container.classList.add("hide"); setTimeout(() => { $container.remove(); }, 300); } function search() { if (timeout) clearTimeout(timeout); timeout = setTimeout(searchNow.bind(this), 500); } /** * @this {HTMLInputElement} */ async function searchNow() { const val = $searchInput.value.toLowerCase(); if (!val) { $list.content = children; return; } let result; if (searchFunction) { result = searchFunction(val); if (result instanceof Promise) { try { result = await result; } catch (error) { window.log("error", "Search function failed:"); window.log("error", error); result = []; } } } else { result = filterList(val); } $list.textContent = ""; $list.append(...buildSearchContent(result, val)); } /** * Search list items * @param {string} val * @returns */ function filterList(val) { return children.filter((child) => { const text = child.textContent.toLowerCase(); return text.match(val, "i"); }); } /** * Keep grouped settings search results in section cards instead of flattening them. * @param {HTMLElement[]} result * @param {string} val * @returns {HTMLElement[]} */ function buildSearchContent(result, val) { if (!val || !result.length) return result; const groupedSections = []; const sectionIndexByLabel = new Map(); let hasGroups = false; result.forEach(($item) => { const label = $item.dataset.searchGroup; if (!label) { groupedSections.push({ items: [$item], type: "ungrouped", }); return; } hasGroups = true; const existingSectionIndex = sectionIndexByLabel.get(label); if (existingSectionIndex !== undefined) { groupedSections[existingSectionIndex].items.push($item); return; } sectionIndexByLabel.set(label, groupedSections.length); groupedSections.push({ items: [$item], label, type: "group", }); }); if (!hasGroups) return result.map(cloneSearchItem); const countLabel = `${result.length} ${ result.length === 1 ? strings["search result label singular"] : strings["search result label plural"] }`; const content = [
        {countLabel}
        , ]; groupedSections.forEach((section) => { if (section.type === "ungrouped") { content.push(...section.items.map(cloneSearchItem)); return; } content.push(
        {section.label}
        {section.items.map(cloneSearchItem)}
        , ); }); return content; } /** * Render search results without moving the original list items out of their groups. * @param {HTMLElement} $item * @returns {HTMLElement} */ function cloneSearchItem($item) { const $clone = $item.cloneNode(true); $clone.addEventListener("click", () => { $item.click(); }); return $clone; } } export default searchBar; ================================================ FILE: src/components/searchbar/style.scss ================================================ #search-bar { position: fixed; top: 0; left: 0; height: 45px; width: 100%; animation: show-searchbar 300ms ease 1; z-index: 200; background-color: inherit; display: flex; input { flex: 1; border-radius: 4px; box-shadow: rgba(0, 0, 0, 0.2); box-shadow: var(--box-shadow-color); border: none; margin: 5px; box-sizing: border-box; color: var(--primary-text-color); &:focus { outline: none; box-shadow: rgba(0, 0, 0, 0.5); } &::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-search-results-button, &::-webkit-search-results-decoration { display: none; } } .icon { height: 45px; width: 45px; font-size: 1.2em; color: rgb(255, 255, 255); } &.hide { transition: all 300ms ease; opacity: 0; transform: translate(0, -100%, 0); } } ================================================ FILE: src/components/settingsPage.js ================================================ import "./settingsPage.scss"; import colorPicker from "dialogs/color"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; import appSettings from "lib/settings"; import { hideAd } from "lib/startAd"; import FileBrowser from "pages/fileBrowser"; import { isValidColor } from "utils/color/regex"; import helpers from "utils/helpers"; import Checkbox from "./checkbox"; import Page from "./page"; import searchBar from "./searchbar"; /** * @typedef {object} SettingsPage * @property {(goTo:string)=>void} show show settings page * @property {()=>void} hide hide settings page * @property {(key:string)=>HTMLElement[]} search search for a setting * @property {(title:string)=>void} setTitle set title of settings page * @property {()=>void} restoreList restore list to original state */ /** * @typedef {Object} SettingsPageOptions * @property {boolean} [preserveOrder] - If true, items are listed in the order provided instead of alphabetical * @property {string} [pageClassName] - Extra classes to apply to the page element * @property {string} [listClassName] - Extra classes to apply to the list element * @property {string} [defaultSearchGroup] - Default search result group label for this page * @property {boolean} [infoAsDescription] - Override subtitle behavior; defaults to true when valueInTail is enabled * @property {boolean} [valueInTail] - Render item.value as a trailing control/value instead of subtitle * @property {boolean} [groupByDefault] - Wrap uncategorized settings in a grouped section shell * @property {"top"|"bottom"} [notePosition] - Render note before or after the settings list */ /** * Creates a settings page * @param {string} title * @param {ListItem[]} settings * @param {(key, value) => void} callback called when setting is changed * @param {'united'|'separate'} [type='united'] * @param {SettingsPageOptions} [options={}] * @returns {SettingsPage} */ export default function settingsPage( title, settings, callback, type = "united", options = {}, ) { let hideSearchBar = () => {}; const $page = Page(title); $page.id = "settings"; if (options.pageClassName) { $page.classList.add(...options.pageClassName.split(" ").filter(Boolean)); } /**@type {HTMLDivElement} */ const $list =
        ; if (options.listClassName) { $list.classList.add(...options.listClassName.split(" ").filter(Boolean)); } const normalized = normalizeSettings(settings); settings = normalized.settings; /** DISCLAIMER: do not assign hideSearchBar directly because it can change */ $page.ondisconnect = () => hideSearchBar(); $page.onhide = () => { hideAd(); actionStack.remove(title); }; const state = listItems($list, settings, callback, { defaultSearchGroup: title, ...options, }); let children = [...state.children]; $page.body = $list; const searchableItems = state.searchItems; if (shouldEnableSearch(type, settings.length)) { const $search = ; $search.onclick = () => searchBar( $list, (hide) => { hideSearchBar = hide; }, type === "united" ? restoreAllSettingsPages : null, createSearchHandler(type, state.searchItems), ); $page.header.append($search); } if (normalized.note) { const $note = createNote(normalized.note); if (options.notePosition === "top") { children.unshift($note); } else { children.push($note); } } $list.content = children; $page.append(
        ); return { /** * Show settings page * @param {string} goTo Key of setting to scroll to and select * @returns {void} */ show(goTo) { actionStack.push({ id: title, action: $page.hide, }); app.append($page); helpers.showAd(); if (goTo) { const $item = $list.get(`[data-key="${goTo}"]`); if (!$item) return; $item.scrollIntoView(); $item.click(); return; } $list.focus(); }, hide() { $page.hide(); }, /** * Search for a setting * @param {string} key */ search(key) { return searchableItems.filter((child) => { const text = child.textContent.toLowerCase(); return text.match(key, "i"); }); }, /** * Restore list to original state */ restoreList() { $list.content = children; }, /** * Set title of settings page * @param {string} title */ setTitle(title) { $page.settitle(title); }, }; } /** * @typedef {Object} ListItem * @property {string} key * @property {string} text * @property {string} [icon] * @property {string} [iconColor] * @property {string} [info] * @property {string} [value] * @property {(value:string)=>string} [valueText] * @property {string} [category] * @property {string} [searchGroup] * @property {boolean} [checkbox] * @property {boolean} [chevron] * @property {string} [prompt] * @property {string} [promptType] * @property {import('dialogs/prompt').PromptOptions} [promptOptions] */ /** * Creates a list of settings * @param {HTMLUListElement} $list * @param {Array} items * @param {()=>void} callback called when setting is changed * @param {SettingsPageOptions} [options={}] */ function listItems($list, items, callback, options = {}) { const renderedItems = []; const $searchItems = []; const useInfoAsDescription = options.infoAsDescription ?? Boolean(options.valueInTail); const itemByKey = new Map(items.map((item) => [item.key, item])); // sort settings by text before rendering (unless preserveOrder is true) if (!options.preserveOrder) { items.sort((acc, cur) => { if (!acc?.text || !cur?.text) return 0; return acc.text.localeCompare(cur.text); }); } items.forEach((item) => { const $item = createListItemElement(item, options, useInfoAsDescription); insertRenderedItem(renderedItems, item, $item); $item.addEventListener("click", onclick); $searchItems.push($item); }); const topLevelChildren = buildListContent(renderedItems, options); $list.content = topLevelChildren; return { children: topLevelChildren, searchItems: $searchItems, }; /** * Click handler for $list * @this {HTMLElement} * @param {MouseEvent} e */ async function onclick(e) { const $target = e.currentTarget; const { key } = $target.dataset; const item = itemByKey.get(key); if (!item) return; const result = await resolveItemInteraction(item, $target); if (result.shouldCallCallback === false) return; if (!result.shouldUpdateValue) return callback.call($target, key, item.value); item.value = result.value; updateItemValueDisplay($target, item, options, useInfoAsDescription); callback.call($target, key, item.value); } } function normalizeSettings(settings) { /** @type {string | undefined} */ let note; const normalizedSettings = settings.filter((setting) => { if ("note" in setting) { note = setting.note; return false; } return true; }); return { note, settings: normalizedSettings, }; } function shouldEnableSearch(type, settingsCount) { return type === "united" || (type === "separate" && settingsCount > 5); } function restoreAllSettingsPages() { Object.values(appSettings.uiSettings).forEach((page) => { page.restoreList(); }); } function createSearchHandler(type, searchItems) { return (key) => { if (type === "united") { const $items = []; Object.values(appSettings.uiSettings).forEach((page) => { $items.push(...page.search(key)); }); return $items; } return searchItems.filter((item) => { const text = item.textContent.toLowerCase(); return text.match(key, "i"); }); }; } function createNote(note) { return (
        {strings.info}

        ); } function createListItemElement(item, options, useInfoAsDescription) { const $setting = Ref(); const $tail = Ref(); const isCheckboxItem = isBooleanSetting(item); const state = getItemDisplayState(item, useInfoAsDescription); /**@type {HTMLDivElement} */ const $item = (
        {item.text?.capitalize?.(0) ?? item.text}
        ); const searchGroup = item.searchGroup || item.category || options.defaultSearchGroup; if (searchGroup) { $item.dataset.searchGroup = searchGroup; } if (isCheckboxItem) { const $checkbox = Checkbox("", item.checkbox || item.value); $tail.el.appendChild($checkbox); } if (state.hasSubtitle) { const $valueText = createSubtitleElement(item, state); $setting.append($valueText); $item.classList.add("has-subtitle"); if (!state.showInfoAsSubtitle) { setColor($item, item.value); } } else { $item.classList.add("compact"); } if (shouldShowTrailingValue(item, options)) { $item.classList.add("has-tail-value"); if (item.select) { $item.classList.add("has-tail-select"); } $tail.el.append(createTrailingValueDisplay(item)); } if (shouldShowTailChevron(item)) { $tail.el.append( , ); } if (!$tail.el.children.length) { $tail.el.remove(); } return $item; } function isBooleanSetting(item) { return item.checkbox !== undefined || typeof item.value === "boolean"; } function getItemDisplayState(item, useInfoAsDescription) { const isCheckboxItem = isBooleanSetting(item); const subtitle = isCheckboxItem ? item.info : getSubtitleText(item, useInfoAsDescription); const showInfoAsSubtitle = isCheckboxItem || useInfoAsDescription || (item.value === undefined && item.info); return { subtitle, showInfoAsSubtitle, hasSubtitle: subtitle !== undefined && subtitle !== null && subtitle !== "", }; } function createSubtitleElement(item, state) { const $valueText = ; setValueText( $valueText, state.subtitle, state.showInfoAsSubtitle ? null : item.valueText?.bind(item), ); if (state.showInfoAsSubtitle) { $valueText.classList.add("setting-info"); } return $valueText; } function shouldShowTrailingValue(item, options) { return ( options.valueInTail && item.value !== undefined && item.checkbox === undefined && typeof item.value !== "boolean" ); } function createTrailingValueDisplay(item) { const $trailingValueText = ( ); setValueText($trailingValueText, item.value, item.valueText?.bind(item)); return (
        {$trailingValueText} {item.select ? ( ) : null}
        ); } function shouldShowTailChevron(item) { return ( item.chevron || (!item.select && Boolean(item.prompt || item.file || item.folder || item.link)) ); } function insertRenderedItem(renderedItems, item, $item) { if (Number.isInteger(item.index)) { renderedItems.splice(item.index, 0, { item, $item }); return; } renderedItems.push({ item, $item }); } function buildListContent(renderedItems, options) { const $content = []; /** @type {HTMLElement | null} */ let $currentSectionCard = null; let currentCategory = null; renderedItems.forEach(({ item, $item }) => { const category = item.category?.trim() || (options.groupByDefault ? "__default__" : ""); if (!category) { currentCategory = null; $currentSectionCard = null; $content.push($item); return; } if (currentCategory !== category || !$currentSectionCard) { currentCategory = category; const section = createSectionElements(category); $currentSectionCard = section.$card; $content.push(section.$section); } $currentSectionCard.append($item); }); return $content.length ? $content : renderedItems.map(({ $item }) => $item); } function createSectionElements(category) { const shouldShowLabel = category !== "__default__"; const $label = shouldShowLabel ? (
        {category}
        ) : null; const $card =
        ; return { $card, $section: (
        {$label} {$card}
        ), }; } async function resolveItemInteraction(item, $target) { const { select: selectOptions, prompt: promptText, color: selectColor, checkbox, file, folder, link, text, value, promptType, promptOptions, } = item; try { if (selectOptions) { const selectedValue = await select(text, selectOptions, { default: value, }); return { shouldUpdateValue: selectedValue !== undefined, value: selectedValue, }; } if (checkbox !== undefined) { const $checkbox = $target.get(".input-checkbox"); $checkbox.toggle(); return { shouldUpdateValue: true, value: $checkbox.checked, }; } if (promptText) { const promptedValue = await prompt( promptText, value, promptType, promptOptions, ); if (promptedValue === null) { return { shouldUpdateValue: false, shouldCallCallback: false, }; } return { shouldUpdateValue: true, value: promptedValue, }; } if (file || folder) { const mode = file ? "file" : "folder"; const { url } = await FileBrowser(mode); return { shouldUpdateValue: true, value: url, }; } if (selectColor) { const color = await colorPicker(value); return { shouldUpdateValue: true, value: color, }; } if (link) { system.openInBrowser(link); return { shouldUpdateValue: false, shouldCallCallback: false, }; } } catch (error) { window.log("error", error); } return { shouldUpdateValue: false, shouldCallCallback: true, }; } function updateItemValueDisplay($target, item, options, useInfoAsDescription) { if (options.valueInTail) { syncTrailingValueDisplay($target, item, options); } else { syncInlineValueDisplay($target, item, useInfoAsDescription); } setColor($target, item.value); } function syncTrailingValueDisplay($target, item, options) { const shouldRenderTrailingValue = shouldShowTrailingValue(item, options); let $tail = $target.get(".setting-tail"); let $valueDisplay = $target.get(".setting-value-display"); if (!shouldRenderTrailingValue) { $valueDisplay?.remove(); $target.classList.remove("has-tail-value", "has-tail-select"); if ($tail && !$tail.children.length) { $tail.remove(); } return; } if (!$tail) { $tail =
        ; $target.append($tail); } if (!$valueDisplay) { $valueDisplay = createTrailingValueDisplay(item); const $chevron = $tail.get(".settings-chevron"); if ($chevron) { $tail.insertBefore($valueDisplay, $chevron); } else { $tail.append($valueDisplay); } } const $trailingValueText = $valueDisplay.get(".setting-trailing-value"); setValueText($trailingValueText, item.value, item.valueText?.bind(item)); $target.classList.add("has-tail-value"); $target.classList.toggle("has-tail-select", Boolean(item.select)); } /** * Keeps the inline subtitle/value block in sync when a setting value changes. * @param {HTMLElement} $target * @param {ListItem} item * @param {boolean} useInfoAsDescription */ function syncInlineValueDisplay($target, item, useInfoAsDescription) { const state = getItemDisplayState(item, useInfoAsDescription); let $valueText = $target.get(".value"); const $container = $target.get(".container"); if (!$container) return; if (!state.hasSubtitle) { $valueText?.remove(); $target.classList.remove("has-subtitle"); $target.classList.add("compact"); return; } if (!$valueText) { $valueText = ; $container.append($valueText); } $valueText.classList.toggle("setting-info", state.showInfoAsSubtitle); setValueText( $valueText, state.subtitle, state.showInfoAsSubtitle ? null : item.valueText?.bind(item), ); $target.classList.add("has-subtitle"); $target.classList.remove("compact"); } function getSubtitleText(item, useInfoAsDescription) { if (useInfoAsDescription) { return item.info; } return item.value ?? item.info; } /** * Sets color decoration of a setting * @param {HTMLDivElement} $setting * @param {string} color * @returns */ function setColor($setting, color) { if (!isValidColor(color)) return; /**@type {HTMLSpanElement} */ const $noIcon = $setting.get(".no-icon"); if (!$noIcon) return; $noIcon.style.backgroundColor = color; } /** * Sets the value text of a setting * @param {HTMLSpanElement} $valueText * @param {string} value * @param {string} valueText * @returns */ function setValueText($valueText, value, valueText) { if (!$valueText) return; if (typeof valueText === "function") { value = valueText(value); } if (typeof value === "string") { const shouldPreserveFullText = $valueText.classList.contains("value"); if (!shouldPreserveFullText) { if (value.includes("\n")) [value] = value.split("\n"); if (value.length > 47) { value = value.slice(0, 47) + "..."; } } } $valueText.textContent = value; } ================================================ FILE: src/components/settingsPage.scss ================================================ @mixin settings-page-shell($list-selector) { #{$list-selector} { display: flex; flex-direction: column; gap: 1.2rem; width: 100%; max-width: 48rem; margin: 0 auto; padding: 0.5rem 0 5.5rem; box-sizing: border-box; background: var(--secondary-color); } .settings-section { display: flex; flex-direction: column; gap: 0; width: 100%; } .settings-search-summary { padding: 0.2rem 1rem; font-size: 0.84rem; font-weight: 600; line-height: 1.35; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 18%); } .settings-section-label { padding: 0.5rem 1rem 0.45rem; font-size: 0.84rem; font-weight: 600; line-height: 1.3; color: color-mix(in srgb, var(--active-color), transparent 18%); } .settings-section-card { width: 100%; overflow: hidden; box-sizing: border-box; } } @mixin settings-icon-reset { .icon { &:active, &.active { transform: none; background-color: transparent !important; } } } wc-page.main-settings-page { background: var(--secondary-color); @include settings-page-shell(".main-settings-list"); .main-settings-list > .list-item, .settings-section-card > .list-item { display: flex; width: 100%; min-height: 64px; margin: 0; padding: 0.75rem 1rem; box-sizing: border-box; align-items: center; gap: 0.85rem; background: transparent; cursor: pointer; &.no-leading-icon { gap: 0; } &:not(:last-of-type) { border-bottom: 1px solid var(--border-color); border-bottom: 1px solid color-mix(in srgb, var(--border-color), transparent 20%); } &:focus, &:active { background: var(--popup-background-color); background: color-mix(in srgb, var(--secondary-color), var(--popup-text-color) 4%); } > .icon.no-icon { width: 0; min-width: 0; height: 0; margin: 0; } > .icon:first-child:not(.no-icon) { display: flex; align-items: center; justify-content: center; width: 1.6rem; min-width: 1.6rem; height: 1.6rem; font-size: 1.3rem; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 18%); } > .container { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: visible; gap: 0.2rem; > .text { display: block; flex: none; min-width: 0; font-size: 1rem; line-height: 1.3; font-weight: 600; color: var(--popup-text-color); } > .value { flex: none; font-size: 0.82rem; line-height: 1.35; opacity: 1; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 30%); text-transform: none; white-space: normal; overflow: visible; overflow-wrap: anywhere; } } &.compact { min-height: 56px; } } .main-settings-list .settings-chevron { display: flex; align-items: center; justify-content: center; width: 1.2rem; min-width: 1.2rem; height: 1.2rem; margin-left: 0.35rem; font-size: 1.1rem; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 40%); } .settings-search-section .list-item { > .container { padding-right: 0.35rem; gap: 0.18rem; } > .setting-tail { display: flex; align-items: center; justify-content: flex-end; flex-shrink: 0; min-height: 1.65rem; gap: 0.32rem; margin-left: 0.9rem; align-self: center; } &.has-subtitle > .container { gap: 0.24rem; padding-top: 0.14rem; padding-right: 0.6rem; } &.has-subtitle.has-tail-select > .container { padding-right: 0.95rem; } &.compact > .container { align-self: center; justify-content: center; gap: 0; padding-top: 0; } &.compact > .setting-tail { align-self: center; } } .settings-search-section .setting-value-display { display: inline-flex; align-items: center; gap: 0.15rem; min-height: auto; padding: 0; border: none; border-radius: 0; background: transparent; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 18%); box-sizing: border-box; &.is-select { min-width: clamp(6.75rem, 30vw, 8.5rem); max-width: min(45vw, 11.5rem); min-height: 2.35rem; padding: 0 0.8rem 0 0.95rem; gap: 0.55rem; justify-content: space-between; border: 1px solid var(--border-color); border: 1px solid color-mix(in srgb, var(--border-color), transparent 12%); border-radius: 0.56rem; background: color-mix( in srgb, var(--secondary-color), var(--popup-background-color) 42% ); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-color), transparent 44%), 0 1px 2px rgba(0, 0, 0, 0.12); } } .settings-search-section .setting-trailing-value { font-size: 0.88rem; line-height: 1.2; color: inherit; text-transform: none; white-space: nowrap; &.is-select { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 0.94rem; font-weight: 500; letter-spacing: -0.01em; color: color-mix(in srgb, var(--popup-text-color), transparent 14%); } } .settings-search-section .setting-value-icon, .settings-search-section .settings-chevron, .main-settings-list .settings-chevron { display: flex; align-items: center; justify-content: center; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 40%); } .settings-search-section .setting-value-icon { width: 1rem; min-width: 1rem; height: 1rem; font-size: 1rem; .setting-value-display.is-select & { width: 0.95rem; min-width: 0.95rem; height: 0.95rem; font-size: 1rem; color: color-mix(in srgb, var(--secondary-text-color), transparent 22%); } } .settings-search-section .settings-chevron, .main-settings-list .settings-chevron { width: 1.2rem; min-width: 1.2rem; height: 1.2rem; margin-left: 0.1rem; font-size: 1.1rem; line-height: 1; } .settings-search-section .input-checkbox { display: inline-flex; align-items: center; justify-content: flex-end; line-height: 0; span:last-child { display: none; } .box { width: 2.8rem !important; height: 1.65rem !important; margin: 0; border: 1px solid var(--border-color); border: 1px solid color-mix(in srgb, var(--border-color), transparent 6%); border-radius: 999px; background: var(--popup-background-color); background: color-mix( in srgb, var(--secondary-color), var(--popup-background-color) 30% ); transition: background-color 160ms ease, border-color 160ms ease, box-shadow 180ms ease; &::after { width: 1.25rem; height: 1.25rem; margin: 0.14rem; border-radius: 999px; background: var(--popup-text-color); background: color-mix( in srgb, var(--popup-text-color), var(--popup-background-color) 18% ); opacity: 1; box-shadow: 0 0 0 1px color-mix(in srgb, var(--border-color), transparent 34%), 0 1px 3px rgba(0, 0, 0, 0.22); transition: transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1), background-color 160ms ease, box-shadow 180ms ease; } } input:checked + .box { background: var(--button-background-color); border-color: color-mix(in srgb, var(--button-background-color), transparent 10%); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--button-background-color), transparent 12%); } input:checked + .box::after { transform: translateX(1.12rem); background: var(--button-text-color); opacity: 1; box-shadow: 0 2px 8px color-mix(in srgb, var(--button-background-color), transparent 55%); } input:not(:checked) + .box::after { opacity: 1; } } .settings-search-section .list-item.has-tail-select:focus .setting-value-display.is-select, .settings-search-section .list-item.has-tail-select:active .setting-value-display.is-select { border-color: color-mix(in srgb, var(--active-color), transparent 48%); background: color-mix( in srgb, var(--popup-background-color), var(--active-color) 9% ); } @media screen and (min-width: 768px) { .main-settings-list { padding-left: 0.5rem; padding-right: 0.5rem; } } @media screen and (max-width: 480px) { .settings-search-section .setting-trailing-value { max-width: 7rem; overflow: hidden; text-overflow: ellipsis; } .settings-search-section .setting-value-display.is-select { min-width: 6.25rem; max-width: 9rem; padding-left: 0.85rem; padding-right: 0.7rem; } } @include settings-icon-reset; } wc-page.detail-settings-page { background: var(--secondary-color); @include settings-page-shell(".detail-settings-list"); .detail-settings-list > .list-item, .settings-section-card > .list-item { display: flex; width: 100%; margin: 0; padding: 0.75rem 1rem; min-height: 3.2rem; box-sizing: border-box; align-items: center; gap: 0.85rem; background: transparent; cursor: pointer; transition: background-color 140ms ease; &.compact { align-items: center; } &.no-leading-icon { gap: 0; } &:not(:last-of-type) { border-bottom: 1px solid var(--border-color); border-bottom: 1px solid color-mix(in srgb, var(--border-color), transparent 20%); } &:focus, &:active { background: var(--popup-background-color); background: color-mix(in srgb, var(--secondary-color), var(--popup-text-color) 4%); } > .icon.no-icon { width: 0; min-width: 0; height: 0; margin: 0; } > .icon:first-child:not(.no-icon) { display: flex; align-items: center; justify-content: center; width: 1.4rem; min-width: 1.4rem; height: 1.4rem; font-size: 1.15rem; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 18%); } > .container { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: visible; gap: 0.18rem; padding-right: 0.35rem; > .text { display: flex; align-items: center; flex: none; min-width: 0; font-size: 1rem; line-height: 1.2; font-weight: 600; color: var(--popup-text-color); } > .value { flex: none; font-size: 0.82rem; line-height: 1.35; opacity: 1; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 30%); text-transform: none; white-space: normal; overflow: visible; overflow-wrap: anywhere; } } > .setting-tail { display: flex; align-items: center; justify-content: flex-end; flex-shrink: 0; min-height: 1.65rem; gap: 0.32rem; margin-left: 0.9rem; align-self: center; } } .detail-settings-list > .list-item.has-subtitle > .container, .settings-section-card > .list-item.has-subtitle > .container { gap: 0.24rem; padding-top: 0.14rem; padding-right: 0.6rem; } .detail-settings-list > .list-item.has-subtitle.has-tail-select > .container, .settings-section-card > .list-item.has-subtitle.has-tail-select > .container { padding-right: 0.95rem; } .detail-settings-list > .list-item.compact > .container, .settings-section-card > .list-item.compact > .container { align-self: center; justify-content: center; gap: 0; padding-top: 0; } .detail-settings-list > .list-item.compact > .setting-tail, .settings-section-card > .list-item.compact > .setting-tail { align-self: center; } .setting-value-display { display: inline-flex; align-items: center; gap: 0.15rem; min-height: auto; padding: 0; border: none; border-radius: 0; background: transparent; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 18%); box-sizing: border-box; &.is-select { min-width: clamp(6.75rem, 30vw, 8.5rem); max-width: min(45vw, 11.5rem); min-height: 2.35rem; padding: 0 0.8rem 0 0.95rem; gap: 0.55rem; justify-content: space-between; border: 1px solid var(--border-color); border: 1px solid color-mix(in srgb, var(--border-color), transparent 12%); border-radius: 0.56rem; background: color-mix( in srgb, var(--secondary-color), var(--popup-background-color) 42% ); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-color), transparent 44%), 0 1px 2px rgba(0, 0, 0, 0.12); } } .setting-trailing-value { font-size: 0.88rem; line-height: 1.2; color: inherit; text-transform: none; white-space: nowrap; &.is-select { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 0.94rem; font-weight: 500; letter-spacing: -0.01em; color: color-mix(in srgb, var(--popup-text-color), transparent 14%); } } .setting-value-icon, .settings-chevron { display: flex; align-items: center; justify-content: center; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 40%); } .setting-value-icon { width: 1rem; min-width: 1rem; height: 1rem; font-size: 1rem; .setting-value-display.is-select & { width: 0.95rem; min-width: 0.95rem; height: 0.95rem; font-size: 1rem; color: color-mix(in srgb, var(--secondary-text-color), transparent 22%); } } .settings-chevron { width: 1.2rem; min-width: 1.2rem; height: 1.2rem; margin-left: 0.1rem; font-size: 1.1rem; line-height: 1; } .input-checkbox { display: inline-flex; align-items: center; justify-content: flex-end; line-height: 0; span:last-child { display: none; } .box { width: 2.8rem !important; height: 1.65rem !important; margin: 0; border: 1px solid var(--border-color); border: 1px solid color-mix(in srgb, var(--border-color), transparent 6%); border-radius: 999px; background: var(--popup-background-color); background: color-mix( in srgb, var(--secondary-color), var(--popup-background-color) 30% ); transition: background-color 160ms ease, border-color 160ms ease, box-shadow 180ms ease; &::after { width: 1.25rem; height: 1.25rem; margin: 0.14rem; border-radius: 999px; background: var(--popup-text-color); background: color-mix( in srgb, var(--popup-text-color), var(--popup-background-color) 18% ); opacity: 1; box-shadow: 0 0 0 1px var(--border-color), 0 1px 3px rgba(0, 0, 0, 0.22); box-shadow: 0 0 0 1px color-mix(in srgb, var(--border-color), transparent 34%), 0 1px 3px rgba(0, 0, 0, 0.22); transition: transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1), background-color 160ms ease, box-shadow 180ms ease; } } input:checked + .box { background: var(--button-background-color); border-color: var(--button-background-color); border-color: color-mix(in srgb, var(--button-background-color), transparent 10%); box-shadow: inset 0 0 0 1px var(--button-background-color); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--button-background-color), transparent 12%); } input:checked + .box::after { transform: translateX(1.12rem); background: var(--button-text-color); opacity: 1; box-shadow: 0 2px 8px var(--button-background-color); box-shadow: 0 2px 8px color-mix(in srgb, var(--button-background-color), transparent 55%); } input:not(:checked) + .box::after { opacity: 1; } } .note { display: flex; align-items: flex-start; gap: 0.6rem; width: auto; box-sizing: border-box; margin: 0.25rem 0.75rem; padding: 0.9rem 1rem; border: 1px solid var(--active-color); border: 1px solid color-mix(in srgb, var(--active-color), transparent 60%); border-radius: 10px; background: var(--popup-background-color); background: color-mix(in srgb, var(--popup-background-color), var(--active-color) 8%); .note-title { display: flex; align-items: center; justify-content: center; flex-shrink: 0; height: auto; padding: 0; background: transparent; text-transform: none; > .icon { display: inline-flex; align-items: center; justify-content: center; width: 1.15rem; min-width: 1.15rem; height: 1.15rem; background: transparent; color: var(--active-color); color: color-mix(in srgb, var(--active-color), transparent 10%); font-size: 1.1rem; margin-top: 0.12rem; } > span:last-child { display: none; } } p { margin: 0; padding: 0; font-size: 0.84rem; line-height: 1.55; color: var(--secondary-text-color); color: color-mix(in srgb, var(--secondary-text-color), transparent 8%); } } @media screen and (max-width: 480px) { .setting-trailing-value { max-width: 7rem; overflow: hidden; text-overflow: ellipsis; } .setting-value-display.is-select { min-width: 6.25rem; max-width: 9rem; padding-left: 0.85rem; padding-right: 0.7rem; } } .detail-settings-list > .list-item.has-tail-select:focus .setting-value-display.is-select, .detail-settings-list > .list-item.has-tail-select:active .setting-value-display.is-select, .settings-section-card > .list-item.has-tail-select:focus .setting-value-display.is-select, .settings-section-card > .list-item.has-tail-select:active .setting-value-display.is-select { border-color: color-mix(in srgb, var(--active-color), transparent 48%); background: color-mix( in srgb, var(--popup-background-color), var(--active-color) 9% ); } @include settings-icon-reset; } wc-page.detail-settings-page.formatter-settings-page { .detail-settings-list > .list-item, .settings-section-card > .list-item { padding-top: 0.5rem; padding-bottom: 0.5rem; &.compact { min-height: 3.5rem; padding-top: 0.18rem; padding-bottom: 0.18rem; } &.has-subtitle { min-height: 4.5rem; padding-top: 0.74rem; padding-bottom: 0.74rem; } > .icon:first-child:not(.no-icon) { width: 1.1rem; min-width: 1.1rem; height: 1.1rem; margin-right: 0.7rem; font-size: 1rem; } > .container { gap: 0.2rem; } > .container > .value { -webkit-line-clamp: unset; } } .detail-settings-list > .list-item.compact > .container, .settings-section-card > .list-item.compact > .container, .detail-settings-list > .list-item.compact > .setting-tail, .settings-section-card > .list-item.compact > .setting-tail { align-self: center; } } ================================================ FILE: src/components/sideButton/index.js ================================================ import "./style.scss"; /**@type {HTMLDivElement} */ export const sideButtonContainer =
        ; export default function SideButtons({ text, icon, onclick, backgroundColor, textColor, }) { const $button = ( ); return { show() { sideButtonContainer.append($button); }, hide() { $button.remove(); }, }; } ================================================ FILE: src/components/sideButton/style.scss ================================================ .side-button { position: relative; width: 15px; padding: 2.5px 0; border: none; box-sizing: border-box; .icon { margin: 0 0 5px 0; } span { writing-mode: vertical-rl; } &:active::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba($color: #000000, $alpha: 0.5); } } .side-buttons { position: absolute; right: 0; top: 0; z-index: 999; display: flex; flex-direction: column; gap: 5px; } ================================================ FILE: src/components/sidebar/index.js ================================================ import "./style.scss"; 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"; let $sidebar; /**@type {Array<(el:HTMLElement)=>boolean>} */ let preventSlideTests = []; const events = { show: [], 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 * @param {HTMLElement} [$toggler] - the element that will toggle the sidebar * @returns {Sidebar} */ function create($container, $toggler) { let { innerWidth } = window; const START_THRESHOLD = constants.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(); const userAvatar = Ref(); const userContextMenu = Ref(); $container = $container || app; let mode = innerWidth > 600 ? "tab" : "phone"; let width = +(localStorage.sideBarWidth || MIN_WIDTH); const eventOptions = { passive: false }; const $el = ( ); const mask = ; const touch = { startX: 0, totalX: 0, endX: 0, startY: 0, totalY: 0, endY: 0, target: null, }; let openedFolders = []; let resizeTimeout = null; let setWidthTimeout = null; $toggler?.addEventListener("click", toggle); $container.addEventListener("touchstart", ontouchstart, eventOptions); window.addEventListener("resize", onWindowResize); if (mode === "tab" && localStorage.sidebarShown === "1") { show(); } loginEvents.on(() => { updateSidebarAvatar(); }); async function handleUserIconClick(e) { try { const isLoggedIn = await auth.isLoggedIn(); if (!isLoggedIn) { auth.openLoginUrl(); } else { toggleUserMenu(); } } catch (error) { console.error("Error checking login status:", error); toast("Error checking login status", 3000); } } 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) && e.target !== userAvatar.el && !userAvatar.el.contains(e.target) ) { userContextMenu.el.classList.remove("active"); document.removeEventListener("click", handleClickOutside); } } 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(); if (success) { userContextMenu.el.classList.remove("active"); document.removeEventListener("click", handleClickOutside); toast("Logged out successfully"); updateSidebarAvatar(); } else { toast("Failed to logout"); } } catch (error) { console.error("Error during logout:", error); } } 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"); if (existingIcon) { existingIcon.remove(); } if (existingAvatar) { existingAvatar.remove(); } if (avatarUrl?.startsWith("data:") || avatarUrl?.startsWith("http")) { // Create and add avatar image 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 onWindowResize() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { const { innerWidth: currentWidth } = window; if (innerWidth === currentWidth) return; hide(true); innerWidth = currentWidth; $el.classList.remove(mode); mode = innerWidth > 750 ? "tab" : "phone"; $el.classList.add(mode); }, 300); } function toggle() { if ($el.activated) return hide(true); show(); } function show() { localStorage.sidebarShown = 1; $el.activated = true; $el.onclick = null; if (mode === "phone") { resizeBar.style.display = "none"; $el.onshow(); app.append($el, mask); $el.classList.add("show"); document.ontouchstart = ontouchstart; actionStack.push({ id: "sidebar", action: hideMaster, }); } else { setWidth(width); resizeBar.style.display = "block"; app.append($el); $el.onclick = () => { if (!$el.textContent) acode.exec("open-folder"); }; } onshow(); } function hide(hideIfTab = false) { localStorage.sidebarShown = 0; if (mode === "phone") { actionStack.remove("sidebar"); hideMaster(); } else if (hideIfTab) { $el.activated = false; root.style.removeProperty("margin-left"); root.style.removeProperty("width"); $el.remove(); // TODO : Codemirror //editorManager.editor.resize(true); } } function hideMaster() { $el.style.transform = null; $el.classList.remove("show"); setTimeout(() => { $el.activated = false; mask.remove(); $el.remove(); $container.style.overflow = null; onhide(); }, 300); document.ontouchstart = null; resetState(); openedFolders.map(($) => ($.onscroll = null)); openedFolders = []; } async function onshow() { if ($el.onshow) $el.onshow.call($el); events.show.forEach((fn) => fn()); // try { // if (await auth.isLoggedIn()) { // const avatar = await auth.getAvatar(); // if (avatar) { // auth.updateSidebarAvatar(avatar); // } // } // } catch (error) { // console.error("Error updating avatar:", error); // } } function onhide() { if ($el.onhide) $el.onhide.call($el); events.hide.forEach((fn) => fn()); } /** * Event handler for touchstart event * @param {TouchEvent} e */ function ontouchstart(e) { const { target } = e; const { clientX, clientY } = getClientCoords(e); if (preventSlideTests.find((test) => test(target))) return; if (mode === "tab") return; $el.style.transition = "none"; touch.startX = clientX; touch.startY = clientY; touch.target = target; if ($el.activated && !$el.contains(target) && target !== mask) { return; } else if ( (!$el.activated && touch.startX > START_THRESHOLD) || target === $toggler ) { return; } document.addEventListener("touchmove", ontouchmove, eventOptions); document.addEventListener("touchend", ontouchend, eventOptions); } /** * Event handler for resize event * @param {MouseEvent | TouchEvent} e * @returns */ function onresize(e) { const { clientX } = getClientCoords(e); let deltaX = 0; const onMove = (e) => { const { clientX: currentX } = getClientCoords(e); deltaX = currentX - clientX; resize(deltaX); }; const onEnd = () => { const newWidth = width + deltaX; if (newWidth <= MIN_WIDTH) width = MIN_WIDTH; else if (newWidth >= MAX_WIDTH()) width = MAX_WIDTH(); else width = newWidth; localStorage.sideBarWidth = width; document.removeEventListener("touchmove", onMove, eventOptions); document.removeEventListener("mousemove", onMove, eventOptions); document.removeEventListener("touchend", onEnd, eventOptions); document.removeEventListener("mouseup", onEnd, eventOptions); document.removeEventListener("mouseleave", onEnd, eventOptions); document.removeEventListener("touchcancel", onEnd, eventOptions); }; document.addEventListener("touchmove", onMove, eventOptions); document.addEventListener("mousemove", onMove, eventOptions); document.addEventListener("touchend", onEnd, eventOptions); document.addEventListener("mouseup", onEnd, eventOptions); document.addEventListener("mouseleave", onEnd, eventOptions); document.addEventListener("touchcancel", onEnd, eventOptions); return; } /** * Resize the sidebar * @param {number} deltaX * @returns */ function resize(deltaX) { const newWidth = width + deltaX; if (newWidth >= MAX_WIDTH()) return; if (newWidth <= MIN_WIDTH) return; setWidth(newWidth); } /** * Event handler for touchmove event * @param {TouchEvent} e */ function ontouchmove(e) { e.preventDefault(); const { clientX, clientY } = getClientCoords(e); touch.endX = clientX; touch.endY = clientY; touch.totalX = touch.endX - touch.startX; touch.totalY = touch.endY - touch.startY; let width = $el.getWidth(); if ( !$el.activated && touch.totalX < width && touch.startX < START_THRESHOLD ) { if (!$el.isConnected) { app.append($el, mask); $container.style.overflow = "hidden"; } $el.style.transform = `translate3d(${-(width - touch.totalX)}px, 0, 0)`; } else if (touch.totalX < 0 && $el.activated) { $el.style.transform = `translate3d(${touch.totalX}px, 0, 0)`; } } /** * Event handler for touchend event * @param {TouchEvent} e */ function ontouchend(e) { if (e.target !== mask && touch.totalX === 0) return resetState(); else if (e.target === mask && touch.totalX === 0) return hide(); e.preventDefault(); const threshold = $el.getWidth() / 3; if ( ($el.activated && touch.totalX > -threshold) || (!$el.activated && touch.totalX >= threshold) ) { lclShow(); } else if ( (!$el.activated && touch.totalX < threshold) || ($el.activated && touch.totalX <= -threshold) ) { hide(); } function lclShow() { onshow(); $el.activated = true; $el.style.transform = `translate3d(0, 0, 0)`; document.addEventListener("touchstart", ontouchstart, eventOptions); actionStack.remove("sidebar"); actionStack.push({ id: "sidebar", action: hideMaster, }); resetState(); } } /** * Reset the touch state */ function resetState() { touch.totalY = 0; touch.startY = 0; touch.endY = 0; touch.totalX = 0; touch.startX = 0; touch.endX = 0; touch.target = null; $el.style.transition = null; document.removeEventListener("touchmove", ontouchmove, eventOptions); document.removeEventListener("touchend", ontouchend, eventOptions); } /** * Set the width of the sidebar * @param {number} width */ function setWidth(width) { $el.style.transition = "none"; $el.style.maxWidth = width + "px"; root.style.marginLeft = width + "px"; root.style.width = `calc(100% - ${width}px)`; clearTimeout(setWidthTimeout); setWidthTimeout = setTimeout(() => { editorManager?.editor?.resize(true); }, 300); } /** * Get the clientX and clientY from the event * @param {TouchEvent | MouseEvent} e * @returns {{clientX: number, clientY: number}} */ function getClientCoords(e) { const { clientX, clientY } = (e.touches ?? [])[0] ?? e; return { clientX, clientY }; } $el.show = show; $el.hide = hide; $el.toggle = toggle; $el.onshow = () => {}; $el.getWidth = function () { const width = innerWidth * 0.7; return mode === "phone" ? (width >= 350 ? 350 : width) : MIN_WIDTH; }; return $el; } /** * Create a sidebar or return the existing one * @param {object} [arg0] - the element that will activate the sidebar * @param {HTMLElement} [arg0.container] - the element that will contain the sidebar * @param {HTMLElement} [arg0.toggler] - the element that will toggle the sidebar * @returns {HTMLElement & SideBar} */ function Sidebar({ container, toggler }) { $sidebar = $sidebar ?? create(container, toggler); return $sidebar; } Sidebar.hide = () => $sidebar?.hide(); Sidebar.show = () => $sidebar?.show(); Sidebar.toggle = () => $sidebar?.toggle(); Sidebar.on = ( /**@type {'hide'|'show'} */ event, /**@type {Function} */ callback, ) => { if (!events[event]) return; events[event].push(callback); }; Sidebar.off = ( /**@type {'hide'|'show'} */ event, /**@type {Function} */ callback, ) => { if (!events[event]) return; events[event] = events[event].filter((cb) => cb !== callback); }; /**@type {HTMLElement} */ Sidebar.el = null; Object.defineProperty(Sidebar, "el", { get() { return $sidebar; }, }); preventSlideTests.push((target) => { let lastEl; return testScrollable(target.closest(".scroll")); /** * Test if the element is scrollable recursively * @param {HTMLElement} container * @returns */ function testScrollable(container) { if (!container || container === lastEl) return false; const { scrollHeight, offsetHeight, scrollWidth, offsetWidth } = container; if (scrollHeight > offsetHeight) return true; if (scrollWidth > offsetWidth) return true; lastEl = container; return testScrollable(container.parentElement.closest(".scroll")); } }); preventSlideTests.push((target) => { return ( target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.contentEditable === "true" ); }); export default Sidebar; /** * Prevent the sidebar from sliding when the test returns true * @param {(target:Element)=>boolean} test */ export function preventSlide(test) { preventSlideTests.push(test); } ================================================ FILE: src/components/sidebar/style.scss ================================================ body.no-animation { #sidebar { border-right: solid 1px rgba(0, 0, 0, 0.2); } } .page-replacement~#sidebar { opacity: 0; } #sidebar { z-index: 109; position: fixed; left: 0; top: 0; width: 70vw; max-width: 350px; height: 100vh; display: flex; background-color: rgb(153, 153, 255); background-color: var(--primary-color); color: rgb(255, 255, 255); color: var(--primary-text-color); overflow: hidden; box-sizing: border-box; &+.mask { z-index: 108; } &.phone { transition: transform 300ms ease; transform: translate(-100%, 0); box-shadow: 2px 0 4px rgba(0, 0, 0, 0.07); .resize-bar { pointer-events: none; } } &.tab { max-width: 250px; } &.show { transform: translate(0, 0); animation: show-sidebar 300ms ease 1; } .resize-bar { height: 100vh; width: 5px; margin-left: -2.5px; z-index: 110; } .apps { width: 52px; min-width: 52px; height: 100%; background-color: rgba(0, 0, 0, 0.15); padding: 0; display: flex; flex-direction: column; position: relative; box-sizing: border-box; .app-icons-container { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px 0; display: flex; flex-direction: column; align-items: center; gap: 4px; scrollbar-width: none; -ms-overflow-style: none; &::-webkit-scrollbar { display: none; } } .user-icon-container { position: sticky; bottom: 0; width: 100%; padding: 12px 0; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(255, 255, 255, 0.08); .avatar { display: block; height: 32px; width: 32px; border-radius: 50%; object-fit: cover; object-position: center; cursor: pointer; transition: all 0.2s ease; border: 2px solid transparent; &:hover { transform: scale(1.05); border-color: rgba(255, 255, 255, 0.3); } } .icon { height: 40px; width: 40px; color: currentColor; font-size: 1.4em; border-radius: 10px; opacity: 0.6; transition: all 0.2s ease; margin: 0 auto; cursor: pointer; text-align: center; line-height: 40px; &:hover { opacity: 0.9; background-color: rgba(255, 255, 255, 0.08); } &.active { opacity: 1; } } } .icon { height: 40px; width: 40px; min-height: 40px; color: currentColor; font-size: 1.3em; border-radius: 10px; opacity: 0.55; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); margin: 0 auto; cursor: pointer; position: relative; box-sizing: border-box; text-align: center; line-height: 40px; // Active indicator bar using ::after to not interfere with font icon's ::before &::after { content: ''; position: absolute; left: -6px; top: 50%; transform: translateY(-50%) scaleY(0); width: 3px; height: 20px; background-color: currentColor; border-radius: 0 2px 2px 0; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease; opacity: 0; } &:hover { opacity: 0.85; background-color: rgba(255, 255, 255, 0.08); } &.active { opacity: 1; background-color: rgba(255, 255, 255, 0.12); &::after { transform: translateY(-50%) scaleY(1); opacity: 1; } } // Special styling for the favorites/sponsors icon &.favorite { margin-top: 8px; &::after { display: none; } &:hover { color: #ff6b8a; opacity: 1; background-color: rgba(255, 107, 138, 0.12); } } } } .container { overflow: hidden; box-sizing: border-box; display: flex; flex-direction: column; width: 100%; height: 100%; &.files { overflow-x: auto; } >.list { width: 100%; max-width: 100%; max-height: 100%; &.hidden { max-height: 36px !important; min-height: 36px !important; overflow: hidden !important; } .tile { &:not(:first-child) { background-color: inherit; } &.notice { &::before { content: "\2022"; color: rgb(212, 250, 150); display: flex; align-items: center; justify-content: center; font-size: 1em; } } } .icon { height: 34px; width: 34px; min-width: 34px; color: currentColor; font-size: 1.15em; text-align: center; line-height: 34px; } >ul { overflow: auto; width: 100%; max-width: 100%; max-height: calc(100% - 36px); height: calc(100% - 36px); .tile:active { >*:nth-child(2) { color: rgb(255, 215, 0); } } li { >*:nth-child(2) { color: currentColor; } &.active { >*:nth-child(2) { color: rgb(255, 215, 0); } } } } } .tile { user-select: none; } } textarea { padding: 5px !important; box-sizing: border-box; } textarea, input { color: rgb(255, 255, 255); color: var(--primary-text-color); border: solid 1px var(--border-color); border: solid 1px color-mix(in srgb, var(--border-color) 70%, transparent); border-radius: 8px; height: 30px; width: 90%; padding: 0 12px; text-indent: 0; margin-bottom: 12px; background: transparent; transition: all 0.2s ease; &:focus { border-color: var(--border-color) !important; background: var(--secondary-color); background: color-mix(in srgb, var(--secondary-color) 10%, transparent); box-shadow: 0 0 0 2px var(---box-shadow-color); } &::placeholder { opacity: 0.5; } } .box { border: solid 1px rgb(255, 255, 255); border: solid 1px var(--primary-text-color); &::after { background-color: rgb(255, 255, 255); background-color: var(--primary-text-color); } } .header { display: flex; flex-shrink: 0; align-items: center; justify-content: center; flex-direction: column; .title { display: flex; width: 100%; height: 36px; align-items: center; justify-content: center; font-weight: 600; } } } .user-menu { position: absolute; bottom: 55px; left: 30px; width: 200px; background-color: var(--popup-background-color); border: 1px solid var(--border-color); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); display: none; z-index: 1000; overflow: hidden; &.active { display: block; } .user-menu-header { padding: 12px; border-bottom: 1px solid var(--border-color); .user-menu-name { font-weight: 500; margin-bottom: 4px; .badge { display: inline-flex; align-items: center; background-color: var(--popup-background-color); background-color: color-mix(in srgb, var(--error-text-color) 20%, transparent); color: var(--error-text-color); padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: 500; margin-left: 8px; } } .user-menu-email { font-size: 12px; color: var(--popup-text-color); color: color-mix(in srgb, var(--popup-text-color) 70%, transparent); } } .user-menu-item { padding: 10px 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; &:hover { background-color: var(--active-icon-color); } } .user-menu-separator { height: 1px; background-color: var(--border-color); margin: 4px 0; } } ================================================ FILE: src/components/symbolsPanel/index.js ================================================ import "./styles.scss"; import { fetchDocumentSymbols, navigateToSymbol } from "cm/lsp"; import actionStack from "lib/actionStack"; let currentPanel = null; const SYMBOL_KIND_ABBREV = { 1: "Fi", 2: "Mo", 3: "Ns", 4: "Pk", 5: "C", 6: "M", 7: "P", 8: "F", 9: "Co", 10: "E", 11: "I", 12: "fn", 13: "V", 14: "c", 15: "S", 16: "#", 17: "B", 18: "[]", 19: "{}", 20: "K", 21: "∅", 22: "Em", 23: "St", 24: "Ev", 25: "Op", 26: "T", }; function sanitize(str) { if (!str) return ""; return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function flattenSymbolsForDisplay(symbols, depth = 0) { const result = []; for (const sym of symbols) { result.push({ ...sym, depth, id: `${sym.selectionRange.startLine}-${sym.selectionRange.startCharacter}-${sym.name}`, }); if (sym.children && sym.children.length > 0) { result.push(...flattenSymbolsForDisplay(sym.children, depth + 1)); } } return result; } function createSymbolsPanel() { const state = { visible: false, expanded: false, loading: true, symbols: [], flatSymbols: [], filteredSymbols: [], searchQuery: "", editorView: null, }; const $mask = ; const $panel =
        ; const $dragHandle =
        ; const $title =
        ; const $subtitle = ; const $searchInput = ( ); const $content =
        ; const $symbolsList =
        ; const $closeBtn = ( ); const $header = (
        {$title} {$subtitle}
        {$closeBtn}
        ); const $search =
        {$searchInput}
        ; $panel.append($dragHandle, $header, $search, $content); $mask.onclick = hide; let startY = 0; let currentY = 0; let isDragging = false; $dragHandle.ontouchstart = onDragStart; $dragHandle.onmousedown = onDragStart; function onDragStart(e) { isDragging = true; startY = e.touches ? e.touches[0].clientY : e.clientY; currentY = startY; $panel.style.transition = "none"; document.addEventListener("touchmove", onDragMove, { passive: false }); document.addEventListener("mousemove", onDragMove); document.addEventListener("touchend", onDragEnd); document.addEventListener("mouseup", onDragEnd); } function onDragMove(e) { if (!isDragging) return; e.preventDefault(); currentY = e.touches ? e.touches[0].clientY : e.clientY; const deltaY = currentY - startY; if (deltaY > 0) { $panel.style.transform = `translateY(${deltaY}px)`; } else if (!state.expanded) { const expansion = Math.min(Math.abs(deltaY), 100); $panel.style.maxHeight = `${60 + (expansion / 100) * 25}vh`; } } function onDragEnd() { isDragging = false; document.removeEventListener("touchmove", onDragMove); document.removeEventListener("mousemove", onDragMove); document.removeEventListener("touchend", onDragEnd); document.removeEventListener("mouseup", onDragEnd); $panel.style.transition = ""; const deltaY = currentY - startY; if (deltaY > 100) { hide(); } else if (deltaY < -50 && !state.expanded) { state.expanded = true; $panel.classList.add("expanded"); $panel.style.transform = ""; $panel.style.maxHeight = ""; } else { $panel.style.transform = ""; $panel.style.maxHeight = ""; } } function setTitle(text) { $title.innerHTML = ""; $title.append( , {sanitize(text)}, ); } function setSubtitle(text) { $subtitle.textContent = text; } function onSearchInput(e) { state.searchQuery = e.target.value.trim(); filterSymbols(); } function clearSearch() { $searchInput.value = ""; state.searchQuery = ""; } function filterSymbols() { const query = state.searchQuery.toLowerCase(); if (!query) { state.filteredSymbols = state.flatSymbols; } else { state.filteredSymbols = state.flatSymbols.filter((sym) => { const nameMatch = sym.name.toLowerCase().includes(query); const kindMatch = sym.kindName.toLowerCase().includes(query); const detailMatch = sym.detail?.toLowerCase().includes(query); return nameMatch || kindMatch || detailMatch; }); } updateList(); } function updateList() { setSubtitle(getSubtitle()); renderSymbolsList(); } function getSubtitle() { const total = state.flatSymbols.length; const filtered = state.filteredSymbols.length; if (total === 0) return "No symbols"; if (filtered === total) return `${total} symbol${total !== 1 ? "s" : ""}`; return `${filtered} of ${total} symbols`; } function renderLoading() { $content.innerHTML = ""; $content.append(
        Loading symbols...
        , ); } function renderEmpty() { const message = state.searchQuery ? "No matching symbols" : "No symbols found"; $content.innerHTML = ""; $content.append(
        {message}
        , ); } function renderNotSupported() { $content.innerHTML = ""; $content.append(
        Language server does not support document symbols
        , ); } function highlightMatch(text, query) { if (!query) return sanitize(text); const lowerText = text.toLowerCase(); const lowerQuery = query.toLowerCase(); const index = lowerText.indexOf(lowerQuery); if (index === -1) return sanitize(text); const before = sanitize(text.slice(0, index)); const match = sanitize(text.slice(index, index + query.length)); const after = sanitize(text.slice(index + query.length)); return `${before}${match}${after}`; } function createSymbolItem(symbol) { const kindName = symbol.kindName || "Unknown"; const kindClass = `kind-${kindName.toLowerCase().replace(/\s+/g, "")}`; const abbrev = SYMBOL_KIND_ABBREV[symbol.kind] || "?"; const indent = (symbol.depth || 0) * 16; const startLine = symbol.selectionRange?.startLine ?? 0; const $item = (
        onSymbolClick(symbol)}> {abbrev} {symbol.detail && ( {sanitize(symbol.detail)} )} :{startLine + 1}
        ); $item.get(".symbol-name").innerHTML = highlightMatch( symbol.name, state.searchQuery, ); return $item; } function renderSymbolsList() { $symbolsList.innerHTML = ""; if (state.filteredSymbols.length === 0) { renderEmpty(); return; } $content.innerHTML = ""; const fragment = document.createDocumentFragment(); for (const symbol of state.filteredSymbols) { fragment.appendChild(createSymbolItem(symbol)); } $symbolsList.appendChild(fragment); $content.appendChild($symbolsList); } function onSymbolClick(symbol) { if (!state.editorView) return; hide(); navigateToSymbol(state.editorView, symbol); } async function loadSymbols() { if (!state.editorView) { renderNotSupported(); return; } renderLoading(); try { const symbols = await fetchDocumentSymbols(state.editorView); if (symbols === null) { state.loading = false; setSubtitle("Not supported"); renderNotSupported(); return; } state.loading = false; state.symbols = symbols; state.flatSymbols = flattenSymbolsForDisplay(symbols); state.filteredSymbols = state.flatSymbols; if (state.flatSymbols.length === 0) { setSubtitle("No symbols"); renderEmpty(); } else { setSubtitle(getSubtitle()); renderSymbolsList(); } } catch (error) { console.error("Failed to load symbols:", error); state.loading = false; setSubtitle("Error"); $content.innerHTML = ""; $content.append(
        {sanitize(error.message || "Failed to load symbols")}
        , ); } } function show(options = {}) { if (currentPanel && currentPanel !== panelInstance) { currentPanel.hide(); } currentPanel = panelInstance; state.editorView = options.view || null; state.symbols = []; state.flatSymbols = []; state.filteredSymbols = []; state.searchQuery = ""; state.loading = true; state.expanded = false; clearSearch(); setTitle("Document Outline"); setSubtitle("Loading..."); renderLoading(); document.body.append($mask, $panel); requestAnimationFrame(() => { $mask.classList.add("visible"); $panel.classList.add("visible"); $panel.classList.remove("expanded"); }); state.visible = true; actionStack.push({ id: "symbols-panel", action: hide, }); loadSymbols(); } function hide() { if (!state.visible) return; state.visible = false; $mask.classList.remove("visible"); $panel.classList.remove("visible"); actionStack.remove("symbols-panel"); setTimeout(() => { $mask.remove(); $panel.remove(); }, 250); if (currentPanel === panelInstance) { currentPanel = null; } if (state.editorView) { state.editorView.focus(); } } const panelInstance = { show, hide, get visible() { return state.visible; }, }; return panelInstance; } let panelSingleton = null; function getPanel() { if (!panelSingleton) { panelSingleton = createSymbolsPanel(); } return panelSingleton; } export function showSymbolsPanel(options) { const panel = getPanel(); panel.show(options); return panel; } export function hideSymbolsPanel() { const panel = getPanel(); panel.hide(); } export async function showDocumentSymbols(view) { if (!view) { const em = globalThis.editorManager; view = em?.editor; } if (!view) { const toast = globalThis.toast; toast?.("No editor available"); return false; } showSymbolsPanel({ view }); return true; } export default { show: showSymbolsPanel, hide: hideSymbolsPanel, showDocumentSymbols, getPanel, }; ================================================ FILE: src/components/symbolsPanel/styles.scss ================================================ @use "../../styles/mixins.scss"; .symbols-panel-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.4); z-index: 100; opacity: 0; transition: opacity 200ms ease-out; pointer-events: none; &.visible { opacity: 1; pointer-events: auto; } } .symbols-panel { position: fixed; left: 0; right: 0; bottom: 0; z-index: 101; background-color: var(--popup-background-color); border-top-left-radius: 12px; border-top-right-radius: 12px; box-shadow: 0 -4px 20px var(--box-shadow-color); transform: translateY(100%); transition: transform 250ms ease-out, max-height 200ms ease-out; display: flex; flex-direction: column; max-height: 60vh; overflow: hidden; &.visible { transform: translateY(0); } &.expanded { max-height: 85vh; } .drag-handle { display: flex; justify-content: center; align-items: center; padding: 8px 0 4px 0; cursor: grab; flex-shrink: 0; &::after { content: ""; width: 36px; height: 4px; background-color: var(--border-color); border-radius: 2px; } &:active { cursor: grabbing; } } .panel-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px 12px 16px; border-bottom: 1px solid var(--border-color); min-height: 44px; flex-shrink: 0; .header-content { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; .header-title { display: flex; align-items: center; gap: 8px; font-size: 1rem; font-weight: 600; color: var(--primary-text-color); .icon { font-size: 1.1em; opacity: 0.7; } } .header-subtitle { font-size: 0.85rem; color: var(--secondary-text-color); opacity: 0.8; } } .header-actions { display: flex; gap: 4px; flex-shrink: 0; } .action-btn { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--secondary-text-color); border-radius: 50%; cursor: pointer; &:active { background-color: var(--active-icon-color); } .icon { font-size: 1.2em; } } } .panel-search { padding: 8px 16px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; input { width: 100%; background-color: var(--secondary-color); border: 1px solid var(--border-color); border-radius: 8px; padding: 10px 12px; font-size: 0.9rem; color: var(--primary-text-color); outline: none; &::placeholder { color: var(--secondary-text-color); opacity: 0.7; } &:focus { border-color: var(--active-color); } } } .panel-content { flex: 1; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; &::-webkit-scrollbar { width: var(--scrollbar-width, 4px); } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: var(--scrollbar-color, rgba(0, 0, 0, 0.333)); border-radius: calc(var(--scrollbar-width, 4px) / 2); } } .loading-state { display: flex; align-items: center; justify-content: center; padding: 32px 16px; color: var(--secondary-text-color); gap: 12px; .loader { @include mixins.circular-loader(24px); } } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 32px 16px; color: var(--secondary-text-color); text-align: center; .icon { font-size: 2rem; margin-bottom: 8px; opacity: 0.5; } } } .symbol-item { box-sizing: border-box; display: flex; align-items: center; gap: 10px; padding: 0 16px; height: 44px; cursor: pointer; user-select: none; will-change: transform; &:active { background-color: var(--active-icon-color); } .symbol-indent { flex-shrink: 0; } .symbol-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 4px; flex-shrink: 0; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; &.kind-file { background-color: rgba(158, 158, 158, 0.2); color: #9e9e9e; } &.kind-module { background-color: rgba(255, 152, 0, 0.2); color: #ff9800; } &.kind-namespace { background-color: rgba(255, 152, 0, 0.2); color: #ff9800; } &.kind-package { background-color: rgba(121, 85, 72, 0.2); color: #795548; } &.kind-class { background-color: rgba(255, 193, 7, 0.2); color: #ffc107; } &.kind-method { background-color: rgba(156, 39, 176, 0.2); color: #9c27b0; } &.kind-property { background-color: rgba(0, 150, 136, 0.2); color: #009688; } &.kind-field { background-color: rgba(0, 150, 136, 0.2); color: #009688; } &.kind-constructor { background-color: rgba(156, 39, 176, 0.2); color: #9c27b0; } &.kind-enum { background-color: rgba(255, 152, 0, 0.2); color: #ff9800; } &.kind-interface { background-color: rgba(0, 188, 212, 0.2); color: #00bcd4; } &.kind-function { background-color: rgba(103, 58, 183, 0.2); color: #673ab7; } &.kind-variable { background-color: rgba(33, 150, 243, 0.2); color: #2196f3; } &.kind-constant { background-color: rgba(33, 150, 243, 0.2); color: #2196f3; } &.kind-string { background-color: rgba(76, 175, 80, 0.2); color: #4caf50; } &.kind-number { background-color: rgba(139, 195, 74, 0.2); color: #8bc34a; } &.kind-boolean { background-color: rgba(33, 150, 243, 0.2); color: #2196f3; } &.kind-array { background-color: rgba(255, 87, 34, 0.2); color: #ff5722; } &.kind-object { background-color: rgba(96, 125, 139, 0.2); color: #607d8b; } &.kind-key { background-color: rgba(233, 30, 99, 0.2); color: #e91e63; } &.kind-null { background-color: rgba(158, 158, 158, 0.2); color: #9e9e9e; } &.kind-enummember { background-color: rgba(255, 152, 0, 0.2); color: #ff9800; } &.kind-struct { background-color: rgba(96, 125, 139, 0.2); color: #607d8b; } &.kind-event { background-color: rgba(255, 235, 59, 0.2); color: #fdd835; } &.kind-operator { background-color: rgba(158, 158, 158, 0.2); color: #9e9e9e; } &.kind-typeparameter { background-color: rgba(0, 188, 212, 0.2); color: #00bcd4; } } .symbol-info { flex: 1; min-width: 0; display: flex; flex-direction: column; .symbol-name { font-size: 0.9rem; font-weight: 500; color: var(--primary-text-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; .symbol-match { background-color: color-mix(in srgb, var(--active-color) 30%, transparent); border-radius: 2px; padding: 0 2px; } } .symbol-detail { font-size: 0.75rem; color: var(--secondary-text-color); opacity: 0.8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } .symbol-line { font-size: 0.75rem; color: var(--secondary-text-color); font-family: monospace; flex-shrink: 0; opacity: 0.7; } } ================================================ FILE: src/components/tabView.js ================================================ import Ref from "html-tag-js/ref"; /** * * @param {object} param0 * @param {string} param0.id * @returns */ export default function TabView({ id, disableSwipe = false }, children) { let moveX = 0; let moveY = 0; let lastX = 0; let lastY = 0; let isScrolling = false; const el = Ref(); return (
        {children}
        ); function ontouchstart(e) { moveX = 0; moveY = 0; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; isScrolling = false; document.addEventListener("touchmove", omtouchmove, { passive: true }); document.addEventListener("touchend", omtouchend); document.addEventListener("touchcancel", omtouchend); } function omtouchmove(e) { const { clientX, clientY } = e.touches[0]; const deltaX = lastX - clientX; const deltaY = lastY - clientY; // Determine if the user is primarily scrolling vertically if (!isScrolling) { isScrolling = Math.abs(deltaY) > Math.abs(deltaX); } if (!isScrolling) { moveX += deltaX; e.preventDefault(); } lastX = clientX; lastY = clientY; } function omtouchend() { document.removeEventListener("touchmove", omtouchmove); document.removeEventListener("touchend", omtouchend); document.removeEventListener("touchcancel", omtouchend); // Only change tabs when a significant horizontal swipe is detected and not scrolling vertically if (!isScrolling && Math.abs(moveX) > 100) { const tabs = Array.from(el.get(".options").children); const currentTab = el.get(".options>span.active"); const direction = moveX > 0 ? 1 : -1; const currentTabIndex = tabs.indexOf(currentTab); const nextTabIndex = (currentTabIndex + direction + tabs.length) % tabs.length; tabs[nextTabIndex].click(); currentTab.classList.remove("active"); tabs[nextTabIndex].classList.add("active"); } } function changeTab(e) { const { target } = e; if (!target.matches(".options>span")) return; const currentTab = el.get(".options>span.active"); if (target === currentTab) return; currentTab.classList.remove("active"); target.classList.add("active"); } } ================================================ FILE: src/components/terminal/index.js ================================================ /** * Terminal Components Export */ import TerminalComponent from "./terminal"; import { DEFAULT_TERMINAL_SETTINGS } from "./terminalDefaults"; import TerminalManager from "./terminalManager"; import TerminalThemeManager from "./terminalThemeManager"; export { DEFAULT_TERMINAL_SETTINGS, TerminalComponent, TerminalManager, TerminalThemeManager, }; export default { TerminalComponent, TerminalManager, TerminalThemeManager, DEFAULT_TERMINAL_SETTINGS, }; ================================================ FILE: src/components/terminal/ligatures.js ================================================ // pretty basic ligature implementation for webview export default class LigaturesAddon { constructor(options = {}) { // fallback ligatures if a font does not support ligatures natively this._fallbackLigatures = options.fallbackLigatures || [ "<--", "<---", "<<-", "<-", "->", "->>", "-->", "--->", "<==", "<===", "<<=", "<=", "=>", "=>>", "==>", "===>", ">=", ">>=", "<->", "<-->", "<--->", "<---->", "<=>", "<==>", "<===>", "<====>", "<~~", "<~", "~>", "~~>", "::", ":::", "==", "!=", "===", "!==", ":=", ":-", ":+", "<*", "<*>", "*>", "<|", "<|>", "|>", "+:", "-:", "=:", ":>", "++", "+++", " clientX - rect.x */ offsetX = clientX - rect.x; offsetY = clientY - rect.y; tabLeft = rect.left; tabWidth = rect.width; parentLeft = parentRect.left; parentRight = parentRect.right; MAX_SCROLL = $parent.scrollWidth - parentRect.width; MIN_SCROLL = 0; // setup the cloned tab $tabClone.classList.add("drag"); $tabClone.style.height = `${rect.height}px`; $tabClone.style.width = `${rect.width}px`; $tabClone.style.transform = `translate3d(${rect.x}px, ${rect.y}px, 0)`; app.append($tabClone); $tab.click(); document.addEventListener("mousemove", onDrag, opts); document.addEventListener("touchmove", onDrag, opts); document.addEventListener("mouseup", releaseDrag, opts); document.addEventListener("touchend", releaseDrag, opts); document.addEventListener("touchcancel", releaseDrag, opts); document.addEventListener("mouseleave", releaseDrag, opts); prevScrollLeft = $parent.scrollLeft; $parent.addEventListener("scroll", preventDefaultScroll, opts); } /** * On mouse or touch move * @param {MouseEvent|TouchEvent} e * @returns */ function onDrag(e) { if (e instanceof Event) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } const { clientX, clientY } = getClientPos(e); tabLeft = clientX - offsetX; tabTop = clientY - offsetY; $tabClone.style.transform = `translate3d(${tabLeft}px, ${tabTop}px, 0)`; if ($parent.scrollWidth === $parent.clientWidth) return; const scroll = getScroll(); // if can scroll and already scrolling return // or if can't scroll and not scrolling return if (!!scroll === !!animationFrame) return; // if can't scroll and scrolling clear interval if (!scroll && animationFrame) { cancelAnimationFrame(animationFrame); animationFrame = null; return; } scrollContainer(); } /** * Cancels the drag * @param {MouseEvent} e */ function releaseDrag(e) { const { clientX, clientY } = getClientPos(e); /**@type {HTMLDivElement} target tab */ const $target = document.elementFromPoint(clientX, clientY); if ( $parent.contains($target) && // target is in parent $target !== $tab && // target is not the tab !$tab.contains($target) // target is not a child of tab ) { // get the target tab, if target is a child it will get the parent const $targetTab = $target.closest(".tile"); if ($targetTab) { const rect = $targetTab.getBoundingClientRect(); const midX = rect.left + rect.width / 2; const pointerX = tabLeft + tabWidth / 2; if (midX < pointerX) { // move right const $nextSibling = $targetTab.nextElementSibling; if ($nextSibling) { $parent.insertBefore($tab, $nextSibling); } else { $parent.appendChild($tab); } } else { $parent.insertBefore($tab, $targetTab); } updateFileList($parent); } } else if ( $target.tagName === "INPUT" || $target.tagName === "TEXTAREA" || $target.isContentEditable || $target.closest(".cm-editor") ) { // If released on an input area or CodeMirror editor const filePath = editorManager.activeFile.uri; if (filePath) { if ($target.closest(".cm-editor")) { const view = editorManager.editor; view.dispatch(view.state.replaceSelection(filePath)); } else { $target.value += filePath; } } } cancelAnimationFrame(animationFrame); $tabClone.remove(); $tabClone = null; document.removeEventListener("mousemove", onDrag, opts); document.removeEventListener("touchmove", onDrag, opts); document.removeEventListener("mouseup", releaseDrag, opts); document.removeEventListener("touchend", releaseDrag, opts); document.removeEventListener("touchcancel", releaseDrag, opts); document.removeEventListener("mouseleave", releaseDrag, opts); $parent.removeEventListener("scroll", preventDefaultScroll); } function preventDefaultScroll() { this.scrollLeft = prevScrollLeft; } /** * Scrolls the container using animation frame */ function scrollContainer() { return animate(); function animate() { const scroll = getScroll(); if (!scroll) return; prevScrollLeft = $parent.scrollLeft += scroll; animationFrame = requestAnimationFrame(animate); } } /** * Gets the client position from the event * @param {MouseEvent & TouchEvent} e * @returns {MouseEvent} */ function getClientPos(e) { const { touches, changedTouches } = e; let { clientX = 0, clientY = 0 } = e; if (touches?.length) { const [touch] = touches; clientX = touch.clientX; clientY = touch.clientY; } else if (changedTouches?.length) { const [touch] = changedTouches; clientX = touch.clientX; clientY = touch.clientY; } return { clientX, clientY }; } /** * Update the position of the file list * @param {HTMLElement} $parent */ function updateFileList($parent) { const pinnedCount = editorManager.files.filter((file) => file.pinned).length; const children = [...$parent.children]; const newFileList = []; for (let el of children) { for (let file of editorManager.files) { if (file.tab === el) { newFileList.push(file); break; } } } editorManager.files = newFileList; const draggedFile = newFileList.find((file) => file.tab === $tab); if (draggedFile) { const draggedIndex = newFileList.indexOf(draggedFile); let nextPinnedState; if (!draggedFile.pinned && draggedIndex < pinnedCount) { nextPinnedState = true; } else if (draggedFile.pinned && draggedIndex >= pinnedCount) { nextPinnedState = false; } if (nextPinnedState !== undefined) { draggedFile.setPinnedState(nextPinnedState, { reorder: false }); if (typeof editorManager.normalizePinnedTabOrder === "function") { editorManager.normalizePinnedTabOrder(editorManager.files); } } } } /** * Checks if the tab is going to scroll and returns the scroll value */ function getScroll() { const tabRight = tabLeft + tabWidth; const scrollX = $parent.scrollLeft; /**@type {number} scroll value */ let scroll = 0; // tab right should be greater than parent right const rightDiff = tabRight - parentRight; // tab left should be less than parent left const leftDiff = parentLeft - tabLeft; const scrollSpeed = (diff) => { const ratio = diff / tabWidth; return ratio * MAX_SCROLL_SPEED; }; if (leftDiff > 0 && scrollX > MIN_SCROLL) { scroll = -scrollSpeed(leftDiff); } else if (rightDiff > 0 && scrollX < MAX_SCROLL) { scroll = scrollSpeed(rightDiff); } return scroll; } ================================================ FILE: src/handlers/intent.js ================================================ import fsOperation from "fileSystem"; import openFile from "lib/openFile"; import helpers from "utils/helpers"; const handlers = []; /** * Queue to store intents that arrive before files are restored * @type {Array<{url: string, options: object}>} */ const pendingIntents = []; /** * * @param {Intent} intent */ export default async function HandleIntent(intent = {}) { const type = intent.action.split(".").slice(-1)[0]; if (["SEND", "VIEW", "EDIT"].includes(type)) { /**@type {string} */ const url = intent.fileUri || intent.data; if (!url) return; if (url.startsWith("acode://")) { const path = url.replace("acode://", ""); const [module, action, value] = path.split("/"); let defaultPrevented = false; const event = new IntentEvent(module, action, value); for (const handler of handlers) { handler(event); if (event.defaultPrevented) defaultPrevented = true; if (event.propagationStopped) break; } if (defaultPrevented) return; if (module === "plugin") { const { default: Plugin } = await import("pages/plugin"); const installed = await fsOperation(PLUGIN_DIR, value).exists(); Plugin({ id: value, installed, install: action === "install" }); } return; } if (sessionStorage.getItem("isfilesRestored") === "true") { await openFile(url, { mode: "single", render: true, }); } else { // Store the intent for later processing when files are restored pendingIntents.push({ url, options: { mode: "single", render: true, }, }); } } } HandleIntent.onError = (error) => { helpers.error(error); }; export function addIntentHandler(handler) { handlers.push(handler); } export function removeIntentHandler(handler) { const index = handlers.indexOf(handler); if (index > -1) handlers.splice(index, 1); } /** * Process all pending intents that were queued before files were restored. * This function is called after isfilesRestored is set to true in main.js. * @returns {Promise} */ export async function processPendingIntents() { if (sessionStorage.getItem("isfilesRestored") !== "true") return; // Process all pending intents while (pendingIntents.length > 0) { const pendingIntent = pendingIntents.shift(); try { await openFile(pendingIntent.url, pendingIntent.options); } catch (error) { helpers.error(error); } } } class IntentEvent { module; action; value; #defaultPrevented = false; #propagationStopped = false; /** * Creates an instance of IntentEvent. * @param {string} module * @param {string} action * @param {string} value */ constructor(module, action, value) { this.module = module; this.action = action; this.value = value; } preventDefault() { this.#defaultPrevented = true; } stopPropagation() { this.#propagationStopped = true; } get defaultPrevented() { return this.#defaultPrevented; } get propagationStopped() { return this.#propagationStopped; } } ================================================ FILE: src/handlers/keyboard.js ================================================ import { getSystemConfiguration, HARDKEYBOARDHIDDEN_NO, } from "lib/systemConfiguration"; import KeyboardEvent from "utils/keyboardEvent"; import windowResize from "./windowResize"; /** * Keyboard event list * @typedef {'key'|'keyboardShow'|'keyboardHide'|'keyboardShowStart'|'keyboardHideStart'} KeyboardEventName */ // Assuming that keyboard height is at least 200px let MIN_KEYBOARD_HEIGHT = 100; const event = { key: [], keyboardShow: [], keyboardHide: [], keyboardShowStart: [], keyboardHideStart: [], }; let escKey = false; let escResetTimeout = null; let softKeyboardHeight = 0; let windowHeight = window.innerHeight; let currentWindowHeight = windowHeight; export const keydownState = { /** * Get esc key state * @returns {boolean} */ get esc() { return escKey; }, /** * Set esc key state * @param {boolean} val */ set esc(val) { escKey = val; if (!val) return; clearTimeout(escResetTimeout); escResetTimeout = setTimeout(() => { escKey = false; }, 500); }, }; /** * Handles keyboard events * @param {KeyboardEvent} e */ export default function keyboardHandler(e) { const $target = e.target; const { key, ctrlKey, shiftKey, altKey, metaKey } = e; if (shouldIgnoreEditorShortcutTarget($target)) { keydownState.esc = key === "Escape"; return; } if (!ctrlKey && !shiftKey && !altKey && !metaKey) return; if (["Control", "Alt", "Meta", "Shift"].includes(key)) return; const target = editorManager?.editor?.contentDOM; if (!target) return; // Physical keyboard events already reaching CodeMirror should not be // re-dispatched from the document listener. if ($target === target || (target.contains?.($target) ?? false)) return; const event = KeyboardEvent("keydown", { key, ctrlKey, shiftKey, altKey, metaKey, }); target?.dispatchEvent?.(event); } /** * Returns true when a keyboard event target should keep the shortcut local * instead of forwarding it into the editor. * @param {EventTarget | null} target * @returns {boolean} */ function shouldIgnoreEditorShortcutTarget(target) { if (!(target instanceof Element)) return false; return ( target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target.isContentEditable || !!target.closest(".prompt, #palette") ); } document.addEventListener("admob.banner.size", async (event) => { const { height } = event.size; MIN_KEYBOARD_HEIGHT = height + 10; }); windowResize.on("resizeStart", async () => { const { keyboardHeight, hardKeyboardHidden } = await getSystemConfiguration(); const externalKeyboard = hardKeyboardHidden === HARDKEYBOARDHIDDEN_NO; if (currentWindowHeight > window.innerHeight) { // height decreasing softKeyboardHeight = keyboardHeight > MIN_KEYBOARD_HEIGHT ? keyboardHeight : 0; if (!externalKeyboard && softKeyboardHeight) { emit("keyboardShowStart"); } } else if (currentWindowHeight < window.innerHeight) { // height increasing if (!externalKeyboard && softKeyboardHeight) { emit("keyboardHideStart"); } } currentWindowHeight = window.innerHeight; }); windowResize.on("resize", async () => { currentWindowHeight = window.innerHeight; if (currentWindowHeight > windowHeight) { windowHeight = currentWindowHeight; } const { hardKeyboardHidden } = await getSystemConfiguration(); const externalKeyboard = hardKeyboardHidden === HARDKEYBOARDHIDDEN_NO; if (externalKeyboard || !softKeyboardHeight) return; const keyboardHiddenYes = windowHeight <= window.innerHeight; if (keyboardHiddenYes) { emit("keyboardHide"); } else { emit("keyboardShow"); } focusBlurEditor(keyboardHiddenYes); showHideAd(keyboardHiddenYes); }); /** * Add event listener for keyboard event. * @param {KeyboardEventName} eventName * @param {Function} callback * @returns */ keyboardHandler.on = (eventName, callback) => { if (!event[eventName]) return; event[eventName].push(callback); }; /** * Remove event listener for keyboard event. * @param {KeyboardEventName} eventName * @param {Function} callback * @returns */ keyboardHandler.off = (eventName, callback) => { if (!event[eventName]) return; event[eventName] = event[eventName].filter((cb) => cb !== callback); }; /** * Emit keyboard event. * @param {KeyboardEventName} eventName * @returns */ function emit(eventName) { if (!event[eventName]) return; event[eventName].forEach((cb) => cb()); } /** * Focus the editor if keyboard is visible, blur it otherwise. * @param {boolean} keyboardHidden * @returns */ function focusBlurEditor(keyboardHidden) { if (keyboardHidden) { document.activeElement?.blur(); } } /** * Show ad if keyboard is hidden and ad is active, hide ad otherwise. * @param {boolean} keyboardHidden */ function showHideAd(keyboardHidden) { const bannerIsActive = !!window.ad?.active; if (!keyboardHidden && bannerIsActive) { window.ad?.hide(); } else if (bannerIsActive) { window.ad?.show(); } } ================================================ FILE: src/handlers/purchase.js ================================================ import helpers from "utils/helpers"; export default function purchaseListener(onpurchase, onerror) { return [ (purchases) => { const [purchase] = purchases; if (purchase.purchaseState === iap.PURCHASE_STATE_PURCHASED) { if (!purchase.isAcknowledged) { iap.acknowledgePurchase( purchase.purchaseToken, () => { onpurchase(); }, (error) => { if (typeof onerror === "function") onerror(error); }, ); return; } onpurchase(); return; } const message = purchase.purchaseState === iap.PURCHASE_STATE_PENDING ? strings["purchase pending"] : strings.failed; helpers.error(message); if (typeof onerror === "function") onerror(message); }, (error) => { if (error === iap.ITEM_ALREADY_OWNED) { onpurchase(); return; } let message = error === iap.USER_CANCELED ? strings.failed : strings.canceled; if (typeof onerror === "function") onerror(message); }, ]; } ================================================ FILE: src/handlers/quickTools.js ================================================ import { findNext as cmFindNext, findPrevious as cmFindPrevious, replaceAll as cmReplaceAll, replaceNext as cmReplaceNext, getSearchQuery, SearchQuery, setSearchQuery, } from "@codemirror/search"; import { executeCommand } from "cm/commandRegistry"; import quickTools from "components/quickTools"; import actionStack from "lib/actionStack"; import searchHistory from "lib/searchHistory"; import appSettings from "lib/settings"; import searchSettings from "settings/searchSettings"; import KeyboardEvent from "utils/keyboardEvent"; /**@type {HTMLInputElement | HTMLTextAreaElement} */ let input; const state = { shift: false, alt: false, ctrl: false, meta: false, }; const events = { shift: [], alt: [], ctrl: [], meta: [], }; function getRefValue(ref) { if (!ref) return ""; const direct = ref.value; if (typeof direct === "string") return direct; if (typeof direct === "number") return String(direct); if (ref.el) { const elValue = ref.el.value; if (typeof elValue === "string") return elValue; if (typeof elValue === "number") return String(elValue); } return ""; } function setRefValue(ref, value) { if (!ref) return; const normalized = typeof value === "string" ? value : String(value ?? ""); if (ref.el) ref.el.value = normalized; ref.value = normalized; } function applySearchQuery(editor, searchValue, replaceValue) { if (!editor) return null; const options = appSettings?.value?.search ?? {}; const queryConfig = { search: String(searchValue ?? ""), caseSensitive: !!options.caseSensitive, regexp: !!options.regExp, wholeWord: !!options.wholeWord, }; if (replaceValue !== undefined) { queryConfig.replace = String(replaceValue ?? ""); } const query = new SearchQuery(queryConfig); editor.dispatch({ effects: setSearchQuery.of(query) }); return query; } function clearSearchQuery(editor) { if (!editor) return; editor.dispatch({ effects: setSearchQuery.of(new SearchQuery({ search: "" })), }); } function getSelectedText(editor) { if (!editor) return ""; if (typeof editor.getSelectedText === "function") { try { return editor.getSelectedText() ?? ""; } catch (_) { // fall back to CodeMirror state } } try { const { state } = editor; if (!state) return ""; const { from, to } = state.selection.main ?? {}; if (typeof from !== "number" || typeof to !== "number") return ""; if (from === to) return ""; return state.sliceDoc(from, to); } catch (_) { return ""; } } function selectionMatchesQuery(editor, query) { try { if (!editor || !query || !query.valid || !query.search) return false; const range = editor.state?.selection?.main; if (!range || range.from === range.to) return false; const cursor = query.getCursor(editor.state.doc, range.from, range.to); cursor.next(); return ( !cursor.done && cursor.value.from === range.from && cursor.value.to === range.to ); } catch (_) { return false; } } /** * @typedef { 'shift' | 'alt' | 'ctrl' | 'meta' } QuickToolsEvent * @typedef {(value: boolean)=>void} QuickToolsEventListener */ quickTools.$input.addEventListener("input", (e) => { const key = e.target.value.toUpperCase(); quickTools.$input.value = ""; if (!key || key.length > 1) return; const keyCombination = getKeys({ key }); if ( keyCombination.shiftKey && !keyCombination.ctrlKey && !keyCombination.altKey && !keyCombination.metaKey ) { resetKeys(); insertText(shiftKeyMapping(key)); return; } const event = KeyboardEvent("keydown", keyCombination); input = input || editorManager.editor.contentDOM; resetKeys(); input.dispatchEvent(event); }); quickTools.$input.addEventListener("keydown", (e) => { const { keyCode, key, which } = e; const keyCombination = getKeys({ keyCode, key, which }); if ( !["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown"].includes( keyCombination.key, ) ) return; e.preventDefault(); const event = KeyboardEvent("keydown", keyCombination); if (input && input !== quickTools.$input) { input.dispatchEvent(event); } else { // Otherwise fallback to editor view content editorManager.editor.contentDOM.dispatchEvent(event); } }); appSettings.on("update:quicktoolsItems:after", () => { setTimeout(() => { if (actionStack.has("search-bar")) return; const { $footer, $row1, $row2 } = quickTools; const height = getFooterHeight(); $footer.content = [$row1, $row2].slice(0, height); }, 100); }); let historyNavigationInitialized = false; // Initialize history navigation function setupHistoryNavigation() { if (historyNavigationInitialized) return; historyNavigationInitialized = true; const { $searchInput, $replaceInput } = quickTools; // Search input history navigation if ($searchInput.el) { $searchInput.el.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "f") { e.preventDefault(); const { editor, activeFile } = editorManager; editor.focus(); actionStack.get("search-bar")?.action(); } else if (e.key === "ArrowUp") { e.preventDefault(); const newValue = searchHistory.navigateSearchUp($searchInput.el.value); $searchInput.el.value = newValue; // Trigger search find(0, false); } else if (e.key === "ArrowDown") { e.preventDefault(); const newValue = searchHistory.navigateSearchDown( $searchInput.el.value, ); $searchInput.el.value = newValue; // Trigger search find(0, false); } else if (e.key === "Enter" || e.key === "Escape") { // Reset navigation on enter or escape searchHistory.resetSearchNavigation(); } }); // Reset navigation when user starts typing $searchInput.el.addEventListener("input", () => { searchHistory.resetSearchNavigation(); }); } // Replace input history navigation if ($replaceInput.el) { $replaceInput.el.addEventListener("keydown", (e) => { if (e.key === "ArrowUp") { e.preventDefault(); const newValue = searchHistory.navigateReplaceUp( $replaceInput.el.value, ); $replaceInput.el.value = newValue; } else if (e.key === "ArrowDown") { e.preventDefault(); const newValue = searchHistory.navigateReplaceDown( $replaceInput.el.value, ); $replaceInput.el.value = newValue; } else if (e.key === "Enter" || e.key === "Escape") { // Reset navigation on enter or escape searchHistory.resetReplaceNavigation(); } }); // Reset navigation when user starts typing $replaceInput.el.addEventListener("input", () => { searchHistory.resetReplaceNavigation(); }); } } export const key = { get shift() { return state.shift; }, get alt() { return state.alt; }, get ctrl() { return state.ctrl; }, get meta() { return state.meta; }, /** * Add listener when key changes * @param {QuickToolsEvent} event QuickTools event * @param {QuickToolsEventListener} callback Callback to call when key changes */ on(event, callback) { events[event].push(callback); }, /** * Remove listener * @param {QuickToolsEvent} event QuickTools event * @param {QuickToolsEventListener} callback Callback to remove */ off(event, callback) { events[event] = events[event].filter((cb) => cb !== callback); }, }; /** * Performs quick actions * @param {string} action Action to perform * @param {string} value Value for the action * @returns {boolean} Whether the action was performed */ export default function actions(action, value) { const { editor } = editorManager; const { $input, $replaceInput } = quickTools; if (Object.keys(state).includes(action)) { setInput(); value = !state[action]; state[action] = value; events[action].forEach((cb) => cb(value)); if (Object.values(state).includes(true)) { $input.focus(); } else if (input) { input.focus(); } else { $input.blur(); } return value; } switch (action) { case "insert": return insertText(value); case "command": { const commandName = typeof value === "string" ? value : String(value ?? ""); if (!commandName) return false; return executeCommand(commandName, editor); } case "key": { value = Number.parseInt(value, 10); const event = KeyboardEvent("keydown", getKeys({ keyCode: value })); if (value > 40 && value < 37) { resetKeys(); } setInput(); input.dispatchEvent(event); return true; } case "search": toggleSearch(); return actionStack.has("search-bar"); case "toggle": toggle(); return true; case "set-height": if (typeof value === "object") { setHeight(value.height, value.save); } else { setHeight(value); } return true; case "search-prev": if (quickTools.$searchInput.el.value) { searchHistory.addToHistory(quickTools.$searchInput.el.value); } find(1, true); return true; case "search-next": if (quickTools.$searchInput.el.value) { searchHistory.addToHistory(quickTools.$searchInput.el.value); } find(1, false); return true; case "search-settings": searchSettings().show(); return true; case "search-replace": if ($replaceInput.value) { searchHistory.addToHistory($replaceInput.value); } if (editor) { const replaceValue = getRefValue($replaceInput); const query = applySearchQuery( editor, getRefValue(quickTools.$searchInput), replaceValue, ); if (query && query.search && query.valid) { cmReplaceNext(editor); } updateSearchState(); } return true; case "search-replace-all": if ($replaceInput.value) { searchHistory.addToHistory($replaceInput.value); } if (editor) { const replaceValue = getRefValue($replaceInput); const query = applySearchQuery( editor, getRefValue(quickTools.$searchInput), replaceValue, ); if (query && query.search && query.valid) { cmReplaceAll(editor); } } updateSearchState(); return true; default: return false; } } function setInput() { const terminalInput = getActiveTerminalInput(); if (terminalInput) { input = terminalInput; return; } const { activeElement } = document; if ( !activeElement || activeElement === quickTools.$input || activeElement === document.body ) return; input = activeElement; } function toggleSearch() { const $footer = quickTools.$footer; const $searchRow1 = quickTools.$searchRow1; const $searchRow2 = quickTools.$searchRow2; const $searchInput = quickTools.$searchInput.el; const $toggler = quickTools.$toggler; const { editor } = editorManager; const selectedText = getSelectedText(editor); if (!$footer.contains($searchRow1)) { const { className } = quickTools.$toggler; const $content = [...$footer.children]; const footerHeight = getFooterHeight(); $toggler.className = "floating icon clearclose"; $footer.content = [$searchRow1, $searchRow2]; setRefValue($searchInput, selectedText || ""); $searchInput.oninput = function () { find(0, false); }; $searchInput.onsearch = function () { if (this.value) { searchHistory.addToHistory(this.value); find(1, false); } else { find(0, false); } }; // Setup history navigation for search inputs setupHistoryNavigation(); setFooterHeight(2); find(0, false); actionStack.push({ id: "search-bar", action: () => { removeSearch(); $footer.content = $content; $toggler.className = className; setFooterHeight(footerHeight); }, }); } else { const inputValue = getRefValue($searchInput); if (inputValue !== selectedText) { setRefValue($searchInput, selectedText || ""); find(0, false); return; } actionStack.get("search-bar").action(); } $searchInput.focus(); } function toggle() { // if search is active, remove it const searchBar = actionStack.get("search-bar"); if (searchBar?.action) { searchBar.action(); return; } const $footer = quickTools.$footer; const $row1 = quickTools.$row1; const $row2 = quickTools.$row2; if (!$footer.contains($row1)) { setHeight(); } else if (!$footer.contains($row2)) { setHeight(2); } else { setHeight(0); } focusEditor(); } function setHeight(height = 1, save = true) { const { $footer, $row1, $row2 } = quickTools; const { editor, activeFile } = editorManager; // If active file has hideQuickTools, force height to 0 and don't save if (activeFile?.hideQuickTools) { height = 0; save = false; } setFooterHeight(height); if (save) { appSettings.update({ quickTools: height }, false); } if (!height) { $row1.remove(); $row2.remove(); return; } if (height >= 1) { $row1.style.scrollBehavior = "unset"; $footer.append($row1); $row1.scrollLeft = Number.parseInt( localStorage.quickToolRow1ScrollLeft, 10, ); --height; } if (height >= 1) { $row2.style.scrollBehavior = "unset"; $footer.append($row2); $row2.scrollLeft = Number.parseInt( localStorage.quickToolRow2ScrollLeft, 10, ); --height; } } /** * Removes search bar from footer */ function removeSearch() { const { $footer, $searchRow1, $searchRow2 } = quickTools; if (!$footer.contains($searchRow1)) return; actionStack.remove("search-bar"); $footer.removeAttribute("data-searching"); $searchRow1.remove(); $searchRow2.remove(); // Reset history navigation when search is closed searchHistory.resetAllNavigation(); const { activeFile, editor } = editorManager; // Check if current tab is a terminal if ( activeFile && activeFile.type === "terminal" && activeFile.terminalComponent ) { activeFile.terminalComponent.searchAddon?.clearDecorations(); activeFile.terminalComponent.searchAddon?.clearActiveDecoration(); return; } clearSearchQuery(editor); focusEditor(); } /** * Finds the next/previous search result * @param {number} skip Number of search results to skip * @param {boolean} backward Whether to search backward */ function find(skip, backward) { const { $searchInput } = quickTools; const { activeFile } = editorManager; // Check if current tab is a terminal if ( activeFile && activeFile.type === "terminal" && activeFile.terminalComponent ) { activeFile.terminalComponent.search($searchInput.value, skip, backward); } else { const editor = editorManager.editor; const searchValue = getRefValue($searchInput); const query = applySearchQuery(editor, searchValue); if (!query || !query.search || !query.valid) { updateSearchState(); return; } const normalizedSkip = Number(skip) || 0; if (normalizedSkip === 0 && selectionMatchesQuery(editor, query)) { updateSearchState(); return; } const steps = Math.max(1, normalizedSkip); const runCommand = backward ? cmFindPrevious : cmFindNext; for (let i = 0; i < steps; ++i) { if (!runCommand(editor)) break; } } updateSearchState(); } function updateSearchState() { const MAX_COUNT = 999; const { activeFile, editor } = editorManager; const { $searchPos, $searchTotal } = quickTools; // Check if current tab is a terminal if (activeFile && activeFile.type === "terminal") { // For terminal, we can't easily count all matches like in ACE editor // xterm search addon doesn't provide this information // So we just show a generic indicator $searchTotal.textContent = "?"; $searchPos.textContent = "?"; return; } const query = editor ? getSearchQuery(editor.state) : null; if (!query || !query.search || !query.valid) { $searchTotal.textContent = "0"; $searchPos.textContent = "0"; return; } const cursor = query.getCursor(editor.state.doc); let total = 0; let before = 0; let limited = false; const cursorPos = editor.state.selection.main.head; for (cursor.next(); !cursor.done; cursor.next()) { total++; if (cursorPos >= cursor.value.from) { before = Math.min(total, MAX_COUNT); } if (total === MAX_COUNT) { cursor.next(); limited = !cursor.done; break; } } $searchTotal.textContent = limited ? "999+" : String(total); $searchPos.textContent = String(before); } /** * Sets the height of the footer * @param {number} height Height of the footer * @returns {void} */ function setFooterHeight(height) { const { $toggler, $footer, $searchRow1 } = quickTools; if (height) root.setAttribute("footer-height", height); else root.removeAttribute("footer-height"); if ($toggler.classList.contains("clearclose")) return; if (height > 1 && !$footer.contains($searchRow1)) { $toggler.classList.remove("keyboard_arrow_up"); $toggler.classList.add("keyboard_arrow_down"); } else { $toggler.classList.remove("keyboard_arrow_down"); $toggler.classList.add("keyboard_arrow_up"); } } function getFooterHeight() { return Number.parseInt(root.getAttribute("footer-height")) || 0; } function focusEditor() { const { editor, activeFile } = editorManager; if (!activeFile?.focused) { return; } if (activeFile.type === "terminal" && activeFile.terminalComponent) { activeFile.terminalComponent.focus(); return; } if (editor) { editor.focus(); } } function resetKeys() { state.shift = false; events.shift.forEach((cb) => cb(false)); state.alt = false; events.alt.forEach((cb) => cb(false)); state.ctrl = false; events.ctrl.forEach((cb) => cb(false)); state.meta = false; events.meta.forEach((cb) => cb(false)); input?.focus?.(); } /** * Gets the current state of the modifier keys * @param {object} key Key object * @param {int} [key.keyCode] Key code * @param {string} [key.key] Key * @returns {KeyboardEventInit} */ export function getKeys(key = {}) { return { ...key, shiftKey: state.shift, altKey: state.alt, ctrlKey: state.ctrl, metaKey: state.meta, }; } function getActiveTerminalComponent() { const { activeFile } = editorManager; if (activeFile?.type !== "terminal") return null; return activeFile.terminalComponent || null; } function getActiveTerminalInput() { return getActiveTerminalComponent()?.terminal?.textarea || null; } function insertText(value) { const text = String(value ?? ""); if (!text) return false; const terminalComponent = getActiveTerminalComponent(); if (terminalComponent?.terminal) { if (typeof terminalComponent.terminal.paste === "function") { terminalComponent.terminal.paste(text); terminalComponent.focus(); return true; } if (terminalComponent.serverMode && terminalComponent.isConnected) { terminalComponent.write(text); terminalComponent.focus(); return true; } return false; } const { editor } = editorManager; return editor ? editor.insert(text) : false; } function shiftKeyMapping(char) { switch (char) { case "1": return "!"; case "2": return "@"; case "3": return "#"; case "4": return "$"; case "5": return "%"; case "6": return "^"; case "7": return "&"; case "8": return "*"; case "9": return "("; case "0": return ")"; case "-": return "_"; case "=": return "+"; case "[": return "{"; case "]": return "}"; case "\\": return "|"; case ";": return ":"; case "'": return '"'; case ",": return "<"; case ".": return ">"; case "/": return "?"; default: return char.toUpperCase(); } } ================================================ FILE: src/handlers/quickToolsInit.js ================================================ import quickTools from "components/quickTools"; import constants from "lib/constants"; import appSettings from "lib/settings"; import actions, { key } from "./quickTools"; const CONTEXT_MENU_TIMEOUT = 500; const MOVE_X_THRESHOLD = 50; let time; let moveX; let movedX; // total moved x let touchMoved; let isClickMode; let contextmenu; let startTime; let contextmenuTimeout; let active = false; // is button already active let slide = 0; /**@type {HTMLElement} */ let $row; /**@type {number} */ let timeout; /**@type {HTMLElement} */ let $touchstart; function reset() { moveX = 0; movedX = 0; time = 300; $row = null; $touchstart = null; contextmenu = false; touchMoved = undefined; contextmenuTimeout = null; active = false; } /** * Initialize quick tools * @param {HTMLElement} $footer */ export default function init() { const { $footer, $toggler, $input } = quickTools; $toggler.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); actions("toggle"); }); key.on("shift", (value) => { if (value) $footer.setAttribute("data-shift", "true"); else $footer.removeAttribute("data-shift"); }); key.on("ctrl", (value) => { if (value) $footer.setAttribute("data-ctrl", "true"); else $footer.removeAttribute("data-ctrl"); }); key.on("alt", (value) => { if (value) $footer.setAttribute("data-alt", "true"); else $footer.removeAttribute("data-alt"); }); key.on("meta", (value) => { if (value) $footer.setAttribute("data-meta", "true"); else $footer.removeAttribute("data-meta"); }); editorManager.on(["file-content-changed", "switch-file"], () => { if (editorManager.activeFile?.isUnsaved) { $footer.setAttribute("data-unsaved", "true"); } else { $footer.removeAttribute("data-unsaved"); } }); editorManager.on("save-file", () => { $footer.removeAttribute("data-unsaved"); }); root.append($footer, $toggler); document.body.append($input); if ( appSettings.value.quickToolsTriggerMode === appSettings.QUICKTOOLS_TRIGGER_MODE_CLICK ) { isClickMode = true; $footer.addEventListener("click", onclick); $footer.addEventListener("contextmenu", oncontextmenu, true); $footer.addEventListener("wheel", onwheel, { passive: false }); } else { $footer.addEventListener("touchstart", touchstart); $footer.addEventListener("keydown", touchstart); } appSettings.on("update:quickToolsTriggerMode", (value) => { if (value === appSettings.QUICKTOOLS_TRIGGER_MODE_CLICK) { $footer.removeEventListener("touchstart", touchstart); $footer.removeEventListener("keydown", touchstart); $footer.addEventListener("contextmenu", onclick, true); $footer.addEventListener("click", onclick); $footer.addEventListener("wheel", onwheel, { passive: false }); } else { $footer.removeEventListener("contextmenu", onclick, true); $footer.removeEventListener("click", onclick); $footer.removeEventListener("wheel", onwheel); $footer.addEventListener("keydown", touchstart); $footer.addEventListener("touchstart", touchstart); } }); } function onwheel(e) { e.preventDefault(); const $el = e.target; const { $row1, $row2 } = quickTools; let $row; if ($row1?.contains($el)) { $row = $row1; } else if ($row2?.contains($el)) { $row = $row2; } if ($row) { $row.scrollLeft += e.deltaY; } } function onclick(e) { reset(); e.preventDefault(); e.stopPropagation(); click(e.target); clearTimeout(timeout); } function touchstart(e) { reset(); const $el = e.target; if ($el instanceof HTMLInputElement) { return; } startTime = performance.now(); $touchstart = $el; e.preventDefault(); e.stopPropagation(); if ($el.dataset.repeat === "true") { contextmenuTimeout = setTimeout(() => { if (touchMoved) return; contextmenu = true; oncontextmenu(e); }, CONTEXT_MENU_TIMEOUT); } if ($el.classList.contains("active")) { active = true; } else { $el.classList.add("active"); } document.addEventListener("touchmove", touchmove); document.addEventListener("keyup", touchcancel); document.addEventListener("touchend", touchend); document.addEventListener("touchcancel", touchcancel); } /** * Event handler for touchmove event * @param {TouchEvent} e */ function touchmove(e) { if (contextmenu || touchMoved === false) return; const $el = e.target; const { $row1, $row2 } = quickTools; const { clientX } = e.touches[0]; if (moveX === 0) { moveX = clientX; return; } const diff = moveX - clientX; if (touchMoved === undefined) { if (Math.abs(diff) > appSettings.value.touchMoveThreshold) { touchMoved = true; } else { if ($row) { const movedX = $row.scrollLeft % $row.clientWidth; // $row.scrollBy(-movedX, 0); // scrollBy is not working on mobile $row.scrollLeft -= movedX; } touchMoved = false; return; } } movedX += diff; if (!$row) { if ($row1?.contains($el)) { $row = $row1; } else if ($row2?.contains($el)) { $row = $row2; } slide = Number.parseInt($row.scrollLeft / $row.clientWidth, 10); } if ($row) { $row.style.scrollBehavior = "unset"; $row.scrollLeft += diff; } if (!active) $touchstart.classList.remove("active"); moveX = clientX; } /** * Event handler for touchend event * @param {TouchEvent} e */ function touchend(e) { const { $row1 } = quickTools; const $el = document.elementFromPoint( e.changedTouches[0].clientX, e.changedTouches[0].clientY, ); if (touchMoved && $row) { $row.style.scrollBehavior = "smooth"; let scroll = 0; if (movedX < 0 && movedX < -MOVE_X_THRESHOLD) { scroll = (slide - 1) * $row.clientWidth; } else if (movedX > 0 && movedX > MOVE_X_THRESHOLD) { scroll = (slide + 1) * $row.clientWidth; } else { scroll = slide * $row.clientWidth; } if ($row === $row1) { localStorage.quickToolRow1ScrollLeft = scroll; } else { localStorage.quickToolRow2ScrollLeft = scroll; } $row.scrollLeft = scroll; touchcancel(e); if ($el === $touchstart && performance.now() - startTime < 100) { click($el); } return; } if ($touchstart !== $el || contextmenu) { touchcancel(e); return; } touchcancel(e); click($el); } /** * * @param {TouchEvent} e */ function touchcancel(e) { document.removeEventListener("keyup", touchcancel); document.removeEventListener("touchend", touchend); document.removeEventListener("touchcancel", touchcancel); document.removeEventListener("touchmove", touchmove); clearTimeout(timeout); clearTimeout(contextmenuTimeout); if (!active) $touchstart.classList.remove("active"); } /** * Handler for contextmenu event * @param {TouchEvent|MouseEvent} e */ function oncontextmenu(e) { const $el = e.target; const { lock } = $el.dataset; if (lock === "true") { return; // because button with lock=true is locked when clicked so contextmenu doesn't make sense } const { editor, activeFile } = editorManager; if (isClickMode && appSettings.value.vibrateOnTap) { navigator.vibrate(constants.VIBRATION_TIME_LONG); $el.classList.add("active"); } const dispatchEventWithTimeout = () => { if (time > 50) { time -= 10; } click($el); timeout = setTimeout(dispatchEventWithTimeout, time); }; if (activeFile.focused) { editor.focus(); } dispatchEventWithTimeout(); } /** * Executes the action associated with the button * @param {HTMLElement} $el */ function click($el) { $el.classList.add("click"); clearTimeout($el.dataset.timeout); $el.dataset.timeout = setTimeout(() => { $el.classList.remove("click"); }, 300); const { action } = $el.dataset; if (!action) return; let { value } = $el.dataset; if (!value) { value = $el.value; } actions(action, value); } ================================================ FILE: src/handlers/windowResize.js ================================================ let resizeTimeout; /** * @typedef {'resize'|'resizeStart'} ResizeEventName */ const event = { resize: [], resizeStart: [], }; /** * *external keyboard* * -> config.hardKeyboardHidden = HARDKEYBOARDHIDDEN_NO * This means that external keyboard is connected. If external keyboard is connected, * we don't need to do anything. * * *floating keyboard is active* * -> No way to detect this. * * *keyboard is hidden* * -> If window height is smaller than earlier, keyboard is visible. and vice versa. * If keyboard is not visible, we need to blur the editor, and hide the ad. * Else we need to focus the editor, and show the ad. */ export default function windowResize() { if (!resizeTimeout) { emit("resizeStart"); } clearTimeout(resizeTimeout); resizeTimeout = setTimeout(onResize, 100); } /** * Add event listener for window done resizing. * @param {ResizeEventName} eventName * @param {Function} callback * @returns */ windowResize.on = (eventName, callback) => { if (!event[eventName]) return; event[eventName].push(callback); }; /** * Remove event listener for window done resizing. * @param {ResizeEventName} eventName * @param {Function} callback * @returns */ windowResize.off = (eventName, callback) => { if (!event[eventName]) return; event[eventName] = event[eventName].filter((cb) => cb !== callback); }; /** * Timeout function for window done resizing. */ function onResize() { resizeTimeout = null; emit("resize"); } /** * Emit event * @param {ResizeEventName} eventName * @returns */ function emit(eventName) { if (!event[eventName]) return; event[eventName].forEach((cb) => cb()); } ================================================ FILE: src/index.d.ts ================================================ type LanguageMap = { [key: string]: string }; declare const strings: LanguageMap; declare const ASSETS_DIRECTORY: string; 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; interface Window { ASSETS_DIRECTORY: string; DATA_STORAGE: string; CACHE_STORAGE: string; PLUGIN_DIR: string; KEYBINDING_FILE: string; IS_FREE_VERSION: string; ANDROID_SDK_INT: number; DOES_SUPPORT_THEME: boolean; acode: object; } interface String { /** * Capitalize the first letter of a string */ capitalize(): string; /** * Generate a hash from a string */ hash(): string; } type ExecutorCallback = ( type: "stdout" | "stderr" | "exit", data: string, ) => void; interface Executor { execute: (command: string, alpine: boolean) => Promise; start: ( command: string, callback: ExecutorCallback, alpine: boolean, ) => Promise; write: (uuid: string, input: string) => Promise; stop: (uuid: string) => Promise; isRunning: (uuid: string) => Promise; /** Move the executor service to the foreground (shows notification) */ moveToForeground: () => Promise; /** Move the executor service to the background (hides notification) */ moveToBackground: () => Promise; /** Stop the executor service completely */ stopService: () => Promise; /** * Background executor */ BackgroundExecutor: Executor; } declare const Executor: Executor | undefined; interface Window { Executor?: Executor; editorManager?: EditorManager; } interface EditorManager { editor?: import("@codemirror/view").EditorView; isCodeMirror?: boolean; activeFile?: AcodeFile; getLspMetadata?: (file: AcodeFile) => LspFileMetadata | null; } interface LspFileMetadata { uri: string; languageId?: string; languageName?: string; view?: import("@codemirror/view").EditorView; file?: AcodeFile; rootUri?: string; } /** * Acode file object */ interface AcodeFile { uri?: string; name?: string; session?: unknown; cacheFile?: string; [key: string]: unknown; } // Extend globalThis with Executor declare global { var Executor: Executor | undefined; } ================================================ FILE: src/lang/ar-ye.json ================================================ { "lang": "العربية", "about": "حول", "active files": "الملفات المفتوحة", "add path": "إضافة مسار", "alert": "تنبيه", "app theme": "سمة التطبيق", "autocorrect": "تفعيل التصحيح التلقائي؟", "autosave": "حفظ تلقائي", "cancel": "إلغاء", "change language": "تغيير اللغة", "choose color": "اختر لوناً", "clear": "مسح", "close app": "إغلاق التطبيق؟", "commit message": "رسالة الـ Commit", "console": "وحدة التحكم", "conflict error": "خطأ! يرجى الانتظار قبل القيام بـ commit آخر.", "copy": "نسخ", "create folder error": "فشل إنشاء مجلد جديد", "cut": "قص", "delete": "حذف", "dependencies": "التبعيات", "delay": "الوقت بالمللي ثانية", "editor settings": "إعدادات المحرر", "editor theme": "سمة المحرر", "enter file name": "أدخل اسم الملف", "enter folder name": "أدخل اسم المجلد", "empty folder message": "مجلد فارغ", "enter line number": "أدخل رقم السطر", "error": "خطأ", "failed": "فشل", "file already exists": "الملف موجود مسبقاً", "file already exists force": "الملف موجود مسبقاً، هل تريد الاستبدال؟", "file changed": "تغير الملف، هل تريد إعادة التحميل؟", "file deleted": "تم الحذف بنجاح", "file is not supported": "الملف غير مدعوم", "file not supported": "نوع الملف غير مدعوم", "file too large": "الملف كبير جداً. أقصى حجم مسموح به هو {size}", "file renamed": "تمت إعادة التسمية بنجاح", "file saved": "تم الحفظ بنجاح", "folder added": "تمت إضافة المجلد", "folder already added": "المجلد مضاف مسبقاً!", "font size": "حجم الخط", "goto": "الذهاب إلى السطر", "icons definition": "تعريف الأيقونات", "info": "معلومات", "invalid value": "قيمة غير صالحة", "language changed": "تم تغيير اللغة بنجاح", "linting": "فحص أخطاء السينتاكس", "logout": "تسجيل الخروج", "loading": "جارٍ التحميل...", "my profile": "ملفي الشخصي", "new file": "ملف جديد", "new folder": "مجلد جديد", "no": "لا", "no editor message": "افتح أو أنشئ ملفاً أو مجلداً من القائمة", "not set": "غير محدد", "unsaved files close app": "هناك ملفات غير محفوظة. هل أنت متأكد من الخروج؟", "notice": "ملاحظة", "open file": "فتح ملف", "open files and folders": "فتح ملفات ومجلدات", "open folder": "فتح مجلد", "open recent": "الملفات الأخيرة", "ok": "حسناً", "overwrite": "استبدال", "paste": "لصق", "preview mode": "وضعية المعاينة", "read only file": "فشل حفظ الملف لأنه للقراءة فقط. حاول حفظه باسم آخر.", "reload": "إعادة تحميل", "rename": "إعادة تسمية", "replace": "استبدال", "required": "هذا الحقل مطلوب.", "run your web app": "تشغيل تطبيق الويب", "save": "حفظ", "saving": "جارٍ الحفظ...", "save as": "حفظ باسم", "save file to run": "يرجى حفظ الملف لتتمكن من تشغيله في المتصفح!", "search": "بحث", "see logs and errors": "عرض السجلات والأخطاء", "select folder": "تحديد مجلد", "settings": "الإعدادات", "settings saved": "تم حفظ الإعدادات", "show line numbers": "عرض أرقام الأسطر", "show hidden files": "عرض الملفات المخفية", "show spaces": "عرض المسافات", "soft tab": "تبويب سلس", "sort by name": "الترتيب حسب الاسم؟", "success": "نجاح", "tab size": "حجم التبويبات", "text wrap": "التفاف النص", "theme": "السمة", "unable to delete file": "عذراً، فشل حذف الملف", "unable to open file": "عذراً، فشل فتح الملف", "unable to open folder": "عذراً، فشل فتح المجلد", "unable to save file": "عذراً، فشل حفظ الملف", "unable to rename": "عذراً، فشلت إعادة التسمية", "unsaved file": "هذا الملف لم يحفظ، هل تريد الإغلاق على أي حال؟", "warning": "تحذير", "use emmet": "استخدام Emmet", "use quick tools": "استخدام الأدوات السريعة", "yes": "نعم", "encoding": "ترميز النص", "syntax highlighting": "تمييز الصيغة (Syntax Highlighting)", "read only": "للقراءة فقط", "select all": "تحديد الكل", "select branch": "حدد فرعاً (Branch)", "create new branch": "إنشاء فرع جديد", "use branch": "استخدام الفرع", "new branch": "فرع جديد", "branch": "فرع", "key bindings": "اختصارات لوحة المفاتيح", "edit": "تعديل", "reset": "إعادة ضبط", "color": "اللون", "select word": "تحديد كلمة", "quick tools": "الأدوات السريعة", "select": "اختيار", "editor font": "خط المحرر", "new project": "مشروع جديد", "format": "تنسيق", "project name": "اسم المشروع", "unsupported device": "جهازك لا يدعم السمات.", "vibrate on tap": "الاهتزاز عند اللمس", "copy command is not supported by ftp.": "أمر النسخ غير مدعوم عبر FTP.", "support title": "دعم Acode", "fullscreen": "شاشة كاملة", "animation": "الرسوم المتحركة", "backup": "نسخ احتياطي", "restore": "استعادة", "backup successful": "تم النسخ الاحتياطي بنجاح", "invalid backup file": "ملف نسخ احتياطي غير صالح", "live autocompletion": "إكمال تلقائي مباشر", "file properties": "خصائص الملف", "path": "المسار", "type": "النوع", "word count": "عدد الكلمات", "line count": "عدد الأسطر", "last modified": "آخر تعديل", "size": "الحجم", "share": "مشاركة", "show print margin": "إظهار هامش الطباعة", "login": "تسجيل الدخول", "scrollbar size": "حجم شريط التمرير", "cursor controller size": "حجم وحدة التحكم بالمؤشر", "none": "بلا", "small": "صغير", "large": "كبير", "floating button": "الزر العائم", "confirm on exit": "تأكيد عند الخروج", "show console": "إظهار وحدة التحكم", "image": "صورة", "insert file": "إدراج ملف", "insert color": "إدراج لون", "powersave mode warning": "قم بإيقاف وضع توفير الطاقة للمعاينة في متصفح خارجي.", "exit": "خروج", "custom": "مخصص", "reset warning": "هل أنت متأكد من إعادة تعيين السمة؟", "theme type": "نوع السمة", "light": "فاتح", "dark": "داكن", "file browser": "متصفح الملفات", "operation not permitted": "العملية غير مسموح بها", "no such file or directory": "لا يوجد ملف أو مجلد بهذا الاسم", "input/output error": "خطأ في الإدخال/الإخراج", "permission denied": "تم رفض الإذن", "bad address": "عنوان غير صالح", "file exists": "الملف موجود بالفعل", "not a directory": "ليس مجلداً", "is a directory": "هذا مجلد", "invalid argument": "معامل غير صالح", "too many open files in system": "عدد كبير جداً من الملفات المفتوحة في النظام", "too many open files": "عدد كبير جداً من الملفات المفتوحة", "text file busy": "الملف النصي قيد الاستخدام", "no space left on device": "لا توجد مساحة تخزين كافية على الجهاز", "read-only file system": "نظام الملفات للقراءة فقط", "file name too long": "اسم الملف طويل جداً", "too many users": "عدد كبير جداً من المستخدمين", "connection timed out": "انتهت مهلة الاتصال", "connection refused": "تم رفض الاتصال", "owner died": "توقف المالك (Owner died)", "an error occurred": "حدث خطأ ما", "add ftp": "إضافة اتصال FTP", "add sftp": "إضافة اتصال SFTP", "save file": "حفظ الملف", "save file as": "حفظ الملف باسم", "files": "الملفات", "help": "مساعدة", "file has been deleted": "تم حذف الملف {file}!", "feature not available": "هذه الميزة متاحة فقط في النسخة المدفوعة.", "deleted file": "ملف محذوف", "line height": "ارتفاع السطر", "preview info": "لتشغيل الملف النشط، انقر مطولاً على أيقونة التشغيل.", "manage all files": "امنح التطبيق إذن إدارة جميع الملفات من الإعدادات لتتمكن من التحرير بسهولة.", "close file": "إغلاق الملف", "reset connections": "إعادة تعيين الاتصالات", "check file changes": "التحقق من تغييرات الملف", "open in browser": "فتح في المتصفح", "desktop mode": "وضع سطح المكتب", "toggle console": "تبديل وحدة التحكم", "new line mode": "وضع السطر الجديد", "add a storage": "إضافة وحدة تخزين", "rate acode": "تقييم Acode", "support": "الدعم", "downloading file": "جاري تنزيل {file}...", "downloading...": "جاري التنزيل...", "folder name": "اسم المجلد", "keyboard mode": "وضع لوحة المفاتيح", "normal": "عادي", "app settings": "إعدادات التطبيق", "disable in-app-browser caching": "تعطيل التخزين المؤقت للمتصفح الداخلي", "copied to clipboard": "تم النسخ إلى الحافظة", "remember opened files": "تذكر الملفات المفتوحة", "remember opened folders": "تذكر المجلدات المفتوحة", "no suggestions": "بدون اقتراحات", "no suggestions aggressive": "بدون اقتراحات (صارم)", "install": "تثبيت", "installing": "جاري التثبيت...", "plugins": "الإضافات", "recently used": "المستخدمة مؤخراً", "update": "تحديث", "uninstall": "إلغاء التثبيت", "download acode pro": "تنزيل Acode Pro", "loading plugins": "جاري تحميل الإضافات...", "faqs": "الأسئلة الشائعة", "feedback": "ملاحظات", "header": "الرأس", "sidebar": "الشريط الجانبي", "inapp": "داخل التطبيق", "browser": "المتصفح", "diagonal scrolling": "تمرير قطري", "reverse scrolling": "تمرير عكسي", "formatter": "منسق الكود", "format on save": "تنسيق عند الحفظ", "remove ads": "إزالة الإعلانات", "fast": "سريع", "slow": "بطيء", "scroll settings": "إعدادات التمرير", "scroll speed": "سرعة التمرير", "loading...": "جاري التحميل...", "no plugins found": "لم يتم العثور على إضافات", "name": "الاسم", "username": "اسم المستخدم", "optional": "اختياري", "hostname": "اسم المضيف (Hostname)", "password": "كلمة المرور", "security type": "نوع الأمان", "connection mode": "وضع الاتصال", "port": "المنفذ", "key file": "ملف المفتاح", "select key file": "اختر ملف المفتاح", "passphrase": "عبارة المرور", "connecting...": "جاري الاتصال...", "type filename": "اكتب اسم الملف", "unable to load files": "تعذر تحميل الملفات", "preview port": "منفذ المعاينة", "find file": "البحث عن ملف", "system": "النظام", "please select a formatter": "يرجى اختيار منسق كود", "case sensitive": "حساس لحالة الأحرف", "regular expression": "تعبير نمطي (Regex)", "whole word": "كلمة بالكامل", "edit with": "تحرير بواسطة", "open with": "فتح بواسطة", "no app found to handle this file": "لم يتم العثور على تطبيق لفتح هذا الملف", "restore default settings": "استعادة الإعدادات الافتراضية", "server port": "منفذ الخادم", "preview settings": "إعدادات المعاينة", "preview settings note": "إذا اختلف منفذ المعاينة عن منفذ الخادم، فلن يقوم التطبيق بتشغيل الخادم وسيفتح بدلاً من ذلك الرابط في المتصفح. هذا مفيد عند تشغيل خادم خارجي.", "backup/restore note": "سيتم فقط نسخ الإعدادات، السمات المخصصة، الإضافات المثبتة، واختصارات المفاتيح. لن يتم نسخ اتصالات FTP/SFTP أو حالة التطبيق.", "host": "المضيف", "retry ftp/sftp when fail": "إعادة المحاولة عند فشل FTP/SFTP", "more": "المزيد", "thank you :)": "شكراً لك :)", "purchase pending": "الشراء معلق", "cancelled": "ملغي", "local": "محلي", "remote": "عن بعد", "show console toggler": "إظهار زر تبديل وحدة التحكم", "binary file": "هذا الملف يحتوي على بيانات ثنائية، هل تريد فتحه؟", "relative line numbers": "أرقام أسطر نسبية", "elastic tabstops": "علامات تبويب مرنة (Elastic)", "line based rtl switching": "تبديل اتجاه RTL لكل سطر", "hard wrap": "التفاف صارم", "spellcheck": "تدقيق إملائي", "wrap method": "طريقة الالتفاف", "use textarea for ime": "استخدام Textarea لدعم IME", "invalid plugin": "إضافة غير صالحة", "type command": "اكتب أمراً", "plugin": "إضافة", "quicktools trigger mode": "نمط تشغيل الأدوات السريعة", "print margin": "هامش الطباعة", "touch move threshold": "حساسية حركة اللمس", "info-retryremotefsafterfail": "إعادة محاولة اتصال FTP/SFTP تلقائياً عند الفشل.", "info-fullscreen": "إخفاء شريط العنوان في الشاشة الرئيسية.", "info-checkfiles": "التحقق من تغييرات الملفات عندما يكون التطبيق في الخلفية.", "info-console": "اختر وحدة تحكم JavaScript. Legacy هي الافتراضية، و Eruda هي وحدة تحكم خارجية.", "info-keyboardmode": "وضع لوحة المفاتيح لإدخال النص؛ خيار 'بدون اقتراحات' سيخفي الاقتراحات والتصحيح التلقائي.", "info-rememberfiles": "تذكر الملفات المفتوحة عند إغلاق التطبيق.", "info-rememberfolders": "تذكر المجلدات المفتوحة عند إغلاق التطبيق.", "info-floatingbutton": "إظهار أو إخفاء زر الأدوات السريعة العائم.", "info-openfilelistpos": "مكان عرض قائمة الملفات النشطة.", "info-touchmovethreshold": "إذا كانت حساسية اللمس عالية جداً، زد هذه القيمة لتجنب التحريك العرضي.", "info-scroll-settings": "تحتوي هذه الإعدادات على خيارات التمرير بما في ذلك التفاف النص.", "info-animation": "إذا شعرت ببطء في التطبيق، قم بتعطيل الرسوم المتحركة.", "info-quicktoolstriggermode": "غير هذه القيمة إذا كانت أزرار الأدوات السريعة لا تعمل بشكل صحيح.", "info-checkForAppUpdates": "التحقق من تحديثات التطبيق تلقائياً.", "info-quickTools": "إظهار أو إخفاء الأدوات السريعة.", "info-showHiddenFiles": "عرض الملفات والمجلدات المخفية (التي تبدأ بنقطة .).", "info-all_file_access": "تمكين الوصول إلى /sdcard و /storage في الطرفية.", "info-fontSize": "حجم الخط المستخدم لعرض النص.", "info-fontFamily": "عائلة الخط المستخدمة لعرض النص.", "info-theme": "سمة ألوان الطرفية.", "info-cursorStyle": "شكل المؤشر عند التركيز على الطرفية.", "info-cursorInactiveStyle": "شكل المؤشر عندما لا تكون الطرفية في حالة تركيز.", "info-fontWeight": "سماكة الخط المستخدم للنصوص غير العريضة.", "info-cursorBlink": "ما إذا كان المؤشر يومض أم لا.", "info-scrollback": "عدد الأسطر التي يتم الاحتفاظ بها في ذاكرة التمرير للخلف بالطرفية.", "info-tabStopWidth": "حجم مسافات التبويب (Tab) في الطرفية.", "info-letterSpacing": "المسافة بين الأحرف بالبكسل.", "info-imageSupport": "ما إذا كانت الصور مدعومة في الطرفية.", "info-fontLigatures": "ما إذا كانت ربطات الخط (Ligatures) مفعلة في الطرفية.", "info-confirmTabClose": "طلب تأكيد قبل إغلاق تبويبات الطرفية.", "info-backup": "إنشاء نسخة احتياطية لتثبيت الطرفية.", "info-restore": "استعادة نسخة احتياطية لتثبيت الطرفية.", "info-uninstall": "إلغاء تثبيت الطرفية.", "owned": "مملوك", "api_error": "خادم API معطل، يرجى المحاولة لاحقاً.", "installed": "مثبت", "all": "الكل", "medium": "متوسط", "refund": "استرداد الأموال", "product not available": "المنتج غير متوفر", "no-product-info": "هذا المنتج غير متوفر في بلدك حالياً، يرجى المحاولة لاحقاً.", "close": "إغلاق", "explore": "استكشاف", "key bindings updated": "تم تحديث اختصارات المفاتيح", "search in files": "البحث في الملفات", "exclude files": "استثناء الملفات", "include files": "تضمين الملفات", "search result": "وجد {matches} نتيجة في {files} ملفاً.", "invalid regex": "تعبير نمطي غير صالح: {message}.", "bottom": "أسفل", "save all": "حفظ الكل", "close all": "إغلاق الكل", "unsaved files warning": "بعض الملفات غير محفوظة. اضغط 'موافق' للاختيار أو 'إلغاء' للعودة.", "save all warning": "هل أنت متأكد من حفظ جميع الملفات والإغلاق؟ لا يمكن التراجع عن هذا الإجراء.", "save all changes warning": "هل أنت متأكد من حفظ جميع التغييرات؟", "close all warning": "هل أنت متأكد من إغلاق جميع الملفات؟ ستفقد أي تغييرات غير محفوظة.", "refresh": "تحديث", "shortcut buttons": "أزرار الاختصار", "no result": "لا توجد نتائج", "searching...": "جاري البحث...", "quicktools:ctrl-key": "مفتاح Control/Command", "quicktools:tab-key": "مفتاح Tab", "quicktools:shift-key": "مفتاح Shift", "quicktools:undo": "تراجع", "quicktools:redo": "إعادة", "quicktools:search": "البحث في الملف", "quicktools:save": "حفظ الملف", "quicktools:esc-key": "مفتاح Escape", "quicktools:curlybracket": "إدراج قوس متعرج { }", "quicktools:squarebracket": "إدراج قوس مربع [ ]", "quicktools:parentheses": "إدراج قوسين ( )", "quicktools:anglebracket": "إدراج قوس زاوية < >", "quicktools:left-arrow-key": "سهم يسار", "quicktools:right-arrow-key": "سهم يمين", "quicktools:up-arrow-key": "سهم لأعلى", "quicktools:down-arrow-key": "سهم لأسفل", "quicktools:moveline-up": "نقل السطر للأعلى", "quicktools:moveline-down": "نقل السطر للأسفل", "quicktools:copyline-up": "نسخ السطر للأعلى", "quicktools:copyline-down": "نسخ السطر للأسفل", "quicktools:semicolon": "إدراج فاصلة منقوطة ;", "quicktools:quotation": "إدراج علامة اقتباس \"", "quicktools:and": "إدراج رمز &", "quicktools:bar": "إدراج رمز الشريطة |", "quicktools:equal": "إدراج علامة =", "quicktools:slash": "إدراج شرطة مائلة /", "quicktools:exclamation": "إدراج علامة تعجب !", "quicktools:alt-key": "مفتاح Alt", "quicktools:meta-key": "مفتاح Windows/Meta", "info-quicktoolssettings": "خصص أزرار الاختصارات ومفاتيح لوحة التحكم في شريط الأدوات السريع أسفل المحرر.", "info-excludefolders": "استخدم النمط **/node_modules/** لتجاهل ملفات مجلد node_modules في القائمة والبحث.", "missed files": "تم فحص {count} ملفاً بعد بدء البحث ولن يتم تضمينها في النتائج.", "remove": "إزالة", "quicktools:command-palette": "لوحة الأوامر", "default file encoding": "ترميز الملف الافتراضي", "remove entry": "هل أنت متأكد من إزالة '{name}' من المسارات المحفوظة؟ لن يتم حذف المجلد الفعلي.", "delete entry": "تأكيد حذف '{name}'. لا يمكن التراجع عن هذا. هل ترغب في المتابعة؟", "change encoding": "إعادة فتح '{file}' بترميز '{encoding}'؟ ستفقد أي تغييرات غير محفوظة.", "reopen file": "هل أنت متأكد من إعادة فتح '{file}'؟ ستفقد التغييرات غير المحفوظة.", "plugin min version": "{name} متاح فقط في Acode إصدار {v-code} وما فوق. اضغط للتحديث.", "color preview": "معاينة اللون", "confirm": "تأكيد", "list files": "هل تريد سرد جميع الملفات في {name}؟ العدد الكبير قد يؤدي لتعطل التطبيق.", "problems": "المشكلات", "show side buttons": "إظهار الأزرار الجانبية", "bug_report": "إرسال تقرير عن خطأ", "verified publisher": "ناشر موثق", "most_downloaded": "الأكثر تنزيلاً", "newly_added": "أضيف حديثاً", "top_rated": "الأعلى تقييماً", "rename not supported": "إعادة التسمية غير مدعومة في مجلد Termux", "compress": "ضغط", "copy uri": "نسخ الرابط (URI)", "delete entries": "هل أنت متأكد من حذف {count} من العناصر؟", "deleting items": "جاري حذف {count} عنصراً...", "import project zip": "استيراد مشروع (zip)", "changelog": "سجل التغييرات", "notifications": "الإشعارات", "no_unread_notifications": "لا توجد إشعارات غير مقروءة", "should_use_current_file_for_preview": "استخدام الملف الحالي للمعاينة بدلاً من index.html", "fade fold widgets": "تلاشي عناصر الطي", "quicktools:home-key": "مفتاح Home", "quicktools:end-key": "مفتاح End", "quicktools:pageup-key": "مفتاح PageUp", "quicktools:pagedown-key": "مفتاح PageDown", "quicktools:delete-key": "مفتاح Delete", "quicktools:tilde": "إدراج رمز ~", "quicktools:backtick": "إدراج علامة `", "quicktools:hash": "إدراج رمز #", "quicktools:dollar": "إدراج رمز $", "quicktools:modulo": "إدراج رمز %", "quicktools:caret": "إدراج رمز ^", "plugin_enabled": "الإضافة مفعلة", "plugin_disabled": "الإضافة معطلة", "enable_plugin": "تفعيل هذه الإضافة", "disable_plugin": "تعطيل هذه الإضافة", "open_source": "مفتوح المصدر", "terminal settings": "إعدادات الطرفية", "font ligatures": "ربطات الخط (Ligatures)", "letter spacing": "تباعد الأحرف", "terminal:tab stop width": "عرض مسافة Tab", "terminal:scrollback": "أسطر ذاكرة التمرير", "terminal:cursor blink": "وميض المؤشر", "terminal:font weight": "سماكة الخط", "terminal:cursor inactive style": "شكل المؤشر غير النشط", "terminal:cursor style": "شكل المؤشر", "terminal:font family": "عائلة الخطوط", "terminal:convert eol": "تحويل EOL", "terminal:confirm tab close": "تأكيد إغلاق تبويب الطرفية", "terminal:image support": "دعم الصور", "terminal": "الطرفية", "allFileAccess": "الوصول لجميع الملفات", "fonts": "الخطوط", "sponsor": "دعم التطبيق", "downloads": "تنزيلات", "reviews": "مراجعات", "overview": "نظرة عامة", "contributors": "المساهمون", "quicktools:hyphen": "إدراج شرطة -", "check for app updates": "التحقق من تحديثات التطبيق", "prompt update check consent message": "يمكن لـ Acode التحقق من التحديثات الجديدة عند الاتصال بالإنترنت. هل تريد التفعيل؟", "keywords": "كلمات مفتاحية", "author": "المؤلف", "filtered by": "تصفية حسب", "clean install state": "حالة تثبيت نظيف", "backup created": "تم إنشاء النسخة الاحتياطية", "restore completed": "اكتملت الاستعادة", "restore will include": "سيتم استعادة التالي", "restore warning": "لا يمكن التراجع عن هذا الإجراء. هل تريد المتابعة؟", "reload to apply": "إعادة تحميل لتطبيق التغييرات؟", "reload app": "إعادة تحميل التطبيق", "preparing backup": "جاري تحضير النسخة الاحتياطية", "collecting settings": "تجميع الإعدادات", "collecting key bindings": "تجميع اختصارات المفاتيح", "collecting plugins": "تجميع معلومات الإضافات", "creating backup": "جاري إنشاء ملف النسخة الاحتياطية", "validating backup": "التحقق من صحة النسخة الاحتياطية", "restoring key bindings": "استعادة اختصارات المفاتيح", "restoring plugins": "استعادة الإضافات", "restoring settings": "استعادة الإعدادات", "legacy backup warning": "هذا تنسيق قديم للنسخ الاحتياطي، بعض الميزات قد تكون محدودة.", "checksum mismatch": "عدم تطابق المجموع التدقيقي (Checksum) - قد يكون الملف معدلاً أو تالفاً.", "plugin not found": "الإضافة غير موجودة في السجل", "paid plugin skipped": "إضافة مدفوعة - لم يتم العثور على عملية شراء", "source not found": "ملف المصدر لم يعد موجوداً", "restored": "تمت الاستعادة", "skipped": "تم التخطي", "backup not valid object": "ملف النسخة الاحتياطية ليس كائناً صالحاً", "backup no data": "لا توجد بيانات لاستعادتها في ملف النسخة الاحتياطية", "backup legacy warning": "هذا تنسيق قديم (v1). بعض الميزات قد تكون محدودة.", "backup missing metadata": "بيانات وصفية مفقودة - بعض المعلومات قد لا تتوفر.", "backup checksum mismatch": "عدم تطابق في المجموع التدقيقي. تابع بحذر.", "backup checksum verify failed": "تعذر التحقق من المجموع التدقيقي", "backup invalid settings": "تنسيق إعدادات غير صالح", "backup invalid keybindings": "تنسيق اختصارات مفاتيح غير صالح", "backup invalid plugins": "تنسيق إضافات مثبتة غير صالح", "issues found": "تم العثور على مشكلات", "error details": "تفاصيل الخطأ", "active tools": "الأدوات النشطة", "available tools": "الأدوات المتاحة", "recent": "الملفات الأخيرة", "command palette": "فتح لوحة الأوامر", "change theme": "تغيير السمة", "documentation": "الوثائق", "open in terminal": "فتح في الطرفية", "developer mode": "وضع المطور", "info-developermode": "تمكين أدوات المطور (Eruda) لتصحيح الإضافات ومعاينة حالة التطبيق. سيتم تفعيل المستكشف عند بدء التشغيل.", "developer mode enabled": "تم تفعيل وضع المطور. استخدم لوحة الأوامر لتبديل المستكشف (Ctrl+Shift+I).", "developer mode disabled": "تم تعطيل وضع المطور", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/be-by.json ================================================ { "lang": "Беларуская", "about": "Пра праграму", "active files": "Адкрытыя файлы", "alert": "Абвестка", "app theme": "Тэма праграмы", "autocorrect": "Уключыць аўтавыпраўленне?", "autosave": "Аўтазахаванне", "cancel": "Скасаваць", "change language": "Змяніць мову", "choose color": "Абраць колер", "clear": "ачысціць", "close app": "Закрыць праграму?", "commit message": "Зафіксаваць паведамленне", "console": "Кансоль", "conflict error": "Канфлікт! Калі ласка, пачакайце, перш чым зафіксаваць.", "copy": "Скапіяваць", "create folder error": "Выбачайце, не ўдалося стварыць новы каталог", "cut": "Выразаць", "delete": "Выдаліць", "dependencies": "Залежнасці", "delay": "Час у мілісекундах", "editor settings": "Налады рэдактара", "editor theme": "Тэма рэдактара", "enter file name": "Увядзіце назву файла", "enter folder name": "Увядзіце назву каталога", "empty folder message": "Пусты каталог", "enter line number": "Увядзіце нумар радка", "error": "Памылка", "failed": "Не ўдалося", "file already exists": "Файл ужо існуе", "file already exists force": "Файл ужо існуе. Перазапісаць?", "file changed": " быў зменены, перазагрузіць файл?", "file deleted": "Файл выдалены", "file is not supported": "Файл не падтрымліваецца", "file not supported": "Файлы гэтага тыпу не падтрымліваюцца.", "file too large": "Файл занадта вялікі. Максімальны памер {size}", "file renamed": "назва файла змененая", "file saved": "файл захаваны", "folder added": "каталог дададзены", "folder already added": "каталог ужо дададзены", "font size": "Памер шрыфту", "goto": "Перайсці да радка", "icons definition": "Азначэнне значкоў", "info": "інфармацыя", "invalid value": "Хібнае значэнне", "language changed": "мова паспяхова зменена", "linting": "Правяраць на сінтаксічныя памылкі", "logout": "Выйсці", "loading": "Загрузка", "my profile": "Мой профіль", "new file": "Новы файл", "new folder": "Новы каталог", "no": "Не", "no editor message": "Адкрыйце альбо стварыце новы файл і каталог з меню", "not set": "Не вызначана", "unsaved files close app": "Ёсць незахаваныя файлы. Закрыць праграму?", "notice": "Папярэджанне", "open file": "Адкрыць файл", "open files and folders": "Адкрытыя файлы і каталогі", "open folder": "Адкрыць каталог", "open recent": "Адкрыць нядаўнія", "ok": "добра", "overwrite": "Перазапісаць", "paste": "Уставіць", "preview mode": "Рэжым папярэдняга прагляду", "read only file": "Файл адкрыты толькі для чытання. Паспрабуйце \"Захаваць як\"", "reload": "Перазагрузіць", "rename": "Змяніць назву", "replace": "Замяніць", "required": "Гэтае поле абавязковае", "run your web app": "Запусціць сеціўную праграму", "save": "Захаваць", "saving": "Захаванне", "save as": "Захаваць як", "save file to run": "Захавайце файл для запуску ў браўзеры", "search": "Пошук", "see logs and errors": "Праглядзець журналы і памылкі", "select folder": "Абраць каталог", "settings": "Налады", "settings saved": "Налады захаваныя", "show line numbers": "Паказваць нумары радкоў", "show hidden files": "Паказваць схаваныя файлы", "show spaces": "Паказваць прагалы", "soft tab": "Мяккая табуляцыя", "sort by name": "Сартаваць па назве", "success": "Паспяхова", "tab size": "Памер табуляцыя", "text wrap": "Перанос тэксту", "theme": "Тэма", "unable to delete file": "немагчыма выдаліць файл", "unable to open file": "Выбачайце, файл немагчыма адкрыць", "unable to open folder": "Выбачайце, каталог немагчыма адкрыць", "unable to save file": "Выбачайце, файл немагчыма захаваць", "unable to rename": "Выбачайце, немагчыма змяніць назву", "unsaved file": "Файл не захаваны, усё адно закрыць?", "warning": "Папярэджанне", "use emmet": "Выкарыстоўваць emmet", "use quick tools": "Выкарыстоўваць хуткія інструменты", "yes": "Так", "encoding": "Кадаванне", "syntax highlighting": "Падсвятленне сінтаксісу", "read only": "Толькі чытанне", "select all": "Абраць усё", "select branch": "Абраць галіну", "create new branch": "Стварыць новую галіну", "use branch": "Выкарыстоўваць галіну", "new branch": "Новая галіна", "branch": "Галіна", "key bindings": "Прывязванне клавіш", "edit": "Рэдагаваць", "reset": "Скінуць", "color": "Колер", "select word": "Абраць слова", "quick tools": "Хуткія інструменты", "select": "Абраць", "editor font": "Шрыфт рэдактара", "new project": "Новы праект", "format": "Фарматаванне", "project name": "Назва праекта", "unsupported device": "Ваша прылада не падтрымлівае тэмы.", "vibrate on tap": "Вібрацыя пры націсканні", "copy command is not supported by ftp.": "Капіяванне не падтрымліваецца для FTP.", "support title": "Падтрымка Acode", "fullscreen": "Поўнаэкранны рэжым", "animation": "Анімацыя", "backup": "Рэзервовае капіяванне", "restore": "Аднаўленне", "backup successful": "Рэзервовая копія паспяхова створана", "invalid backup file": "Не ўдалося стварыць рэзервовую копію", "add path": "Дадаць шлях", "live autocompletion": "Імгненнае аўтазапаўненне", "file properties": "Уласцівасці файла", "path": "Шлях", "type": "Тып", "word count": "Колькасць слоў", "line count": "Колькасць радкоў", "last modified": "Апошняя змена", "size": "Памер", "share": "Абагуліць", "show print margin": "Паказаць поле друку", "login": "Лагін", "scrollbar size": "Памер паласы пракручвання", "cursor controller size": "Памер курсора", "none": "Няма", "small": "Маленькі", "large": "Вялікі", "floating button": "Выплыўная панэль кнопак", "confirm on exit": "Пацвярджэнне выхаду", "show console": "Паказваць кансоль", "image": "Выява", "insert file": "Уставіць файл", "insert color": "Уставіць колер", "powersave mode warning": "Для папярэдняга прагляду ў вонкавым браўзеры адключыце рэжым энергазберажэння.", "exit": "Выйсці", "custom": "Адвольна", "reset warning": "Сапраўды хочаце скінуць тэму?", "theme type": "Тып тэмы", "light": "Светлая", "dark": "Цёмная", "file browser": "Агляд файлаў", "operation not permitted": "Аперацыя забароненая", "no such file or directory": "Такі файл альбо каталог не існуе", "input/output error": "Памылка ўводу або вываду", "permission denied": "Адмоўлена ў доступе", "bad address": "Няправільны адрас", "file exists": "Файл існуе", "not a directory": "Гэта не каталог", "is a directory": "Гэта каталог", "invalid argument": "Хібны аргумент", "too many open files in system": "Занадта шмат адкрытых файлаў у сістэме", "too many open files": "Занадта шмат адкрытых файлаў", "text file busy": "Тэкставы файл заняты", "no space left on device": "На прыладзе скончылася вольнае месца", "read-only file system": "Файлавая сістэма даступная толькі для чытання", "file name too long": "Назва файла занадта доўгая", "too many users": "Занадта шмат карыстальнікаў", "connection timed out": "Час чакання злучэння скончыўся", "connection refused": "Злучэнне адкінута", "owner died": "Уладальнік знік", "an error occurred": "Адбылася памылка", "add ftp": "Дадаць FTP", "add sftp": "Дадаць SFTP", "save file": "Захаваць файл", "save file as": "Захаваць файл як", "files": "Файлы", "help": "Даведка", "file has been deleted": "{file} выдалены!", "feature not available": "Гэтая функцыя даступная толькі для ў платнай версіі праграмы.", "deleted file": "Выдалены файл", "line height": "Вышыня радка", "preview info": "Калі вы хочаце запусціць актыўны файл, націсніце і ўтрымлівайце значок прайгравання.", "manage all files": "Дазвольце Acode кіраваць усімі файламі ў наладах, каб праграма мела магчымасць рэдагаваць файлы.", "close file": "Закрыць файл", "reset connections": "Скінуць злучэнні", "check file changes": "Правяраць файлы на наяўнасць зменаў", "open in browser": "Адкрыць у браўзеры", "desktop mode": "Настольны рэжым", "toggle console": "Пераключэнне кансолі", "new line mode": "Рэжым новага радка", "add a storage": "Дадаць сховішча", "rate acode": "Ацаніць Acode", "support": "Падтрымка", "downloading file": "Спампоўванне {file}", "downloading...": "Спампоўванне...", "folder name": "Назва каталога", "keyboard mode": "Рэжым клавіятуры", "normal": "Звычайны", "app settings": "Налады праграмы", "disable in-app-browser caching": "Адключыць кэшаванне ў праграмным сродку агляду", "Should use Current File For preview instead of default (index.html)": "Для папярэдняга прагляду варта выкарыстоўваць бягучы файл замест прадвызначанага (index.html)", "copied to clipboard": "Скапіявана ў буфер абмену", "remember opened files": "Запамінаць адкрытыя файлы", "remember opened folders": "Запамінаць адкрытыя каталогі", "no suggestions": "Няма прапаноў", "no suggestions aggressive": "Без настойлівых прапаноў", "install": "Усталяваць", "installing": "Усталяванне...", "plugins": "Убудовы", "recently used": "Нядаўна выкарыстаныя", "update": "Абнавіць", "uninstall": "Выдаліць", "download acode pro": "Спампаваць Acode pro", "loading plugins": "Загрузка ўбудоў", "faqs": "Частыя пытанні", "feedback": "Зваротная сувязь", "header": "Загаловак", "sidebar": "Бакавая панэль", "inapp": "Inapp", "browser": "Браўзер", "diagonal scrolling": "Дыяганальнае пракручванне", "reverse scrolling": "Адваротнае пракручванне", "formatter": "Сродкі фарматавання", "format on save": "Фарматаваць пры захаванні", "remove ads": "Выдаліць рэкламу", "fast": "Хутка", "slow": "Павольна", "scroll settings": "Налады пракручвання", "scroll speed": "Хуткасць пракручвання", "loading...": "Загрузка...", "no plugins found": "Не знойдзена ўбудоў", "name": "Назва", "username": "Імя карыстальніка", "optional": "неабавязкова", "hostname": "Назва хоста", "password": "Пароль", "security type": "Тып бяспекі", "connection mode": "Рэжым злучэння", "port": "Порт", "key file": "Файл ключа", "select key file": "Абраць файл ключа", "passphrase": "Парольная фраза", "connecting...": "Злучэнне...", "type filename": "Увядзіце назву ключа", "unable to load files": "Немагчыма загрузіць файлы", "preview port": "Порт папярэдняга прагляду", "find file": "Пошук файлаў", "system": "Сістэмны", "please select a formatter": "Абярыце сродак фарматавання", "case sensitive": "Зважаць на рэгістр", "regular expression": "Рэгулярны выраз", "whole word": "Цэлае слова", "edit with": "Рэдагаваць у", "open with": "Адкрыць у", "no app found to handle this file": "Няма праграмы для апрацоўвання гэтага файла", "restore default settings": "Аднавіць прадвызначаныя налады", "server port": "Порт сервера", "preview settings": "Налады папярэдняга прагляду", "preview settings note": "Калі порт папярэдняга прагляду і порт сервера адрозніваюцца, праграма не запусціць сервер, а адкрые ў браўзеры або ўбудаваным браўзеры https://:. Гэта карысна, калі вы працуеце з серверам у іншым месцы.", "backup/restore note": "Будуць стварацца рэзервовыя копіі вашых налад, тэмаў, усталяваных убудоў і прывязаных клавіш. Рэзервовая копія вашага FTP/SFTP або стану праграмы стварацца не будзе.", "host": "Хост", "retry ftp/sftp when fail": "Пры няўдачы паўтараць спробу падлучэння да ftp/sftp", "more": "Яшчэ", "thank you :)": "Дзякуй :)", "purchase pending": "чаканне куплі", "cancelled": "скасавана", "local": "Лакальны", "remote": "Адлеглы", "show console toggler": "Паказваць элемент пераключэння кансолі", "binary file": "Гэты файл змяшчае двайковыя даныя, хочаце адкрыць яго?", "relative line numbers": "Адносныя нумары радкоў", "elastic tabstops": "Эластычныя табулятары", "line based rtl switching": "Лінейнае пераключэнне RTL", "hard wrap": "Строгі перанос", "spellcheck": "Праверка правапісу", "wrap method": "Метад пераносу", "use textarea for ime": "Выкарыстоўваць тэкставае поле для IME", "invalid plugin": "Хібная ўбудова", "type command": "Увядзіце каманду", "plugin": "Убудова", "quicktools trigger mode": "Рэжым запуску хуткіх інструментаў", "print margin": "Поле друку", "touch move threshold": "Парог адчувальнасці сэнсара", "info-retryremotefsafterfail": "Пры няўдачы паўтараць спробу падлучэння да FTP/SFTP.", "info-fullscreen": "Схаваць радок загалоўка на галоўным экране.", "info-checkfiles": "Правяранне файла на наяўнасць зменаў, калі праграма працуе ў фонавым рэжыме.", "info-console": "Абярыце кансоль JavaScript. Legacy - прадвызначаная кансоль, eruda - старонняя кансоль.", "info-keyboardmode": "Рэжым клавіятуры для ўводу тэксту. \"Без прапаноў\" - не будзе прапаноў і аўтаматычнага выпраўлення. Калі параметр не працуе, паспрабуйце змяніць значэнне на \"Без настойлівых прапаноў\".", "info-rememberfiles": "Запамінаць адкрытыя файлы, калі праграма закрытая.", "info-rememberfolders": "Запамінаць адкрытыя каталогі, калі праграма закрытая.", "info-floatingbutton": "Паказаць або схаваць выплыўную кнопку хуткіх інструментаў.", "info-openfilelistpos": "Размяшчэнне спіса актыўных файлаў.", "info-touchmovethreshold": "Калі адчувальнасць сэнсара вашай прылады занадта высокая, вы можаце павялічыць гэтае значэнне, каб прадухіліць выпадковыя рухі.", "info-scroll-settings": "Гэтыя налады змяшчаюць налады пракручвання, уключаючы перанос тэксту.", "info-animation": "Калі ў праграме ёсць затрымліванне, адключыце анімацыю.", "info-quicktoolstriggermode": "Калі кнопка ў хуткіх інструментах не працуе, паспрабуйце змяніць гэтае значэнне.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Ва ўласнасці", "api_error": "Сервер API не працуе, паспрабуйце праз некаторы час.", "installed": "Усталявана", "all": "Усе", "medium": "Сярэдняя", "refund": "Вяртанне грошай", "product not available": "Прадукт недаступны", "no-product-info": "Гэты прадукт зараз недаступны ў вашай краіне, паўтарыце спробу пазней.", "close": "Закрыць", "explore": "Агляд", "key bindings updated": "Прывязванне клавіш абноўлена", "search in files": "Пошук у файлах", "exclude files": "Выключаныя файлы", "include files": "Уключаныя файлы", "search result": "{matches} вынік(аў) у {files} файле(ах).", "invalid regex": "Хібны рэгулярны выраз: {message}.", "bottom": "Уніз", "save all": "Захаваць усё", "close all": "Закрыць усе", "unsaved files warning": "Некаторыя файлы не былі захаваныя. Націсніце \"Добра\" і абярыце, што рабіць, або націсніце \"Скасаваць\", каб вярнуцца.", "save all warning": "Сапраўды хочаце захаваць усе файлы і закрыць? Гэтае дзеянне нельга скасаваць.", "save all changes warning": "Сапраўды хочаце захаваць усе файлы?", "close all warning": "Сапраўды хочаце закрыць усе файлы? Гэтае дзеянне нельга скасаваць, усе файлы страцяцца.", "refresh": "Абнавіць", "shortcut buttons": "Спалучэнні клавіш для кнопак", "no result": "Няма вынікаў", "searching...": "Пошук...", "quicktools:ctrl-key": "Control (Command)", "quicktools:tab-key": "Tab", "quicktools:shift-key": "Shift", "quicktools:undo": "Адрабіць", "quicktools:redo": "Паўтарыць", "quicktools:search": "Пошук у файле", "quicktools:save": "Захаваць файл", "quicktools:esc-key": "Escape", "quicktools:curlybracket": "Уставіць фігурную дужку", "quicktools:squarebracket": "Уставіць квадратную дужку", "quicktools:parentheses": "Уставіць круглую дужку", "quicktools:anglebracket": "Уставіць вуглавую дужку", "quicktools:left-arrow-key": "Стрэлка \"Улева\"", "quicktools:right-arrow-key": "Стрэлка \"Управа\"", "quicktools:up-arrow-key": "Стрэлка \"Уверх\"", "quicktools:down-arrow-key": "Стрэлка \"Уніз\"", "quicktools:moveline-up": "Перамясціць радок вышэй", "quicktools:moveline-down": "Перамясціць радок ніжэй", "quicktools:copyline-up": "Скапіяваць радок вышэй", "quicktools:copyline-down": "Скапіяваць радок ніжэй", "quicktools:semicolon": "Уставіць кропку з коскай", "quicktools:quotation": "Уставіць цытату", "quicktools:and": "Уставіць &", "quicktools:bar": "Уставіць вертыкальную рыску", "quicktools:equal": "Уставіць знак роўна", "quicktools:slash": "Уставіць касую рыску", "quicktools:exclamation": "Уставіць клічнік", "quicktools:alt-key": "Alt", "quicktools:meta-key": "Windows (Meta)", "info-quicktoolssettings": "Наладзьце спалучэнні клавішы і клавішы клавіятуры ў кантэйнеры хуткіх інструментаў пад рэдактарам, каб павысіць зручнасць працы.", "info-excludefolders": "Выкарыстоўвайце шаблон **/node_modules/**, каб ігнараваць усе файлы з каталога node_modules. Гэта выключыць файлы са спіса і пошуку файлаў.", "missed files": "Ад пачатку пошуку апрацавана {count} файлаў. Яны не будуць уключаны ў пошук.", "remove": "Выдаліць", "quicktools:command-palette": "Палітра каманд", "default file encoding": "Прадвызначанае кадаванне файлаў", "remove entry": "Сапраўды хочаце выдаліць \"{name}\" з захаваных шляхоў? Звярніце ўвагу, што сам шлях не выдаліцца.", "delete entry": "Пацвярджэнне выдалення: \"{name}\". Гэтае дзеянне нельга скасаваць. Працягнуць?", "change encoding": "Паўторна адкрыць \"{file}\" з кадаваннем \"{encoding}\"? Усе незахаваныя змены страцяцца. Хочаце працягнуць паўторнае адкрыццё?", "reopen file": "Сапраўды хочаце паўторна адкрыць \"{file}\"? Усе незахаваныя змены страцяцца.", "plugin min version": "{name} даступна толькі ў Acode - {v-code} і вышэй. Націсніце тут, каб абнавіць.", "color preview": "Папярэдні прагляд колеру", "confirm": "Пацвердзіць", "list files": "Пералічыць усе файлы ў {name}? Калі іх будзе занадта шмат, гэта можа прывесці да аварыйнага завяршэння праграмы.", "problems": "Праблемы", "show side buttons": "Паказваць бакавыя кнопкі", "bug_report": "Паведаміць пра хібу", "verified publisher": "Правераная асоба", "most_downloaded": "Найбольш спампоўваліся", "newly_added": "Нядаўна дададзеныя", "top_rated": "Найвышэйшы рэйтынг", "rename not supported": "Змена назвы ў каталозе termux не падтрымліваецца", "compress": "Запакаваць", "copy uri": "Скапіяваць URl", "delete entries": "Сапраўды хочаце выдаліць {count} элемент(аў)?", "deleting items": "Выдаленне {count} элемента(аў)...", "import project zip": "Імпартаваць праект(zip)", "changelog": "Журнал змен", "notifications": "Апавяшчэнні", "no_unread_notifications": "Няма непрачытаных апавяшчэнняў", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Спонсар", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/bn-bd.json ================================================ { "lang": "বাংলা", "about": "সম্পর্কে", "active files": "সক্রিয় ফাইল", "alert": "সতর্কীকরণ", "app theme": "অ্যাপ থীম", "autocorrect": " অটো কারেক্ট সক্রীয় করুন?", "autosave": "অটো সংরক্ষণ", "cancel": "বাতিল করুন", "change language": "ভাষা বদলান", "choose color": "কালার পছন্দ করুন", "clear": "মুছে ফেলুন", "close app": "অ্যাপটি বন্ধ করুন?", "commit message": "কমিট মেসেজ", "console": "কনসোল", "conflict error": "কনফ্লিক্ট। আরেকটি কমিট করার আগে অপেক্ষা করুন। ", "copy": " কপি", "create folder error": "দুঃখিত, ফোল্ডার তৈরী করতে অসমর্থ", "cut": "কাট", "delete": "ডিলেট", "dependencies": "নির্ভরতা", "delay": "মিলিসেকেন্ডে সময়", "editor settings": "সম্পাদক সেটিংস", "editor theme": "সম্পাদক থীম", "enter file name": "ফাইল এর নাম লিখুন", "enter folder name": "ফোল্ডার এর নাম লিখুন", "empty folder message": "ফাঁকা ফোল্ডার এর বার্তা", "enter line number": "লাইন নম্বর লিখুন", "error": "ত্রুটি", "failed": "ব্যার্থ", "file already exists": "ফাইলটি বিদ্যমান", "file already exists force": "ফাইলটি বিদ্যমান. ওভার রাইট?", "file changed": "ফাইলটি পরিবর্তিত হয়েছে, আবার লোড করুন?", "file deleted": "ফাইল ডিলেটেড ", "file is not supported": "ফাইল অসমর্থিত", "file not supported": "এই ধরনের ফাইল অসমর্থিত", "file too large": "ফাইলটি তুলনামূকভাবে বড়। সর্বোচ্চ অনুমোদিত সাইজ হচ্ছে {size}", "file renamed": "ফাইল এর নাম পরিবর্তিত হয়েছে", "file saved": "ফাইল সংরক্ষিত", "folder added": "ফোল্ডার যোগ হয়েছে", "folder already added": "ফোল্ডারটি আগেই সংযোজিত", "font size": "ফন্টের আকার", "goto": "লাইনে যান", "icons definition": "প্রতীক বিবরন", "info": "তথ্য", "invalid value": "অকার্যকর মান", "language changed": "সফলভাবে ভাষা পরিবর্তন হয়েছে", "linting": "সিনট্যাক্স চেক করুন", "logout": "লগ আউট", "loading": "লোড হচ্ছে", "my profile": "আমার প্রোফাইল", "new file": "নতুন ফাইল", "new folder": "নতুন ফোল্ডার", "no": "না", "no editor message": "মেনু থেকে নতুন ফাইল বা ফোল্ডার খুলুন বা তৈরি করুন", "not set": "নির্দিষ্ট করা নেই", "unsaved files close app": "অসংরক্ষিত ফাইল বিদ্যমান। অ্যাপ বন্ধ করবেন?", "notice": "বিজ্ঞপ্তি", "open file": "ফাইল খুলুন", "open files and folders": "ফাইল এবং ফোল্ডার খুলুন", "open folder": "ফোল্ডার খুলুন", "open recent": "সাম্প্রতিক ফাইল", "ok": "ঠিক আছে", "overwrite": "ওভাররাইট", "paste": "পেস্ট", "preview mode": "প্রদর্শন মোড", "read only file": "রিড অনলি ফাইল সংরক্ষণে অসমর্থ। অনুগ্রপূর্বক সেইভ অ্যাস করুন", "reload": "পুনরায় লোড করুন", "rename": "পুণ: নামকরন", "replace": "প্রতিস্থাপন", "required": "এই ফিল্ডটি প্রয়োজনীয়", "run your web app": "আপনার ওয়েব অ্যাপ রান করুন", "save": "সংরক্ষণ করুন", "saving": "সংরক্ষিত হচ্ছে", "save as": "সংরক্ষণের ধরন", "save file to run": "অনুগ্রহ পূর্বক ব্রাউজারে রান করানোর আগে ফাইল টি সংরক্ষণ করুন", "search": "খুঁজুন", "see logs and errors": "logs এবং ভুল গুলো দেখুন", "select folder": "ফোল্ডার নির্বাচন করুন", "settings": "সেটিংস", "settings saved": "সেটিংস সংরক্ষিত হয়েছে", "show line numbers": "লাইন নাম্বার দেখান", "show hidden files": "লুকায়িত ফাইলগুলো দেখুন", "show spaces": "ফাকাগুলি দেখুন", "soft tab": "Soft tab", "sort by name": "নাম অনুসারে সাজান", "success": "সফল", "tab size": "ট্যাব আকার", "text wrap": "টেক্সট মোড়ান /wrap", "theme": "থীম", "unable to delete file": "ডিলেট করতে অসমর্থ", "unable to open file": "দুঃখিত, ফাইলটি খুলতে ব্যার্থ", "unable to open folder": "দুঃখিত, ফোল্ডার খুলতে ব্যার্থ", "unable to save file": "দুঃখিত, ফাইলটি সংরক্ষণে ব্যার্থ", "unable to rename": "দুঃখিত, পুন: নামকরনে ব্যার্থ", "unsaved file": "ফাইলটি সংরক্ষণ করা হইনি, তাও বন্ধ করবেন?", "warning": "সতর্ক বাণী", "use emmet": "emmet ব্যবহার করুন", "use quick tools": "quick tools ব্যবহার করুন", "yes": "হ্যা", "encoding": "টেক্সট এনকোডিং", "syntax highlighting": "সিনট্যাক্স হাইলাইট", "read only": "রিড অনলি", "select all": "সবটুকু নির্বাচন করুন", "select branch": "শাখা নির্বাচন করুন", "create new branch": "নতুন শাখা খুলুন", "use branch": "শাখা ব্যবহার করুন", "new branch": "নতুন শাখা", "branch": "শাখা", "key bindings": "কী বাইন্ডিংস", "edit": "সম্পাদন করুন", "reset": "রিসেট", "color": "রং", "select word": "শব্দ নির্বাচন করুন", "quick tools": "Quick tools", "select": "নির্বাচন", "editor font": "সম্পাদক ফন্ট", "new project": "নতুন প্রজেক্ট", "format": "সাজান", "project name": "প্রোজেক্ট এর নাম ", "unsupported device": "আপনার ডিভাইসটি এই থিম সাপোর্ট করেনা।", "vibrate on tap": "Vibrate on tap", "copy command is not supported by ftp.": "কপি কমান্ড এফটিপি দ্বারা সমর্থিত নয়।", "support title": "Acode কে সমর্থন করুন", "fullscreen": "ফুলস্ক্রিন", "animation": "অ্যানিমেশন", "backup": "ব্যাকআপ", "restore": "restore", "backup successful": "ব্যাকআপ সফল", "invalid backup file": "অবৈধ ব্যাকআপ ফাইল", "add path": "পথ যোগ করুন", "live autocompletion": "লাইভ স্বয়ংক্রিয়-সম্পূর্ণকরন", "file properties": "ফাইলের বৈশিষ্ট্য", "path": "পথ", "type": "ধরন", "word count": "শব্দ গণনা", "line count": "লাইন গণনা", "last modified": "শেষ সংশোধন", "size": "আকার", "share": "শেয়ার করুন", "show print margin": "প্রিন্ট মার্জিন দেখাও", "login": "লগইন", "scrollbar size": "স্ক্রলবারের সাইজ", "cursor controller size": "কার্সর কন্ট্রলার আকার", "none": "none", "small": "ছোট", "large": "বড়", "floating button": "ভাসমান বাটোন", "confirm on exit": "প্রস্থান নিশ্চিত করুন", "show console": "কনসোল দেখান", "image": "ছবি", "insert file": "ফাইল যোগ করুন", "insert color": "রঙ যোগ করুন", "powersave mode warning": "এক্সটার্নাল ব্রাউজারে প্রিভিউ দেখতে পাওয়ার সেভিং মোড বন্ধ করুন।", "exit": "প্রস্থান করুন", "custom": "custom", "reset warning": "আপনি কি নিশ্চিত যে আপনি থিম রিসেট করতে চান৷?", "theme type": "থিমের ধরণ", "light": "লাইট", "dark": "ডার্ক", "file browser": "ফাইল ব্রাউজার", "operation not permitted": "কার্যক্রম অনুমোদিত নয়", "no such file or directory": "এমন কোন ফাইল বা ডিরেক্টরি নেই", "input/output error": "ইনপুট/আউটপুট-এ ত্রুটি", "permission denied": "অনুমতি দেয়া হয় নি", "bad address": "Bad address", "file exists": "ফাইল বিদ্যমান", "not a directory": "ডিরেক্টরি নয়", "is a directory": "একটি ডিরেক্টরি", "invalid argument": "অগ্রহণযোগ্য আর্গুমেন্ট", "too many open files in system": "সিস্টেমে অনেকগুলো ফাইল খোলা", "too many open files": "অনেক ফাইল খোলা আছে", "text file busy": "টেক্সট ফাইল ব্যাস্ত", "no space left on device": "ডিভাইসে কোন জায়গা অবশিষ্ট নেই", "read-only file system": "শুধুমাত্র পাঠযোগ্য ফাইল সিস্টেম", "file name too long": "ফাইলের নাম অনেক বড়", "too many users": "অনেক বেশি ব্যবহারকারী", "connection timed out": "সংযোগের সময় শেষ", "connection refused": "সংযোগ প্রত্যাখ্যান করা হয়েছে", "owner died": "Owner died", "an error occurred": "একটি ত্রুটি ঘটেছে", "add ftp": "FTP সংযোগ করুন", "add sftp": "SFTP সংযোগ করুন", "save file": "ফাইলটি সেভ করুন", "save file as": "সেইভ ফাইল অ্যাস", "files": "ফাইলসমূহ", "help": "সাহায্য", "file has been deleted": "{file} ডিলিট করা হয়েছে!", "feature not available": "এই বৈশিষ্ট্যটি শুধুমাত্র অ্যাপটির অর্থপ্রদত্ত সংস্করণে উপলব্ধ।", "deleted file": "ডিলিট করা ফাইল", "line height": "লাইনের উচ্চতা", "preview info": "আপনি যদি সক্রিয় ফাইলটি চালাতে চান, তাহলে প্লে আইকনে আলতো চাপুন", "manage all files": "Acode কে আপনার ডিভাইসের ফাইলগুলিকে সহজেই এডিট করতে সেটিংসে সমস্ত ফাইল এডিট করার অনুমতি দিন৷", "close file": "ফাইল বন্ধ করুন", "reset connections": "কানেকশন রিসেট করুন", "check file changes": "ফাইলের পরিবর্তন চেক করুন", "open in browser": "ব্রাউজারে খুলুন", "desktop mode": "ডেক্সটপ মোড", "toggle console": "কনসোল চালু/বন্ধ করুন", "new line mode": "নিউ লাইন মোড", "add a storage": "স্টোরেজ যোগ করুন", "rate acode": "Acode-কে রেট করুন", "support": "সমর্থন", "downloading file": "{file} ডাউনলোড হচ্ছে", "downloading...": "ডাউনলোড হচ্ছে...", "folder name": "ফোল্ডারের-এর নাম", "keyboard mode": "কীবোর্ডের ধরন", "normal": "স্বাভাবিক", "app settings": "অ্যাপ সেটিংস", "disable in-app-browser caching": "ইন-অ্যাপ ব্রাউজারের ক্যাশ বন্ধ করুন", "copied to clipboard": "ক্লিপবোর্ডে কপি করা হয়েছে", "remember opened files": "খোলা ফাইলগুলো মনে রাখুন", "remember opened folders": "খোলা ফোল্ডারগুলো মনে রাখুন", "no suggestions": "কোনো পরামর্শ নেই", "no suggestions aggressive": "No suggestions aggressive", "install": "ইনস্টল করুন", "installing": " ইনস্টল করা হচ্ছে...", "plugins": "প্লাগইন", "recently used": "সম্প্রতি ব্যবহৃত", "update": "আপডেট করুন", "uninstall": "আনইনস্টল করুন", "download acode pro": "Acode pro ডাউনলোড করুন", "loading plugins": "প্লাগইন লোড হচ্ছে", "faqs": "প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী", "feedback": "প্রতিক্রিয়া", "header": "হেডার", "sidebar": "সাইডবার", "inapp": "অ্যাপে", "browser": "ব্রাউজার", "diagonal scrolling": "তির্যক স্ক্রোলিং", "reverse scrolling": "বিপরীত স্ক্রোলিং", "formatter": "ফরম্যাটার", "format on save": "সংরক্ষনের সাথে বিন্যাস করুন", "remove ads": "বিজ্ঞাপনগুলি সরান", "fast": "দ্রুত", "slow": "ধীর", "scroll settings": "স্ক্রোল সেটিংস", "scroll speed": "স্ক্রোল গতি", "loading...": "লোড হচ্ছে...", "no plugins found": "কোনও প্লাগইন পাওয়া যায়নি", "name": "নাম", "username": "ব্যবহারকারীর নাম", "optional": "ঐচ্ছিক", "hostname": "হোস্টের নাম", "password": "পাসওয়ার্ড", "security type": "নিরাপত্তার প্রকার", "connection mode": "সংযোগের প্রকার", "port": "পোর্ট", "key file": "কী ফাইল", "select key file": "কী ফাইল নির্বাচন করুন", "passphrase": "পাসফ্রেজ", "connecting...": "সংযুক্ত হচ্ছে...", "type filename": "ফাইলের নাম টাইপ করুন", "unable to load files": "ফাইল লোড করতে অক্ষম", "preview port": "প্রিভিউ পোর্ট", "find file": "ফাইলটি খুজুন", "system": "সিস্টেম", "please select a formatter": "একটি ফর্ম্যাটার নির্বাচন করুন৷", "case sensitive": "কেস সেন্সিটিভ", "regular expression": "রেগুলার এক্সপ্রেশন", "whole word": "পুরো শব্দ", "edit with": "এডিট উইথ", "open with": "ওপেন উইথ", "no app found to handle this file": "এই ফাইলটি খুলার জন্য কোনো অ্যাপ পাওয়া যায়নি", "restore default settings": "সেটিংটি পূর্বাবস্থায় ফিরিয়ে আনুন", "server port": "সার্ভার পোর্ট", "preview settings": "প্রিভিউ সেটিংস", "preview settings note": "যদি প্রিভিউ পোর্ট ও সার্ভার পোর্ট ভিন্ন হয়ে থাকে, অ্যাপ খুলবে না এর বদলে ব্রাউজারে অথবা অ্যাাপের ব্রাউজারে https://: খুলবে। এটা আপনি অন্য কোথাও সার্ভার চালিয়ে রাখলে ব্যবহার্যোগ্য।", "backup/restore note": "এটি শুধুমাত্র আপনার সেটিংস, কাস্টম থিম এবং কী বাইন্ডিং ব্যাকআপ করবে। এটি আপনার FPT/SFTP, GitHub প্রোফাইল ব্যাকআপ করবে না।", "host": "হোস্ট", "retry ftp/sftp when fail": "এফটিপি/এসএফটিপি ব্যর্থ হলে, পুনরায় চেষ্টা করুন", "more": "আরো", "thank you :)": "ধন্যবাদ :)", "purchase pending": "ক্রয় অপেক্ষারত", "cancelled": "বাতিল", "local": "স্থানীয়", "remote": "দূরবর্তী", "show console toggler": "কনসোল টগলার দেখান", "binary file": "এই ফাইলটিতে বাইনারি ডেটা রয়েছে, আপনি কি এটি খুলতে চান?", "relative line numbers": "আপেক্ষিক লাইন নম্বর", "elastic tabstops": "ইল্যাস্টিক ট্যাবস্টপ্‌স", "line based rtl switching": "লাইনভিত্তিক আরটিএল সুইচিং", "hard wrap": "হার্ড র‍্যাপ", "spellcheck": "বানান পরীক্ষণ", "wrap method": "র‍্যাপ ম্যাথড", "use textarea for ime": "আইএমই এর জন্য টেক্সএরিয়া ব্যবহার করুন", "invalid plugin": "অবৈধ প্লাগইন", "type command": "কমান্ড লিখুন", "plugin": "প্লাগইন", "quicktools trigger mode": "কুইক টুলস সক্রিয়ন মোড", "print margin": "প্রিন্ট মার্জিন", "touch move threshold": "টাচ মুভ সীমা", "info-retryremotefsafterfail": "এফটিপি/এসএফটিপি কানেকশন ব্যর্থ হলে, পুনরায় চেষ্টা করুন", "info-fullscreen": "হোম স্ক্রিনে টাইটেল-বার লুকান।", "info-checkfiles": "অ্যাপ ব্যকগ্রাউন্ডে থাকা অবস্থায় পরিবর্তন পরীক্ষা করুন।", "info-console": "জাভাস্ক্রিপ্ট কনসোল নির্বাচন করুন। ল্যাগেসি হচ্ছে সাধারন কনসোল , eruda হচ্ছে থার্ড পার্টি কনসোল", "info-keyboardmode": "টেক্সট ইনপুটের জন্য কীবোর্ড মোড। 'No suggestions' সক্রিয় হলে পরামর্শগুলো লুকানো হবে এবং অটো কারেক্ট কাজ করবে। যদি 'No suggestions' কাজ না করে, তবে মানটি 'No suggestions aggressive'-এ পরিবর্তন করে চেষ্টা করুন।", "info-rememberfiles": "অ্যাপ বন্ধ হলেও সক্রিয় ফাইলগুলো মনে রাখুন।", "info-rememberfolders": "অ্যাপ বন্ধ হলেও সক্রিয় ফোল্ডারগুলো মনে রাখুন।", "info-floatingbutton": "কুইক টুলস, ভাসমান বাটন দেখান অথবা লুকান।", "info-openfilelistpos": "সক্রিয় ফাইলের তালিকা যেখানে দেখানো হবে।", "info-touchmovethreshold": "যদি আপনার ডিভাইসের টাচ সেন্সিটিভিটি বেশি হয়ে থাকে, দূর্ঘটনাবসত স্থানান্তর বন্ধ করতে আপনি এই মানটি বাড়াতে পারেন।", "info-scroll-settings": "এই সেটিংস-এর মাঝে স্ক্রল সেটিংসের অন্তর্যুক্ত টেক্সট র‍্যাপ সেটিংস রয়েছে ", "info-animation": "যদি অ্যাপটি ধীরে কাজ করে, অ্যানিমেশন বন্ধ করুন।", "info-quicktoolstriggermode": "যদি quick tools এর বাটনগুলো কাজ না করে, এখানের মানগুলো পরিবর্তন করে চেষ্টা করুন।", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API সার্ভার ডাউন, দয়া করে পুনরায় চেষ্টা করুন", "installed": "ইনস্টল করা হয়েছে", "all": "সব", "medium": "মধ্যম", "refund": "রিফান্ড", "product not available": "প্রোডাক্ট লভ্য নয়", "no-product-info": "এই প্রোডাক্টটি বর্তমানে আপনার দেশে উপলব্ধ নয়, পরে আবার চেষ্টা করুন।", "close": "বন্ধ", "explore": "এক্সপ্লোর", "key bindings updated": "কী বাইন্ডিংস আপডেট হয়েছে", "search in files": "ফাইলগুলোর মাঝে খুঁজুন", "exclude files": "ফাইল ছাঁটাই করুন", "include files": "ফাইল অন্তর্ভুক্ত করুন", "search result": "{files}টি ফাইলের মধ্যে {matches}টি ফলাফল পাওয়া গেছে।", "invalid regex": "অবৈধ রেগুলার এক্সপ্রেশন: {message}।", "bottom": "Bottom", "save all": "সব সংরক্ষন করুন", "close all": "সব বন্ধ করুন", "unsaved files warning": "কিছু ফাইল সংরক্ষিত নয়। 'ok' তে ক্লিক করে কি করা হবে নির্বাচন করুন অথবা 'cancel' চেপে পেছনে ফিরে যান।", "save all warning": "আপনি কি নিশ্চিতভাবে সকল ফাইল সংরক্ষন করে বন্ধ চান? এই কার্যক্রমটির ফলাফল বাতিল করা যাবে না।", "save all changes warning": "আপনি কি নিশ্চিতভাবে সকল ফাইল সংরক্ষন করতে চান?", "close all warning": "আপনি কি নিশ্চিতভাবে সকল ফাইল বন্ধ করতে চান? আপনার অসংরক্ষিত ফলাফলসমূহ হারিয়ে যাবে এবং ফেরানো সম্ভব হবে না।", "refresh": "রিফ্রেস", "shortcut buttons": "সর্টকাট বাটন", "no result": "কোনো ফলাফল নেই", "searching...": "খোঁজ চলছে...", "quicktools:ctrl-key": "কন্ট্রল/কমান্ড কী", "quicktools:tab-key": "ট্যাব কী", "quicktools:shift-key": "শিফট কী", "quicktools:undo": "আনডু", "quicktools:redo": "রিডু", "quicktools:search": "ফাইলের মাঝে খুঁজুন", "quicktools:save": "ফাইল সংরক্ষণ করুন", "quicktools:esc-key": "এসকেপ কী", "quicktools:curlybracket": "কার্লি ব্র্যাকেট যুক্ত করুন", "quicktools:squarebracket": "স্কয়ার ব্র্যাকেট যুক্ত করুন", "quicktools:parentheses": "বন্ধনী যুক্ত করুন", "quicktools:anglebracket": "এঙ্গেল ব্র্যাকেট যুক্ত করুন", "quicktools:left-arrow-key": "লেফট অ্যারো কী", "quicktools:right-arrow-key": "রাইট অ্যাারো কী", "quicktools:up-arrow-key": "আপ অ্যাারো কী", "quicktools:down-arrow-key": "ডাউন অ্যাারো কী", "quicktools:moveline-up": "লাইন উপরে সরান", "quicktools:moveline-down": "লাইন নিচে সরান", "quicktools:copyline-up": "লাইন উপরে কপি করুন", "quicktools:copyline-down": "লাইন নিচে কপি করুন", "quicktools:semicolon": "সেমিকোলোন যোগ করুন", "quicktools:quotation": "কোটেশন যোগ করুন", "quicktools:and": "অ্যাান্ড চিহ্ন যোগ করুন", "quicktools:bar": "বার চিহ্ন যুক্ত করুন", "quicktools:equal": "সমান চিহ্ন যোগ করুন", "quicktools:slash": "স্ল্যাশ চিহ্ন যুক্ত করুন", "quicktools:exclamation": "আশ্চর্যবোধক চিহ্ন যোগ করুন", "quicktools:alt-key": "আল্ট কী", "quicktools:meta-key": "উইন্ডোজ/মেটা কী", "info-quicktoolssettings": "সম্পাদকের নিচে Quicktools কন্টেইনারে শর্টকাট বাটন এবং কীবোর্ড কী কাস্টমাইজ করুন, যাতে কোডিং অভিজ্ঞতা উন্নত হয়।", "info-excludefolders": "node_modules ফোল্ডারের সব ফাইল উপেক্ষা করতে /node_modules/ প্যাটার্ন ব্যবহার করুন। এটি ফাইলগুলো তালিকাভুক্ত হতে বাধা দেবে এবং সার্চে অন্তর্ভুক্ত হবে না।", "missed files": "সার্চ শুরু হওয়ার পরে {count} ফাইল স্ক্যান করা হয়েছে এবং সার্চে অন্তর্ভুক্ত হবে না।", "remove": "মুছে ফেলুন", "quicktools:command-palette": "কমান্ড প্যালেট", "default file encoding": "ডিফল্ট ফাইল এনকোডিং", "remove entry": "আপনি কি নিশ্চিতভাবে '{name}' সংরক্ষিত পথ থেকে মুছে ফেলতে চান? মনে রাখবেন এটা মুছে ফেললেও পথটি মুছে যাবে না।", "delete entry": "মুছে ফেলা নিশ্চিত করুন: '{name}'। কার্যক্রম অসম্পাদিত করা সম্ভব হবে না। চালিয়ে যান?", "change encoding": "পুনরায় '{file}' ফাইলটি '{encoding}'-এ খুলুন? এই কার্যক্রমের মাধ্যমে অসংরক্ষিত পরিবর্তনগুলো হারিয়ে যাবে. আপনি কি নিশ্চিতভাবে পুনরায় খুলতে চান?", "reopen file": "আপনি কি নিশ্চিতভাবে '{file}' পুনরায় খুলতে চান? অসংরক্ষিত পরিবর্তনগুলো হারিয়ে যাবে।", "plugin min version": "{name} শুধমাত্র Acode - {v-code} কিংবা তার পরবর্তিতে লভ্য. আপডেট করতে এখানে ক্লিক করুন।", "color preview": "কালার প্রিভিউ", "confirm": "নিশ্চিত", "list files": "{name}- এর ভেতরের সকল ফাইল তালিকাভুক্ত করুন? অতিরিক্ত ফাইল অ্যাপটি ক্র্যাশ করতে পারে।", "problems": "সমস্যাগুলো", "show side buttons": "সাইড বাটনগুলো দেখান", "bug_report": "বাগ রিপোর্ট জমা দিন", "verified publisher": "ভেরিফায়েড প্রকাশক", "most_downloaded": "সর্বাধিক ডাউনলোড", "newly_added": "নতুন যোগ হয়েছে", "top_rated": "সর্বোচ্চ রেটিং", "rename not supported": "Termux ডিরেক্টরিতে নাম পরিবর্তন সমর্থিত নয়", "compress": "সংকুচিত করুন", "copy uri": "URI কপি করুন", "delete entries": "আপনি কি নিশ্চিত যে {count} আইটেম মুছে ফেলতে চান?", "deleting items": "{count} আইটেম মুছে ফেলা হচ্ছে...", "import project zip": "প্রোজেক্ট (zip) ইম্পোর্ট করুন", "changelog": "পরিবর্তনের তালিকা", "notifications": "নোটিফিকেশন", "no_unread_notifications": "কোনো অনপঠিত নোটিফিকেশন নেই", "should_use_current_file_for_preview": "ডিফল্ট (index.html) এর পরিবর্তে প্রিভিউ-এর জন্য বর্তমান ফাইল ব্যবহার করা উচিত কি?", "fade fold widgets": "ফোল্ড উইজেটস ফেড করুন", "quicktools:home-key": "হোম কী", "quicktools:end-key": "এন্ড কী", "quicktools:pageup-key": "পেজআপ কী", "quicktools:pagedown-key": "পেজডাউন কী", "quicktools:delete-key": "ডিলেট কী", "quicktools:tilde": "টিল্ডা চিহ্ন যোগ করুন", "quicktools:backtick": "ব্যাকটিক চিহ্ন যোগ করুন", "quicktools:hash": "হ্যাশ চিহ্ন যোগ করুন", "quicktools:dollar": "ডলার চিহ্ন যোগ করুন", "quicktools:modulo": "মডুলো/পারসেন্ট চিহ্ন যোগ করুন", "quicktools:caret": "ক্যারেট চিহ্ন যোগ করুন", "plugin_enabled": "প্লাগইন সক্রিয়", "plugin_disabled": "প্লাগইন নিষ্ক্রিয়", "enable_plugin": "এই প্লাগইন সক্রিয় করুন", "disable_plugin": "এই প্লাগইন নিষ্ক্রিয় করুন", "open_source": "ওপেন সোর্স", "terminal settings": "টার্মিনাল সেটিংস", "font ligatures": "ফন্ট লিগেচার", "letter spacing": "অক্ষরের ফাঁক", "terminal:tab stop width": "ট্যাব স্টপ প্রস্থ", "terminal:scrollback": "স্ক্রলব্যাক লাইন", "terminal:cursor blink": "কার্সর ব্লিঙ্ক", "terminal:font weight": "ফন্ট ওজন", "terminal:cursor inactive style": "নিষ্ক্রিয় কার্সর স্টাইল", "terminal:cursor style": "কার্সর স্টাইল", "terminal:font family": "ফন্ট ফ্যামিলি", "terminal:convert eol": "EOL রূপান্তর করুন", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "টার্মিনাল", "allFileAccess": "সকল ফাইল অ্যাক্সেস", "fonts": "ফন্ট", "sponsor": "স্পন্সর", "downloads": "ডাউনলোড", "reviews": "রিভিউ", "overview": "সংক্ষিপ্ত বিবরণ", "contributors": "অবদানকারী", "quicktools:hyphen": "হাইফেন যুক্ত করুন", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/cs-cz.json ================================================ { "lang": "Čeština", "about": "O aplikaci", "active files": "Zobrazení aktivních souborů", "alert": "Upozornění", "app theme": "Motiv aplikace", "autocorrect": "Povolit automatické opravy?", "autosave": "Automatické ukládání", "cancel": "Zrušit", "change language": "Změnit jazyk", "choose color": "Vybrat barvu", "clear": "vymazat", "close app": "Zavřít aplikaci?", "commit message": "Zpráva pro commit", "console": "Konzole", "conflict error": "Commit konflikt! Počkejte prosím s dalším commitem.", "copy": "Kopírovat", "create folder error": "Omlouváme se, ale nelze vytvořit novou složku.", "cut": "Vyjmout", "delete": "Smazat", "dependencies": "Závislosti", "delay": "Čas v milisekundách", "editor settings": "Nastavení editoru", "editor theme": "Motiv editoru", "enter file name": "Zadejte název souboru", "enter folder name": "Zadejte název složky", "empty folder message": "Prázdná složka", "enter line number": "Zadejte číslo řádku", "error": "Chyba", "failed": "selhal", "file already exists": "Soubor již existuje", "file already exists force": "Soubor již existuje. Přepsat?", "file changed": " byl změněn, načíst soubor znovu?", "file deleted": "Soubor smazán", "file is not supported": "Soubor není podporován", "file not supported": "Tento typ souboru není podporován.", "file too large": "Soubor je příliš velký na zpracování. Maximální povolená velikost souboru je {size}", "file renamed": "soubor přejmenován", "file saved": "soubor uložen", "folder added": "složka přidána", "folder already added": "složka již byla přidána", "font size": "Velikost písma", "goto": "Přejít na řádek", "icons definition": "Definice ikon", "info": "Informace", "invalid value": "Neplatná hodnota", "language changed": "Jazyk byl úspěšně změněn", "linting": "Zkontrolujte syntaktickou chybu", "logout": "Odhlásit se", "loading": "Načítání", "my profile": "Můj profil", "new file": "Nový soubor", "new folder": "Nová složka", "no": "Ne", "no editor message": "Otevřít nebo vytvořit nový soubor a složku z nabídky", "not set": "Není nastaveno", "unsaved files close app": "Existují neuložené soubory. Chcete zavřít aplikaci?", "notice": "Upozornění", "open file": "Otevřít soubor", "open files and folders": "Otevřít soubory a složky", "open folder": "Otevřít složku", "open recent": "Otevřít nedávné", "ok": "OK", "overwrite": "Přepsat", "paste": "Vložit", "preview mode": "Režim náhledu", "read only file": "Nelze uložit soubor určený pouze pro čtení. Zkuste prosím uložit jako", "reload": "Znovu načíst", "rename": "Přejmenovat", "replace": "Nahradit", "required": "Toto pole je povinné", "run your web app": "Spusťte svou webovou aplikaci", "save": "Uložit", "saving": "Ukládání", "save as": "Uložit jako", "save file to run": "Uložte si tento soubor pro spuštění v prohlížeči", "search": "Vyhledávání", "see logs and errors": "Zobrazit protokoly a chyby", "select folder": "Vybrat složku", "settings": "Nastavení", "settings saved": "Nastavení uloženo", "show line numbers": "Zobrazit čísla řádků", "show hidden files": "Zobrazit skryté soubory", "show spaces": "Zobrazit mezery", "soft tab": "Měkký tabulátor", "sort by name": "Seřadit podle názvu", "success": "Úspěch", "tab size": "Velikost tabulátoru", "text wrap": "Zalamování textu / Zalamování slov", "theme": "Motiv", "unable to delete file": "nelze smazat soubor", "unable to open file": "Omlouváme se, soubor se nepodařilo otevřít", "unable to open folder": "Omlouváme se, složku se nepodařilo otevřít", "unable to save file": "Omlouváme se, soubor se nepodařilo uložit", "unable to rename": "Omlouváme se, přejmenování se nepodařilo", "unsaved file": "Tento soubor není uložen, přesto ho zavřít?", "warning": "Upozornění", "use emmet": "Použít emmet", "use quick tools": "Použít Rychlé nástroje", "yes": "Ano", "encoding": "Kódování textu", "syntax highlighting": "Zvýrazňování syntaxe", "read only": "Pouze pro čtení", "select all": "Vybrat vše", "select branch": "Vybrat větev", "create new branch": "Vytvořit novou větev", "use branch": "Použít větev", "new branch": "Nová větev", "branch": "Větev", "key bindings": "Klávesové zkratky", "edit": "Editovat", "reset": "Resetovat", "color": "Barva", "select word": "Vybrat slovo", "quick tools": "Rychlé nástroje", "select": "Vybrat", "editor font": "Písmo editoru", "new project": "Nový projekt", "format": "Formát", "project name": "Název projektu", "unsupported device": "Vaše zařízení nepodporuje motiv.", "vibrate on tap": "Vibrace při klepnutí", "copy command is not supported by ftp.": "Příkaz kopírování není FTP podporován.", "support title": "Podpora Acode", "fullscreen": "Celá obrazovka", "animation": "Animace", "backup": "Záloha", "restore": "Obnovit", "backup successful": "Zálohování bylo úspěšné", "invalid backup file": "Neplatný záložní soubor", "add path": "Přidat cestu", "live autocompletion": "Automatické doplňování v reálném čase", "file properties": "Vlastnosti souboru", "path": "Cesta", "type": "Typ", "word count": "Počet slov", "line count": "Počet řádků", "last modified": "Naposledy upraveno", "size": "Velikost", "share": "Sdílet", "show print margin": "Zobrazit okraj tisku", "login": "přihlášení", "scrollbar size": "Velikost posuvníku", "cursor controller size": "Velikost držadel u kurzoru", "none": "Žádná", "small": "Malá", "large": "Velká", "floating button": "Plovoucí tlačítko", "confirm on exit": "Potvrdit při zavření", "show console": "Zobrazit konzoli", "image": "Obrázek", "insert file": "Vložit soubor", "insert color": "Vložit barvu", "powersave mode warning": "Pro zobrazení náhledu v externím prohlížeči vypněte režim úspory energie.", "exit": "Konec", "custom": "Vlastní", "reset warning": "Jste si jisti, že chcete resetovat motiv?", "theme type": "Typ motivu", "light": "Světlý", "dark": "Tmavý", "file browser": "Prohlížeč souborů", "operation not permitted": "Operace není povolena", "no such file or directory": "Soubor nebo adresář neexistuje", "input/output error": "Chyba vstupu/výstupu", "permission denied": "Oprávnění zamítnuto", "bad address": "Špatná adresa", "file exists": "Soubor již existuje", "not a directory": "Není složka", "is a directory": "Je složka", "invalid argument": "Neplatný argument", "too many open files in system": "Příliš mnoho otevřených souborů v systému", "too many open files": "Příliš mnoho otevřených souborů", "text file busy": "Textový soubor je zaneprázdněn", "no space left on device": "Na zařízení nezbývá místo", "read-only file system": "Souborový systém pouze pro čtení", "file name too long": "Název souboru je příliš dlouhý", "too many users": "Příliš mnoho uživatelů", "connection timed out": "Časový limit připojení vypršel", "connection refused": "Spojení odmítnuto", "owner died": "Owner died", "an error occurred": "Došlo k chybě", "add ftp": "Přidat FTP", "add sftp": "Přidat SFTP", "save file": "Uložit soubor", "save file as": "Uložit soubor jako", "files": "Soubory", "help": "Nápověda", "file has been deleted": "Soubor {file} byl smazán!", "feature not available": "Tato funkce je k dispozici pouze v placené verzi aplikace.", "deleted file": "Smazaný soubor", "line height": "Výška řádku", "preview info": "Pokud chcete spustit aktivní soubor, klepněte na a podržte ikonu přehrávání.", "manage all files": "Povolte editoru Acode správu všech souborů v nastavení, abyste mohli snadno upravovat soubory ve svém zařízení.", "close file": "Zavřít soubor", "reset connections": "Obnovit připojení", "check file changes": "Zkontrolovat změny v souboru", "open in browser": "Otevřít v prohlížeči", "desktop mode": "Režim plochy", "toggle console": "Přepnout konzoli", "new line mode": "Režim nového řádku", "add a storage": "Přidat úložiště", "rate acode": "Ohodnotit Acode", "support": "Podpora", "downloading file": "Stahování souboru {file}", "downloading...": "Stahování...", "folder name": "Název složky", "keyboard mode": "Režim klávesnice", "normal": "Normání", "app settings": "Nastavení aplikace", "disable in-app-browser caching": "Zakázat ukládání do mezipaměti v prohlížeči aplikace", "copied to clipboard": "Zkopírováno do schránky", "remember opened files": "Zapamatovat si otevřené soubory", "remember opened folders": "Zapamatovat si otevřené složky", "no suggestions": "Žádné návrhy", "no suggestions aggressive": "Žádné agresivní návrhy", "install": "Instalovat", "installing": "Instalace...", "plugins": "Pluginy", "recently used": "Nedávno použité", "update": "Aktualizovat", "uninstall": "Odinstalovat", "download acode pro": "Stáhnout Acode Pro", "loading plugins": "Načítání pluginů", "faqs": "Často kladené otázky", "feedback": "Zpětná vazba", "header": "Nahoře", "sidebar": "V bočním panelu", "inapp": "V aplikaci", "browser": "Prohlížeč", "diagonal scrolling": "Diagonální posouvání", "reverse scrolling": "Obrácené posouvání", "formatter": "Formátovač", "format on save": "Formátovat při ukládání", "remove ads": "Odstranit reklamy", "fast": "Rychle", "slow": "Pomalu", "scroll settings": "Nastavení posouvání", "scroll speed": "Rychlost posování", "loading...": "Načítání...", "no plugins found": "Nenalezeny žádné pluginy", "name": "Jméno", "username": "Uživatelské jméno", "optional": "volitelné", "hostname": "Název hostitele", "password": "Heslo", "security type": "Typ zabezpečení", "connection mode": "Režim připojení", "port": "port", "key file": "Soubor s klíčem", "select key file": "Vybrat soubor s klíčem", "passphrase": "Heslo", "connecting...": "Připojování...", "type filename": "Zadejte název souboru", "unable to load files": "Nelze načíst soubory", "preview port": "Port náhledu", "find file": "Najít soubor", "system": "Systém", "please select a formatter": "Prosím, vyberte formátovač", "case sensitive": "Rozlišovat velká a malá písmena", "regular expression": "Regulární výraz", "whole word": "Celé slovo", "edit with": "Editovat s", "open with": "Otevřít s", "no app found to handle this file": "Nebyla nalezena žádná aplikace pro zpracování tohoto souboru", "restore default settings": "Obnovit výchozí nastavení", "server port": "Port serveru", "preview settings": "Nastavení náhledu", "preview settings note": "Pokud se port náhledu a port serveru liší, aplikace nespustí server a místo toho otevře https://: v prohlížeči nebo v prohlížeči aplikace. To je užitečné, když provozujete server někde jinde.", "backup/restore note": "Zálohuje pouze vaše nastavení, vlastní šablonu, nainstalované pluginy a klávesové zkratky. Nezálohuje stav vašeho FTP/SFTP ani aplikace.", "host": "Hostite", "retry ftp/sftp when fail": "V případě selhání zkuste znovu ftp/sftp", "more": "Více", "thank you :)": "Děkuji :)", "purchase pending": "nákup čeká na vyřízení", "cancelled": "zrušeno", "local": "Lokální", "remote": "Vzdálený", "show console toggler": "Zobrazit přepínač konzole", "binary file": "Tento soubor obsahuje binární data, chcete ho otevřít?", "relative line numbers": "Relativní čísla řádků", "elastic tabstops": "Elastické záložky", "line based rtl switching": "Přepínání RTL na bázi řádku", "hard wrap": "Pevné zalomení", "spellcheck": "Kontrola pravopisu", "wrap method": "Metoda zalomení", "use textarea for ime": "Použít textovou oblast pro IME", "invalid plugin": "Neplatný plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Režim spouštění Rychlých nástrojů", "print margin": "Okraj tisku", "touch move threshold": "Nastavení citlivosti dotyku", "info-retryremotefsafterfail": "V případě selhání se znovu pokusit o připojení FTP/SFTP.", "info-fullscreen": "Skrýt titulní lištu na domovské obrazovce.", "info-checkfiles": "Kontrolovat změny souborů, když je aplikace na pozadí.", "info-console": "Vyberte konzoli JavaScriptu. Legacy je výchozí konzole, eruda je konzole třetí strany.", "info-keyboardmode": "Režim klávesnice pro zadávání textu, žádné návrhy skryjí návrhy a automatické opravy. Pokud možnost žádné návrhy nefunguje, zkuste změnit hodnotu na agresivní režim bez návrhů.", "info-rememberfiles": "Zapamatovat si otevřené soubory i po zavření aplikace.", "info-rememberfolders": "Zapamatovat si otevřené složky při zavření aplikace.", "info-floatingbutton": "Zobrazit nebo skrýt plovoucí tlačítko pro rychlé nástroje.", "info-openfilelistpos": "Kde zobrazit seznam aktivních souborů.", "info-touchmovethreshold": "Pokud je citlivost dotyku vašeho zařízení příliš vysoká, můžete tuto hodnotu zvýšit, abyste zabránili nechtěnému dotyku.", "info-scroll-settings": "Tato nastavení obsahují nastavení posouvání včetně zalamování textu.", "info-animation": "Pokud se aplikace zdá pomalá, vypněte animace.", "info-quicktoolstriggermode": "Pokud tlačítko v rychlých nástrojích nefunguje, zkuste tuto hodnotu změnit.", "info-checkForAppUpdates": "Automaticky kontrolovat aktualizace aplikace.", "info-quickTools": "Zobrazit nebo skrýt rychlé nástroje.", "info-showHiddenFiles": "Zobrazit skryté soubory a složky. (Začínající .)", "info-all_file_access": "Povolit přístup k /sdcard a /storage v terminálu.", "info-fontSize": "Velikost písma použitá k vykreslení textu.", "info-fontFamily": "Písmo použité k vykreslení textu.", "info-theme": "Barevný motiv terminálu.", "info-cursorStyle": "Styl kurzoru, když je terminál používán.", "info-cursorInactiveStyle": "Styl kurzoru, když terminál není používán.", "info-fontWeight": "Tloušťka písma použitá k vykreslení netučného textu.", "info-cursorBlink": "Zda kurzor bliká.", "info-scrollback": "Míra posunu zpět v terminálu. Posun zpět je počet řádků, které zůstanou zachovány při posunu řádků za počáteční zobrazovací oblast.", "info-tabStopWidth": "Velikost zarážek tabulace v terminálu.", "info-letterSpacing": "Mezery mezi znaky v pixelech.", "info-imageSupport": "Zda jsou v terminálu podporovány obrázky.", "info-fontLigatures": "Zda jsou v terminálu povoleny ligatury písem.", "info-confirmTabClose": "Před zavřením karet terminálu si vyžádat potvrzení.", "info-backup": "Vytvoří zálohu instalace terminálu.", "info-restore": "Obnoví zálohu instalace terminálu.", "info-uninstall": "Odinstaluje instalaci terminálu.", "owned": "Vlastněno", "api_error": "API server je nefunkční, zkuste to prosím později.", "installed": "Nainstalováno", "all": "Vše", "medium": "Medium", "refund": "Vrácení peněz", "product not available": "Produkt není k dispozici", "no-product-info": "Tento produkt momentálně není ve vaší zemi k dispozici, zkuste to prosím znovu později.", "close": "Zavřít", "explore": "Prozkoumat", "key bindings updated": "Klávesové zkratky aktualizovány", "search in files": "Hledat v souborech", "exclude files": "Vyloučit soubory", "include files": "Zahrnout soubory", "search result": "hledání {matches} našlo {files} souborů.", "invalid regex": "Neplatný regulární výraz: {message}.", "bottom": "Dole", "save all": "Uložit vše", "close all": "Zavřít vše", "unsaved files warning": "Některé soubory se nepodařilo uložit. Klikněte na tlačítko „OK“ a vyberte, co chcete udělat, nebo se vraťte zpět tlačítkem „Zrušit“.", "save all warning": "Opravdu chcete uložit všechny soubory a zavřít? Tuto akci nelze vrátit zpět.", "save all changes warning": "Jste si jisti, že chcete uložit všechny soubory?", "close all warning": "Opravdu chcete zavřít všechny soubory? Ztratíte neuložené změny a tuto akci nelze vrátit zpět.", "refresh": "Obnovit", "shortcut buttons": "Zkratky", "no result": "Žádný výsledek", "searching...": "Hledání...", "quicktools:ctrl-key": "Klávesa Control/Command", "quicktools:tab-key": "Klávesa Tab", "quicktools:shift-key": "Klávesa Shift", "quicktools:undo": "Zpět", "quicktools:redo": "Znovu", "quicktools:search": "Hledat v souboru", "quicktools:save": "Uložit soubor", "quicktools:esc-key": "Klávesa Esc", "quicktools:curlybracket": "Vložit složenou závorku", "quicktools:squarebracket": "Vložit hranatou závorku", "quicktools:parentheses": "Vložit závorku", "quicktools:anglebracket": "Vložit ostrou závorku", "quicktools:left-arrow-key": "Šipka vlevo", "quicktools:right-arrow-key": "Šipka vpravo", "quicktools:up-arrow-key": "Šipka nahoru", "quicktools:down-arrow-key": "Šipka dolů", "quicktools:moveline-up": "Posunout řádek nahoru", "quicktools:moveline-down": "Posunout řádek dolů", "quicktools:copyline-up": "Kopírovat řádek nahoru", "quicktools:copyline-down": "Kopírovat řádek dolů", "quicktools:semicolon": "Vložit středník", "quicktools:quotation": "Vložit citaci", "quicktools:and": "Vložit & symbol", "quicktools:bar": "Vložit | symbol ", "quicktools:equal": "Vložit symbol =", "quicktools:slash": "Vložit symbol /", "quicktools:exclamation": "Vložit vykřičník", "quicktools:alt-key": "Klávesa Alt", "quicktools:meta-key": "Klávesa Windows/Meta", "info-quicktoolssettings": "V rychlých nástrojích pod editorem si můžete přizpůsobit tlačítka a klávesové zkratky.", "info-excludefolders": "Použijte vzor **/node_modules/** pro ignorování všech souborů ze složky node_modules. Tím se soubory vyloučí ze seznamu a také zabrání jejich zahrnutí do vyhledávání souborů.", "missed files": "Po zahájení vyhledávání bylo naskenováno {count} souborů, které nebudou zahrnuty do vyhledávání.", "remove": "Odstranit", "quicktools:command-palette": "Paleta příkazů", "default file encoding": "Výchozí kódování souborů", "remove entry": "Opravdu chcete odstranit '{name}' z uložených cest? Upozorňujeme, že jeho odstraněním se samotná cesta neodstraní.", "delete entry": "Potvrdit smazání: '{name}'. Tuto akci nelze vrátit zpět. Pokračovat?", "change encoding": "Znovu otevřít soubor '{file}' s kódováním '{encoding}'? Tato akce povede ke ztrátě všech neuložených změn provedených v souboru. Chcete pokračovat v opětovném otevření?", "reopen file": "Opravdu chcete znovu otevřít soubor '{file}'? Veškeré neuložené změny budou ztraceny.", "plugin min version": "{name} je k dispozici pouze v Acode - {v-code} a vyšších verzích. Klikněte zde pro aktualizaci.", "color preview": "Náhled barev", "confirm": "Potvrdit", "list files": "Zobrazit všechny soubory v {name? Příliš mnoho souborů může způsobit pád aplikace.", "problems": "Problémy", "show side buttons": "Zobrazit boční tlačítka", "bug_report": "Odeslat hlášení o chybě", "verified publisher": "Ověřený vydavatel", "most_downloaded": "Nejvíce stahované", "newly_added": "Nově přidané", "top_rated": "Nejlépe hodnocené", "rename not supported": "Přejmenování adresáře termux není podporováno.", "compress": "Komprimovat", "copy uri": "Kopírovat Uri", "delete entries": "Jste si jisti, že chcete smazat {count} položek?", "deleting items": "Mazání položek ({count})...", "import project zip": "Importovat projekt (zip)", "changelog": "Protokol změn", "notifications": "Oznámení", "no_unread_notifications": "Žádná nepřečtená oznámení", "should_use_current_file_for_preview": "Pro náhled by se měl použít aktuální soubor místo výchozího (index.html).", "fade fold widgets": "Widgety s prolínáním a skládáním", "quicktools:home-key": "Klávesa Home", "quicktools:end-key": "Klávesa End", "quicktools:pageup-key": "Klávesa PageUp", "quicktools:pagedown-key": "Klávesa PageDown", "quicktools:delete-key": "Klávesa Delete", "quicktools:tilde": "Vložit symbol ~", "quicktools:backtick": "Vložit symbol `", "quicktools:hash": "Vložit symbol #", "quicktools:dollar": "Vložit symbol $", "quicktools:modulo": "Vložit symbol %", "quicktools:caret": "Vložit symbol ^", "plugin_enabled": "Plugin je povolen", "plugin_disabled": "Plugin je zakázán", "enable_plugin": "Povolit tento plugin", "disable_plugin": "Zakázat tento plugin", "open_source": "Otevřený zdrojový kód", "terminal settings": "Nastavení terminálu", "font ligatures": "Ligatury písma", "letter spacing": "Mezera mezi písmeny", "terminal:tab stop width": "Šířka zarážky tabulátoru", "terminal:scrollback": "Řádky pro posun zpět", "terminal:cursor blink": "Blikání kurzoru", "terminal:font weight": "Tloušťka písma", "terminal:cursor inactive style": "Styl neaktivního kurzoru", "terminal:cursor style": "Styl kurzoru", "terminal:font family": "Písma", "terminal:convert eol": "Převést EOL", "terminal:confirm tab close": "Potvrzení zavření karty terminálu", "terminal:image support": "Podpora obrázků", "terminal": "Terminál", "allFileAccess": "Přístup ke všem souborům", "fonts": "Fonty", "sponsor": "Sponzor", "downloads": "stahování", "reviews": "recenze", "overview": "Přehled", "contributors": "Přispěvatelé", "quicktools:hyphen": "Vložit symbol -", "check for app updates": "Zkontrolovat aktualizace aplikace", "prompt update check consent message": "Acode může zkontrolovat nové aktualizace aplikace, když jste online. Povolit kontroly aktualizací?", "keywords": "Klíčová slova", "author": "Autor", "filtered by": "Filtrováno podle", "clean install state": "Vymazat stav instalace", "backup created": "Záloha vytvořena", "restore completed": "Obnovení dokončeno", "restore will include": "Tím se obnoví", "restore warning": "Tuto akci nelze vrátit zpět. Pokračovat?", "reload to apply": "Znovu načíst pro použití změn?", "reload app": "Znovu načíst aplikaci", "preparing backup": "Příprava zálohy", "collecting settings": "Shromažďování informací o nastavení", "collecting key bindings": "Shromažďování informací o klávesových zkratkách", "collecting plugins": "Shromažďování informací o pluginech", "creating backup": "Vytváření zálohy", "validating backup": "Ověřování zálohy", "restoring key bindings": "Obnovení klávesových zkratek", "restoring plugins": "Obnovení pluginů", "restoring settings": "Obnovení nastavení", "legacy backup warning": "Toto je starší formát zálohy. Některé funkce mohou být omezené.", "checksum mismatch": "Chybný kontrolní součet – záložní soubor mohl být upraven nebo poškozen.", "plugin not found": "Plugin nebyl nalezen v registru", "paid plugin skipped": "Placený plugin - nebyl koupen", "source not found": "Zdrojový soubor již neexistuje.", "restored": "Obnoveno", "skipped": "Přeskočeno", "backup not valid object": "Záložní soubor není platný.", "backup no data": "Záložní soubor neobsahuje žádná data k obnovení", "backup legacy warning": "Toto je starší formát zálohy (v1). Některé funkce mohou být omezené.", "backup missing metadata": "Chybí zálohovací metadata – některé informace nemusí být k dispozici", "backup checksum mismatch": "Chybný kontrolní součet – záložní soubor mohl být upraven nebo poškozen. Postupujte opatrně.", "backup checksum verify failed": "Nepodařilo se ověřit kontrolní součet", "backup invalid settings": "Neplatný formát nastavení", "backup invalid keybindings": "Neplatný formát klávesových zkratek", "backup invalid plugins": "Neplatný formát instalovaných pluginů", "issues found": "Nalezené problémy", "error details": "Podrobnosti o chybě", "active tools": "Aktivní nástroje", "available tools": "Dostupné nástroje", "recent": "Nedávné soubory", "command palette": "Otevřít paletu příkazů", "change theme": "Změnit motiv", "documentation": "Dokumentace", "open in terminal": "Otevřít v terminálu", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/de-de.json ================================================ { "lang": "Deutsch", "about": "über", "active files": "Aktive Dateien", "alert": "Warnung", "app theme": "App-Design", "autocorrect": "Autokorrektur aktivieren?", "autosave": "Automatisch speichern", "cancel": "Abbrechen", "change language": "Sprache wechseln", "choose color": "Farbe auswählen", "clear": "Löschen", "close app": "Anwendung schließen?", "commit message": "Meldung bestätigen", "console": "Konsole", "conflict error": "Konflikt! Bitte warten Sie vor dem nächsten Commit.", "copy": "Kopieren", "create folder error": "Entschuldigung, Ordner kann nicht angelegt werden", "cut": "Ausschneiden", "delete": "Löschen", "dependencies": "Abhängigkeiten", "delay": "Zeit in Millisekunden", "editor settings": "Editor-Einstellungen", "editor theme": "Editordesign", "enter file name": "Dateinamen eingeben", "enter folder name": "Ordnernamen eingeben", "empty folder message": "Leerer Ordner", "enter line number": "Zeilennummer eingeben", "error": "Fehler", "failed": "Fehlgeschlagen", "file already exists": "Datei existiert bereits", "file already exists force": "Datei existiert bereits. Überschreiben?", "file changed": " wurde verändert, Datei neu laden?", "file deleted": "Datei gelöscht", "file is not supported": "Datei wird nicht unterstützt", "file not supported": "Dieser Dateityp wird nicht unterstützt.", "file too large": "Datei ist zu groß zur Bearbeitung. Maximal erlaubte Größe ist {size}", "file renamed": "Datei umbenannt", "file saved": "Datei gespeichert", "folder added": "Ordner hinzugefügt", "folder already added": "Ordner bereits hinzugefügt", "font size": "Schriftgröße", "goto": "Gehe zur Zeile", "icons definition": "Icon-Definitionen", "info": "Information", "invalid value": "Ungültiger Wert", "language changed": "Sprache wurde erfolgreich gewechselt", "linting": "Syntaxprüfung fehlerhaft", "logout": "Abmelden", "loading": "Laden", "my profile": "Mein Profil", "new file": "Neue Datei", "new folder": "Neuer Ordner", "no": "Nein", "no editor message": "Datei oder Ordner über das Menü öffnen oder erstellen", "not set": "Nicht konfiguriert", "unsaved files close app": "Es sind nicht gespeicherte Dateien vorhanden. Anwendung beenden?", "notice": "Hinweis", "open file": "Datei öffnen", "open files and folders": "Dateien und Ordner öffnen", "open folder": "Ordner öffnen", "open recent": "Letzte Dateien öffnen", "ok": "OK", "overwrite": "Überschreiben", "paste": "Einfügen", "preview mode": "Vorschaumodus", "read only file": "Datei kann im Lesemodus nicht gespeichert werden. Versuchen Sie Speichern unter", "reload": "Neu laden", "rename": "Umbenennen", "replace": "Ersetzen", "required": "Dieses Feld ist notwendig", "run your web app": "Ihre Webanwendung starten", "save": "Speichern", "saving": "Speichern", "save as": "Speichern als", "save file to run": "Zum Starten im Browser Datei bitte speichern", "search": "Suche", "see logs and errors": "Log und Fehler ansehen", "select folder": "Ordner wählen", "settings": "Einstellungen", "settings saved": "Einstellungen gespeichert", "show line numbers": "Zeilennummern anzeigen", "show hidden files": "Versteckte Dateien anzeigen", "show spaces": "Leerzeichen anzeigen", "soft tab": "Weiche Tabs", "sort by name": "Nach Namen sortieren", "success": "Erfolg", "tab size": "Tab-Größe", "text wrap": "Textumbruch / Zeilenumbruch", "theme": "Design", "unable to delete file": "Datei kann nicht gelöscht werden", "unable to open file": "Entschuldigung, Datei kann nicht geöffnet werden", "unable to open folder": "Entschuldigung, Ordner kann nicht geöffnet werden", "unable to save file": "Entschuldigung, Datei kann nicht gespeichert werden", "unable to rename": "Entschuldigung, Umbenennen nicht möglich", "unsaved file": "Datei ist nicht gespeichert, trotzdem schließen?", "warning": "Warnung", "use emmet": "Emmet benutzen", "use quick tools": "Schnelltools benutzen", "yes": "Ja", "encoding": "Textcodierung", "syntax highlighting": "Syntaxhervorhebung", "read only": "Nur Lesen", "select all": "Alles auswählen", "select branch": "Branch wählen", "create new branch": "Neuen Branch erzeugen", "use branch": "Benutzer-Branch", "new branch": "Neuer Branch", "branch": "Branch", "key bindings": "Tastenbelegung", "edit": "Bearbeiten", "reset": "Zurücksetzen", "color": "Farbe", "select word": "Wort auswählen", "quick tools": "Schnelltools", "select": "Auswählen", "editor font": "Editor-Schriftart", "new project": "Neues Projekt", "format": "Format", "project name": "Projektname", "unsupported device": "Ihr Gerät unterstützt keine Designs.", "vibrate on tap": "Vibrieren beim Tippen", "copy command is not supported by ftp.": "Kopierkommando wird bei FTP nicht unterstützt.", "support title": "Acode unterstützen", "fullscreen": "Vollbildmodus", "animation": "Startanimation", "backup": "Sicherung", "restore": "Wiederherstellung", "backup successful": "Sicherung erfolgreich", "invalid backup file": "Ungültige Sicherungsdatei", "add path": "Pfad hinzufügen", "live autocompletion": "Direkte Autovervollständigung", "file properties": "Dateieigenschaften", "path": "Pfad", "type": "Typ", "word count": "Wortanzahl", "line count": "Zeilenanzahl", "last modified": "Zuletzt geändert", "size": "Größe", "share": "Freigabe", "show print margin": "Druckrand anzeigen", "login": "Anmelden", "scrollbar size": "Scrollbar-Größe", "cursor controller size": "Cursor-Marker-Größe", "none": "Keiner", "small": "Klein", "large": "Groß", "floating button": "Schwebende Schaltfläche", "confirm on exit": "Bestätigung beim Beenden", "show console": "Konsole anzeigen", "image": "Bild", "insert file": "Datei einfügen", "insert color": "Farbe einfügen", "powersave mode warning": "Energiesparmodus für eine Vorschau im externen Browser abschalten.", "exit": "Verlassen", "custom": "Benutzerdefiniert", "reset warning": "Wollen Sie wirklich das Design zurücksetzen?", "theme type": "Designtyp", "light": "Hell", "dark": "Dunkel", "file browser": "Datei-Browser", "operation not permitted": "Operation nicht zulässig", "no such file or directory": "Keine Datei oder kein Verzeichnis", "input/output error": "Ein-/Ausgabe-Fehler", "permission denied": "Zugriff verweigert", "bad address": "Unzulässige Adresse", "file exists": "Datei existiert bereits", "not a directory": "Ist kein Verzeichnis", "is a directory": "Ist ein Verzeichnis", "invalid argument": "Ungültiges Argument", "too many open files in system": "Zu viele geöffnete Dateien im System", "too many open files": "Zu viele geöffnete Dateien", "text file busy": "Textdatei in Benutzung", "no space left on device": "Kein Speicherplatz mehr auf diesem Gerät", "read-only file system": "Dateisystem ist schreibgeschützt", "file name too long": "Dateiname zu lang", "too many users": "Zu viele Benutzer", "connection timed out": "Verbindungszeit ist abgelaufen", "connection refused": "Verbindung verweigert", "owner died": "Besitzer existiert nicht", "an error occurred": "Es ist ein Fehler aufgetreten", "add ftp": "FTP hinzufügen", "add sftp": "SFTP hinzufügen", "save file": "Datei speichern", "save file as": "Datei speichern als", "files": "Dateien", "help": "Hilfe", "file has been deleted": "{file} wurde gelöscht!", "feature not available": "Diese Funktion ist nur in der Bezahlversion der App verfügbar.", "deleted file": "Gelöschte Datei", "line height": "Zeilenhöhe", "preview info": "Wenn Sie eine aktive Datei starten möchten, tippen und halten Sie das Wiedergabe-Symbol.", "manage all files": "Erlauben Sie Acode, alle Dateien in den Einstellungen zu verwalten, um Dateien auf Ihrem Gerät einfach zu bearbeiten.", "close file": "Datei schließen", "reset connections": "Verbindungen zurücksetzen", "check file changes": "Dateiänderungen prüfen", "open in browser": "Im Browser öffnen", "desktop mode": "Desktop-Modus", "toggle console": "Konsole umschalten", "new line mode": "Zeilenumbruchmodus", "add a storage": "Speicher hinzufügen", "rate acode": "Acode bewerten", "support": "Unterstützung", "downloading file": "{file} herunterladen", "downloading...": "Herunterladen ...", "folder name": "Ordnername", "keyboard mode": "Tastaturmodus", "normal": "Normal", "app settings": "App-Einstellungen", "disable in-app-browser caching": "In-App-Browser-Cache abschalten", "copied to clipboard": "In die Zwischenablage kopiert", "remember opened files": "Geöffnete Dateien merken", "remember opened folders": "Geöffnete Ordner merken", "no suggestions": "Keine Vorschläge", "no suggestions aggressive": "Keine aufdringlichen Vorschläge", "install": "Installation", "installing": "Installieren ...", "plugins": "Plugins", "recently used": "Kürzlich verwendet", "update": "Aktualisierung", "uninstall": "Deinstallation", "download acode pro": "Acode Pro herunterladen", "loading plugins": "Plugins laden", "faqs": "FAQs", "feedback": "Rückmeldung", "header": "Kopfzeile", "sidebar": "Seitenleiste", "inapp": "App-intern", "browser": "Browser", "diagonal scrolling": "Diagonales Scrollen", "reverse scrolling": "Umgekehrtes Scrollen", "formatter": "Formatierer", "format on save": "Beim Speichern formatieren", "remove ads": "Anzeigen entfernen", "fast": "Schnell", "slow": "Langsam", "scroll settings": "Scroll-Einstellungen", "scroll speed": "Scroll-Geschwindigkeit", "loading...": "Laden ...", "no plugins found": "Keine Plugins gefunden", "name": "Name", "username": "Benutzername", "optional": "optional", "hostname": "Hostname", "password": "Passwort", "security type": "Sicherheitstyp", "connection mode": "Verbindungsmodus", "port": "Port", "key file": "Schlüsseldatei", "select key file": "Schlüsseldatei wählen", "passphrase": "Passphrase", "connecting...": "Verbinden ...", "type filename": "Dateinamen angeben", "unable to load files": "Dateien können nicht geladen werden", "preview port": "Vorschau-Port", "find file": "Datei suchen", "system": "System", "please select a formatter": "Bitte wählen Sie eine Formatierung", "case sensitive": "Groß-/Kleinschreibung beachten", "regular expression": "Regulärer Ausdruck", "whole word": "Gesamtes Wort", "edit with": "Bearbeiten mit", "open with": "Öffnen mit", "no app found to handle this file": "Keine App gefunden, um diese Datei zu verarbeiten", "restore default settings": "Standardeinstellungen wiederherstellen", "server port": "Server-Port", "preview settings": "Vorschau-Einstellungen", "preview settings note": "Wenn sich Vorschau-Port und Server-Port unterscheiden, startet die App keinen Server, sondern öffnet stattdessen https://: im Browser oder App-Browser. Dies ist nützlich, um einen Server an einem anderen Ort zu betreiben.", "backup/restore note": "Es werden nur Ihre Einstellungen, benutzerdefinierte Designs, installierte Plugins und Tastenbelegungen gesichert. FTP/SFTP- oder GitHub-Profile werden nicht gesichert.", "host": "Host", "retry ftp/sftp when fail": "FTP/SFTP bei Fehler wiederholen", "more": "Mehr", "thank you :)": "Dankeschön :)", "purchase pending": "Kauf ausstehend", "cancelled": "abgebrochen", "local": "Lokal", "remote": "Entfernt", "show console toggler": "Konsolen-Umschalter anzeigen", "binary file": "Diese Datei enthält Binärdaten, möchten Sie sie wirklich öffnen?", "relative line numbers": "Relative Zeilennummern", "elastic tabstops": "Elastische Tabulatoren", "line based rtl switching": "Zeilenbasierte RTL-Umschaltung", "hard wrap": "Harter Umbruch", "spellcheck": "Rechtschreibprüfung", "wrap method": "Umbruchmethode", "use textarea for ime": "Textbereich für IME benutzen", "invalid plugin": "Ungültiges Plugin", "type command": "Befehl eingeben", "plugin": "Plugin", "quicktools trigger mode": "Schnelltools-Auslösemodus", "print margin": "Druckrand", "touch move threshold": "Schwellenwert für Berührungsbewegung", "info-retryremotefsafterfail": "FTP/SFTP-Verbindung bei Fehler erneut versuchen.", "info-fullscreen": "Titelzeile auf der Startseite verstecken.", "info-checkfiles": "Dateiänderungen überprüfen, wenn sich die App im Hintergrund befindet.", "info-console": "JavaScript-Konsole wählen. Legacy ist die Standardkonsole, Eruda ist die Konsole eines Drittanbieters.", "info-keyboardmode": "Tastaturmodus für Texteingaben. 'Keine Vorschläge' blendet Vorschläge und automatische Korrektur aus. Wenn 'Keine Vorschläge' nicht funktioniert, versuchen Sie, den Wert auf 'Keine aufdringlichen Vorschläge' zu ändern.", "info-rememberfiles": "Geöffnete Dateien merken, wenn die Anwendung geschlossen wird.", "info-rememberfolders": "Geöffnete Ordner merken, wenn die Anwendung geschlossen wird.", "info-floatingbutton": "Schwebende Schaltfläche für Schnelltools ein- oder ausblenden.", "info-openfilelistpos": "Wo soll die Liste der aktiven Dateien angezeigt werden.", "info-touchmovethreshold": "Wenn die Berührungsempfindlichkeit des Gerätes zu hoch ist, können Sie diesen Wert erhöhen, um versehentliche Berührungen zu verhindern.", "info-scroll-settings": "Diese Einstellungen steuern Scrollen und Textumbruch.", "info-animation": "Wenn die Anwendung träge reagiert, deaktivieren Sie die Animation.", "info-quicktoolstriggermode": "Wenn die Schaltfläche in den Schnelltools nicht funktioniert, versuchen Sie, diesen Wert zu ändern.", "info-checkForAppUpdates": "Automatisch nach App-Updates suchen.", "info-quickTools": "Schnelltools ein- oder ausblenden.", "info-showHiddenFiles": "Versteckte Dateien und Ordner anzeigen. (Diese beginnen mit einem .)", "info-all_file_access": "Zugriff auf /sdcard und /storage im Terminal aktivieren.", "info-fontSize": "Die Schriftgröße, die zum Rendern von Text verwendet wird.", "info-fontFamily": "Die Schriftfamilie, die zum Rendern von Text verwendet wird.", "info-theme": "Das Farbdesign des Terminals.", "info-cursorStyle": "Der Cursorstil, wenn das Terminal fokussiert ist.", "info-cursorInactiveStyle": "Der Cursorstil, wenn das Terminal nicht im Fokus ist.", "info-fontWeight": "Die Schriftstärke, die zum Rendern von nicht fettem Text verwendet wird.", "info-cursorBlink": "Erlaubt das Blinken des Cursors.", "info-scrollback": "Die Anzahl der Zeilen, die beim Zurückblättern erhalten bleiben, wenn über den anfänglichen Anzeigebereich hinaus gescrollt wird.", "info-tabStopWidth": "Die Größe der Tabulatoren im Terminal.", "info-letterSpacing": "Der Abstand zwischen den Zeichen in ganzen Pixeln.", "info-imageSupport": "Unterstützung von Bildern im Terminal.", "info-fontLigatures": "Unterstützung von Ligaturen in der Schriftart im Terminal.", "info-confirmTabClose": "Bestätigung beim Schließen von Terminal-Tabs.", "info-backup": "Erstellt eine Sicherungskopie der Terminal-Installation.", "info-restore": "Stellt eine Sicherung der Terminal-Installation wieder her.", "info-uninstall": "Deinstalliert die Terminal-Installation.", "owned": "Eigene", "api_error": "API-Server nicht verfügbar, bitte nach kurzer Zeit nochmal versuchen.", "installed": "Installiert", "all": "Alle", "medium": "Mittel", "refund": "Erstattung", "product not available": "Produkt nicht verfügbar", "no-product-info": "Dieses Produkt ist in Ihrem Land zur Zeit nicht verfügbar, bitte versuchen Sie es später noch einmal.", "close": "Schließen", "explore": "Erkunden", "key bindings updated": "Tastenbelegung aktualisiert", "search in files": "In Dateien suchen", "exclude files": "Dateien ausschließen", "include files": "Dateien einschließen", "search result": "{matches} Ergebnisse in {files} Dateien.", "invalid regex": "Ungültiger regulärer Ausdruck: {message}.", "bottom": "Unten", "save all": "Alle speichern", "close all": "Alle schließen", "unsaved files warning": "Einige Dateien sind nicht gespeichert. Drücken Sie 'OK', um eine Aktion zu wählen, oder 'Abbrechen', um zurückzugehen.", "save all warning": "Wollen Sie wirklich alle Dateien speichern und schließen? Diese Aktion kann nicht zurückgenommen werden.", "save all changes warning": "Sind Sie sicher, das Sie alle Dateien speichern wollen?", "close all warning": "Wollen Sie wirklich alle Dateien schließen? Sie verlieren alle nicht gespeicherten Änderungen und diese Aktion kann nicht zurückgenommen werden.", "refresh": "Aktualisieren", "shortcut buttons": "Schnellwahl-Schaltflächen", "no result": "Kein Ergebnis", "searching...": "Suche ...", "quicktools:ctrl-key": "Steuerungstaste", "quicktools:tab-key": "Tab-Taste", "quicktools:shift-key": "Umschalttaste", "quicktools:undo": "Rückgängig", "quicktools:redo": "Wiederholen", "quicktools:search": "In Dateien suchen", "quicktools:save": "Datei speichern", "quicktools:esc-key": "Escape-Taste", "quicktools:curlybracket": "Geschweifte Klammer einfügen", "quicktools:squarebracket": "Eckige Klammer einfügen", "quicktools:parentheses": "Klammer einfügen", "quicktools:anglebracket": "Spitze Klammer einfügen", "quicktools:left-arrow-key": "Pfeiltaste nach links", "quicktools:right-arrow-key": "Pfeiltaste nach rechts", "quicktools:up-arrow-key": "Pfeiltaste nach oben", "quicktools:down-arrow-key": "Pfeiltaste nach unten", "quicktools:moveline-up": "Zeile nach oben verschieben", "quicktools:moveline-down": "Zeile nach unten verschieben", "quicktools:copyline-up": "Zeile nach oben kopieren", "quicktools:copyline-down": "Zeile nach unten kopieren", "quicktools:semicolon": "Semikolon einfügen", "quicktools:quotation": "Zitat einfügen", "quicktools:and": "Und-Symbol einfügen", "quicktools:bar": "Balken-Symbol einfügen", "quicktools:equal": "Gleichheitszeichen einfügen", "quicktools:slash": "Schrägstrich einfügen", "quicktools:exclamation": "Ausrufezeichen einfügen", "quicktools:alt-key": "Alt-Taste", "quicktools:meta-key": "Windows-Taste", "info-quicktoolssettings": "Anpassen der Schnellwahl-Schaltflächen und -Tasten im Schnelltools-Container unterhalb des Editors, um Ihre Programmiererfahrung zu verbessern.", "info-excludefolders": "Benutzen Sie das Muster **/node_modules/**, um alle Dateien im Ordner node_modules auszuschließen. Dadurch werden die Dateien nicht aufgelistet und auch nicht in die Dateisuche einbezogen.", "missed files": "Nach Beginn der Suche wurden {count} Dateien gescannt und werden nicht in die Suche einbezogen.", "remove": "Entfernen", "quicktools:command-palette": "Befehlspalette", "default file encoding": "Standard-Dateicodierung", "remove entry": "Sind Sie sicher, das Sie '{name}' von den Speicherpfaden entfernen möchten? Bitte beachten Sie, dass der Pfad selbst nicht gelöscht wird.", "delete entry": "Löschen bestätigen: '{name}'. Diese Aktion kann nicht zurückgenommen werden. Fortfahren?", "change encoding": "Neuladen von '{file}' mit '{encoding}'-Codierung? Bei dieser Aktion gehen alle ungespeicherten Änderungen an dieser Datei verloren. Wollen Sie mit dem Neuladen fortfahren?", "reopen file": "Sind Sie sicher, dass Sie '{file}' erneut öffnen wollen? Alle ungespeicherten Änderungen werden verloren gehen.", "plugin min version": "{name} ist nur in Acode - {v-code} und neuer verfügbar. Klicken Sie hier zum Aktualisieren.", "color preview": "Farbvorschau", "confirm": "Bestätigen", "list files": "Alle Dateien in {name} auflisten? Zu viele Dateien können zum Absturz der App führen.", "problems": "Probleme", "show side buttons": "Seitenknöpfe anzeigen", "bug_report": "Einen Fehlerbericht übermitteln", "verified publisher": "Verifizierter Herausgeber", "most_downloaded": "Meist Heruntergeladene", "newly_added": "Neu Hinzugefügte", "top_rated": "Am besten Bewertete", "rename not supported": "Umbenennen des Termux-Verzeichnisses wird nicht unterstützt", "compress": "Komprimieren", "copy uri": "URI kopieren", "delete entries": "Sind Sie sicher, dass Sie {count} Einträge löschen wollen?", "deleting items": "Löschen von {count} Einträgen ...", "import project zip": "Projekt(-Zip) importieren", "changelog": "Änderungsprotokoll", "notifications": "Benachrichtigungen", "no_unread_notifications": "Keine ungelesenen Benachrichtigungen", "should_use_current_file_for_preview": "Aktuelle Datei für die Vorschau anstelle der Standardeinstellung (index.html) verwenden", "fade fold widgets": "Widgets falten ausblenden", "quicktools:home-key": "Pos 1-Taste", "quicktools:end-key": "Ende-Taste", "quicktools:pageup-key": "Bild auf-Taste", "quicktools:pagedown-key": "Bild ab-Taste", "quicktools:delete-key": "Entfernen-Taste", "quicktools:tilde": "Tilde einfügen", "quicktools:backtick": "Accent grave einfügen", "quicktools:hash": "Raute einfügen", "quicktools:dollar": "Dollar einfügen", "quicktools:modulo": "Prozentzeichen einfügen", "quicktools:caret": "Caret einfügen", "plugin_enabled": "Plugin aktiviert", "plugin_disabled": "Plugin deaktiviert", "enable_plugin": "Dieses Plugin aktivieren", "disable_plugin": "Dieses Plugin deaktivieren", "open_source": "Open Source", "terminal settings": "Terminal-Einstellungen", "font ligatures": "Schriftligaturen", "letter spacing": "Zeichenabstand", "terminal:tab stop width": "Tabulatorbreite", "terminal:scrollback": "Zeilen zurückblättern", "terminal:cursor blink": "Blinkender Cursor", "terminal:font weight": "Schriftstärke", "terminal:cursor inactive style": "Cursorstil inaktiv", "terminal:cursor style": "Cursorstil", "terminal:font family": "Schriftfamilie", "terminal:convert eol": "Zeilenende konvertieren", "terminal:confirm tab close": "Terminal-Tab schließen bestätigen", "terminal:image support": "Bildunterstützung", "terminal": "Terminal", "allFileAccess": "Zugriff auf alle Dateien", "fonts": "Schriftarten", "sponsor": "Sponsor", "downloads": "Downloads", "reviews": "Bewertungen", "overview": "Überblick", "contributors": "Mitwirkende", "quicktools:hyphen": "Bindestrich einfügen", "check for app updates": "Auf App-Updates prüfen", "prompt update check consent message": "Acode kann nach neuen App-Updates suchen, wenn Sie online sind. Update-Prüfungen aktivieren?", "keywords": "Schlüsselwörter", "author": "Autor", "filtered by": "Gefiltert nach", "clean install state": "Sauberer Installationsstatus", "backup created": "Sicherung erstellt", "restore completed": "Wiederherstellung abgeschlossen", "restore will include": "Dies wird wiederhergestellt", "restore warning": "Dieser Vorgang kann nicht rückgängig gemacht werden. Fortfahren?", "reload to apply": "Neu laden, um Änderungen zu übernehmen?", "reload app": "App neu laden", "preparing backup": "Sicherung vorbereiten", "collecting settings": "Einstellungen sammeln", "collecting key bindings": "Tastenbelegung sammeln", "collecting plugins": "Plugin-Informationen sammeln", "creating backup": "Sicherungsdatei erstellen", "validating backup": "Sicherung überprüfen", "restoring key bindings": "Tastenbelegung wiederherstellen", "restoring plugins": "Plugins wiederherstellen", "restoring settings": "Einstellungen wiederherstellen", "legacy backup warning": "Dies ist ein älteres Sicherungsformat. Einige Funktionen sind möglicherweise eingeschränkt.", "checksum mismatch": "Prüfsummenfehler – die Sicherungsdatei wurde möglicherweise geändert oder ist beschädigt.", "plugin not found": "Plugin in der Registrierung nicht gefunden", "paid plugin skipped": "Bezahltes Plugin – Kauf nicht gefunden", "source not found": "Quelldatei existiert nicht mehr", "restored": "Wiederhergestellt", "skipped": "Übersprungen", "backup not valid object": "Die Sicherungsdatei ist kein gültiges Objekt.", "backup no data": "Die Sicherungsdatei enthält keine wiederherzustellenden Daten.", "backup legacy warning": "Dies ist ein älteres Sicherungsformat (v1). Einige Funktionen sind möglicherweise eingeschränkt.", "backup missing metadata": "Fehlende Sicherungsmetadaten – einige Informationen sind möglicherweise nicht verfügbar.", "backup checksum mismatch": "Prüfsummenfehler – die Sicherungsdatei wurde möglicherweise geändert oder ist beschädigt. Gehen Sie vorsichtig vor.", "backup checksum verify failed": "Prüfsumme konnte nicht überprüft werden.", "backup invalid settings": "Ungültiges Einstellungsformat", "backup invalid keybindings": "Ungültiges Tastenbelegungsformat", "backup invalid plugins": "Ungültiges Format der installierten Plugins", "issues found": "Gefundene Probleme", "error details": "Fehlerdetails", "active tools": "Aktive Tools", "available tools": "Verfügbare Tools", "recent": "Zuletzt verwendete Dateien", "command palette": "Befehlspalette öffnen", "change theme": "Design ändern", "documentation": "Dokumentation", "open in terminal": "Im Terminal öffnen", "developer mode": "Entwicklermodus", "info-developermode": "Aktivieren Sie die Entwicklertools (Eruda) zum Debuggen von Plugins und Überprüfen des App-Status. Der Inspektor wird beim Start der App initialisiert.", "developer mode enabled": "Entwicklermodus aktiviert. Verwenden Sie die Befehlspalette, um den Inspektor umzuschalten (Strg+Umschalt+I).", "developer mode disabled": "Entwicklermodus deaktiviert", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/en-us.json ================================================ { "lang": "English", "about": "About", "active files": "Active files", "alert": "Alert", "app theme": "App theme", "autocorrect": "Enable autocorrect?", "autosave": "Autosave", "cancel": "Cancel", "change language": "Change language", "choose color": "Choose color", "clear": "clear", "close app": "Close the application?", "commit message": "Commit message", "console": "Console", "conflict error": "Conflict! Please wait before another commit.", "copy": "Copy", "create folder error": "Sorry, unable create new folder", "cut": "Cut", "delete": "Delete", "dependencies": "Dependencies", "delay": "Time in milliseconds", "editor settings": "Editor settings", "editor theme": "Editor theme", "enter file name": "Enter file name", "enter folder name": "Enter folder name", "empty folder message": "Empty Folder", "enter line number": "Enter line number", "error": "Error", "failed": "Failed", "file already exists": "File already exists", "file already exists force": "File already exists. Overwrite?", "file changed": " has been changed, reload file?", "file deleted": "File deleted", "file is not supported": "File is not supported", "file not supported": "This file type is not supported.", "file too large": "File is to large to handle. Max file size allowed is {size}", "file renamed": "file renamed", "file saved": "file saved", "folder added": "folder added", "folder already added": "folder already added", "font size": "Font size", "goto": "Goto line", "icons definition": "Icons definition", "info": "Info", "invalid value": "Invalid value", "language changed": "language has been changed successfully", "linting": "Check syntax error", "logout": "Logout", "loading": "Loading", "my profile": "My profile", "new file": "New file", "new folder": "New Folder", "no": "No", "no editor message": "Open or create new file and folder from menu", "not set": "Not set", "unsaved files close app": "There are unsaved files. Close application?", "notice": "Notice", "open file": "Open file", "open files and folders": "Open files and folders", "open folder": "Open folder", "open recent": "Open recent", "ok": "ok", "overwrite": "Overwrite", "paste": "Paste", "preview mode": "Preview mode", "read only file": "Cannot save read only file. Please try save as", "reload": "Reload", "rename": "Rename", "replace": "Replace", "required": "This field is required", "run your web app": "Run your web app", "save": "Save", "saving": "Saving", "save as": "Save as", "save file to run": "Please save this file to run in browser", "search": "Search", "see logs and errors": "See logs and errors", "select folder": "Select folder", "settings": "Settings", "settings saved": "Settings saved", "show line numbers": "Show line numbers", "show hidden files": "Show hidden files", "show spaces": "Show spaces", "soft tab": "Soft tab", "sort by name": "Sort by name", "success": "Success", "tab size": "Tab size", "text wrap": "Text wrap / Word wrap", "theme": "Theme", "unable to delete file": "unable to delete file", "unable to open file": "Sorry, unable to open file", "unable to open folder": "Sorry, unable to open folder", "unable to save file": "Sorry, unable to save file", "unable to rename": "Sorry, unable to rename", "unsaved file": "This file is not saved, close anyway?", "warning": "Warning", "use emmet": "Use emmet", "use quick tools": "Use quick tools", "yes": "Yes", "encoding": "Text encoding", "syntax highlighting": "Syntax highlighting", "read only": "Read only", "select all": "Select all", "select branch": "Select branch", "create new branch": "Create new branch", "use branch": "Use branch", "new branch": "New branch", "branch": "Branch", "key bindings": "Key bindings", "edit": "Edit", "reset": "Reset", "color": "Color", "select word": "Select word", "quick tools": "Quick tools", "select": "Select", "editor font": "Editor font", "new project": "New project", "format": "Format", "project name": "Project name", "unsupported device": "Your device does not support theme.", "vibrate on tap": "Vibrate on tap", "copy command is not supported by ftp.": "Copy command is not supported by FTP.", "support title": "Support Acode", "fullscreen": "Fullscreen", "animation": "Animation", "backup": "Backup", "restore": "Restore", "backup successful": "Backup successful", "invalid backup file": "Invalid backup file", "add path": "Add path", "live autocompletion": "Live autocompletion", "file properties": "File properties", "path": "Path", "type": "Type", "word count": "Word count", "line count": "Line count", "last modified": "Last modified", "size": "Size", "share": "Share", "show print margin": "Show print margin", "login": "Login", "scrollbar size": "Scrollbar size", "cursor controller size": "Cursor controller size", "none": "None", "small": "Small", "large": "Large", "floating button": "Floating button", "confirm on exit": "Confirm on exit", "show console": "Show console", "image": "Image", "insert file": "Insert file", "insert color": "Insert color", "powersave mode warning": "Turn off power saving mode to preview in external browser.", "exit": "Exit", "custom": "Custom", "reset warning": "Are you sure you want to reset theme?", "theme type": "Theme type", "light": "Light", "dark": "Dark", "file browser": "File Browser", "operation not permitted": "Operation not permitted", "no such file or directory": "No such file or directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Not a directory", "is a directory": "Is a directory", "invalid argument": "Invalid argument", "too many open files in system": "Too many open files in system", "too many open files": "Too many open files", "text file busy": "Text file busy", "no space left on device": "No space left on device", "read-only file system": "Read-only file system", "file name too long": "File name too long", "too many users": "Too many users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "An error occurred", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "Save file", "save file as": "Save file as", "files": "Files", "help": "Help", "file has been deleted": "{file} has been deleted!", "feature not available": "This feature is only available in paid version of the app.", "deleted file": "Deleted file", "line height": "Line height", "preview info": "If you want run the active file, tap and hold on play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "Close file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Open in browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "Rate Acode", "support": "Support", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme, installed plugins and key bindings. It will not backup your FTP/SFTP or App state.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails.", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "These settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Sponsor", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "available tools": "Available tools", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-custom-server-removed": "Custom server removed", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface." } ================================================ FILE: src/lang/es-sv.json ================================================ { "lang": "Español (by DouZerr)", "about": "Información", "active files": "Archivos Activos", "alert": "Alerta", "app theme": "Tema de App", "autocorrect": "¿Habilitar Autocorrección?", "autosave": "Guardar Automáticamente", "cancel": "Cancelar", "change language": "Cambiar Idioma", "choose color": "Elegir color", "clear": "Limpiar", "close app": "¿Cerrar la Aplicación?", "commit message": "Commitear mensaje", "console": "Consola", "conflict error": "¡Conflicto! Por favor espera antes de otro commit.", "copy": "Copiar", "create folder error": "Lo sentimos, no se puede crear una nueva carpeta", "cut": "Cortar", "delete": "Borrar", "dependencies": "Dependencias", "delay": "Tiempo en milisegundos", "editor settings": "Ajustes del Editor", "editor theme": "Tema del Editor", "enter file name": "Ingrese Nombre del Archivo", "enter folder name": "Ingrese Nombre de la Carpeta", "empty folder message": "Carpeta Vacía", "enter line number": "Ingrese Número de Línea", "error": "Error", "failed": "Ha Fallado", "file already exists": "El archivo ya existe", "file already exists force": "El archivo ya existe. ¿Sobrescribir?", "file changed": " ha sido cambiado, recargar archivo?", "file deleted": "Archivo Borrado", "file is not supported": "El archivo no es compatible", "file not supported": "Este tipo de archivo no es compatible.", "file too large": "El archivo es demasiado grande para manejarlo. El tamaño máximo de archivo permitido es {size}", "file renamed": "Archivo renombrado", "file saved": "Archivo guardado", "folder added": "Carpeta agregada", "folder already added": "Carpeta ya agregada", "font size": "Tamaño de Fuente", "goto": "Ir a la Línea", "icons definition": "Definición de iconos", "info": "info", "invalid value": "Valor Inválido", "language changed": "Idioma cambiado exitosamente", "linting": "Comprobar error de sintaxis", "logout": "Cerrar Sesión", "loading": "Cargando", "my profile": "Mi Perfil", "new file": "Nuevo Archivo", "new folder": "Nueva Carpeta", "no": "No", "no editor message": "Abrir o crear un nuevo archivo y carpeta desde el menú", "not set": "No Establecido", "unsaved files close app": "Existen archivos sin guardar. ¿Cerrar la aplicación?", "notice": "Aviso", "open file": "Abrir Archivo", "open files and folders": "Abrir archivos y carpetas", "open folder": "Abrir Carpeta", "open recent": "Abrir Recientes", "ok": "Aceptar", "overwrite": "Sobrescribir", "paste": "Pegar", "preview mode": "Modo de vista previa", "read only file": "No se puede guardar un archivo de solo lectura. Por favor intenta guardar como", "reload": "Recargar", "rename": "Renombrar", "replace": "Reemplazar", "required": "Este campo es requerido", "run your web app": "Ejecute su aplicación web", "save": "Guardar", "saving": "Guardando", "save as": "Guardar como", "save file to run": "Guardar archivo para ejecutar en el navegador", "search": "Buscar", "see logs and errors": "Ver registros y errores", "select folder": "Seleccionar Carpeta", "settings": "Ajustes", "settings saved": "Ajustes guardados", "show line numbers": "Mostrar números de línea", "show hidden files": "Mostrar archivos ocultos", "show spaces": "Mostrar espacios", "soft tab": "Pestaña suave", "sort by name": "Ordenar por nombre", "success": "Éxito", "tab size": "Tamaño de pestaña", "text wrap": "Ajuste de línea", "theme": "Tema", "unable to delete file": "no se puede eliminar el archivo", "unable to open file": "Lo sentimos, no se puede abrir el archivo", "unable to open folder": "Lo sentimos, no se puede abrir la carpeta", "unable to save file": "Lo sentimos, no se puede guardar el archivo", "unable to rename": "Lo sentimos, no se puede cambiar el nombre", "unsaved file": "Este archivo no se guardado, ¿cerrar de todos modos?", "warning": "Advertencia", "use emmet": "Usar emmet", "use quick tools": "Usa herramientas rápidas", "yes": "Si", "encoding": "Codificación de Texto", "syntax highlighting": "Resaltado de sintaxis", "read only": "Solo lectura", "select all": "Seleccionar todo", "select branch": "Seleccione rama", "create new branch": "Crear nueva rama", "use branch": "Usar rama", "new branch": "Nueva rama", "branch": "Rama", "key bindings": "Atajos de teclado", "edit": "Editar", "reset": "Reiniciar", "color": "Color", "select word": "Seleccionar palabra", "quick tools": "Herramientas rápidas", "select": "Seleccionar", "editor font": "Fuente del editor", "new project": "Nuevo Proyecto", "format": "Formato", "project name": "Nombre del Proyecto", "unsupported device": "Su dispositivo no es compatible con el tema.", "vibrate on tap": "Vibrar al tocar", "copy command is not supported by ftp.": "Comando de copia no es compatible con FTP.", "support title": "Apoye Acode", "fullscreen": "pantalla completa", "animation": "animación", "backup": "respaldo", "restore": "restaurar", "backup successful": "Respaldo exitoso", "invalid backup file": "Archivo de respaldo inválido", "add path": "Añadir dirección", "live autocompletion": "Autocompletado en vivo", "file properties": "Propiedades del archivo", "path": "Dirección", "type": "Tipo", "word count": "Conteo de palabras", "line count": "Conteo de líneas", "last modified": "Última modificación", "size": "Tamaño", "share": "Compartir", "show print margin": "Mostrar margen de impresión", "login": "Iniciar Sesión", "scrollbar size": "Tamaño de barra de scroll", "cursor controller size": "Tamaño del controlador de cursor", "none": "ninguno", "small": "pequeño", "large": "grande", "floating button": "Botón flotante", "confirm on exit": "Confirmar al salir", "show console": "Mostrar consola", "image": "Imagen", "insert file": "Insertar archivo", "insert color": "Insertar color", "powersave mode warning": "Desactive el modo de ahorro de energía para obtener una vista previa en un navegador externo", "exit": "Salir", "custom": "personalizado", "reset warning": "¿Seguro que quieres restablecer el tema?", "theme type": "Tipo de tema", "light": "claro", "dark": "oscuro", "file browser": "Explorador de archivos", "operation not permitted": "Operación no permitida", "no such file or directory": "El fichero o directorio no existe", "input/output error": "Error de entrada/salida", "permission denied": "Permiso denegado", "bad address": "Dirección incorrecta", "file exists": "El archivo ya existe", "not a directory": "No es un directorio", "is a directory": "Es un directorio", "invalid argument": "Argumento no válido", "too many open files in system": "Demasiados archivos abiertos en el sistema", "too many open files": "Demasiados archivos abiertos", "text file busy": "Archivo de texto ocupado", "no space left on device": "No queda espacio en el dispositivo", "read-only file system": "Sistema de archivos de sólo lectura", "file name too long": "Nombre de archivo demasiado largo", "too many users": "Demasiados usuarios", "connection timed out": "Tiempo de conexión agotado", "connection refused": "Conexión denegada", "owner died": "El propietario murio", "an error occurred": "Ocurrió un error", "add ftp": "Añadir FTP", "add sftp": "Añadir SFTP", "save file": "Guardar el archivo", "save file as": "Guardar archivo como", "files": "Archivos", "help": "Ayuda", "file has been deleted": "{file} ha sido eliminado!", "feature not available": "Esta función solo está disponible en la versión de pago de la aplicación.", "deleted file": "Archivo eliminado", "line height": "Altura de la línea", "preview info": "Si desea ejecutar el archivo activo, toque y mantenga presionado el ícono de reproducción.", "manage all files": "Permita que Acode editor administre todos los archivos en la configuración para editar archivos en su dispositivo fácilmente.", "close file": "Cerrar el archivo", "reset connections": "Restablecer conexiones", "check file changes": "Comprobar cambios en el archivo", "open in browser": "Abrir en el navegador", "desktop mode": "Modo escritorio", "toggle console": "Abrir/Cerrar la consola", "new line mode": "Nuevo modo de línea", "add a storage": "Añadir un almacenamiento", "rate acode": "Califica Acode", "support": "Apoyar", "downloading file": "Descargando {file}", "downloading...": "Descargando...", "folder name": "Nombre de la carpeta", "keyboard mode": "Modo de teclado", "normal": "Normal", "app settings": "Ajustes de aplicacion", "disable in-app-browser caching": "Desactivar el almacenamiento en caché de la aplicación en el navegador", "copied to clipboard": "Copiado al portapapeles", "remember opened files": "Recordar archivos abiertos", "remember opened folders": "Recordar carpetas abiertas", "no suggestions": "No hay sugerencias", "no suggestions aggressive": "Ninguna sugerencia, agresivo", "install": "Instalar", "installing": "Instalando...", "plugins": "Extensiones", "recently used": "Usado recientemente", "update": "Actualizar", "uninstall": "Desinstalar", "download acode pro": "Descargar Acode Pro", "loading plugins": "Cargando extensiones", "faqs": "FAQs", "feedback": "Comentarios", "header": "Cabecera", "sidebar": "Barra lateral", "inapp": "Inapp", "browser": "Navegador", "diagonal scrolling": "Desplazamiento en diagonal", "reverse scrolling": "Desplazamiento inverso", "formatter": "Formateador", "format on save": "Formatear al guardar", "remove ads": "Eliminar anuncios", "fast": "Rápido", "slow": "Lento", "scroll settings": "Ajustes de desplazamiento", "scroll speed": "Velocidad de desplazamiento", "loading...": "Cargando...", "no plugins found": "No se han encontrado extensiones", "name": "Nombre", "username": "Usuario", "optional": "opcional", "hostname": "Nombre del host", "password": "Contraseña", "security type": "Tipo de seguridad", "connection mode": "Modo de conexión", "port": "Puerto", "key file": "Archivo de claves", "select key file": "Seleccionar archivo clave", "passphrase": "Frase de acceso", "connecting...": "Conectando...", "type filename": "Escriba el nombre del archivo", "unable to load files": "No se pueden cargar archivos", "preview port": "Puerto de previsualización", "find file": "Buscar archivo", "system": "Sistema", "please select a formatter": "Seleccione un formateador", "case sensitive": "Distingue entre mayúsculas y minúsculas", "regular expression": "Expresión regular", "whole word": "Palabra completa", "edit with": "Editar con", "open with": "Abrir con", "no app found to handle this file": "No se ha encontrado ninguna aplicación que gestione este archivo", "restore default settings": "Restablecer la configuración predeterminada", "server port": "Puerto del servidor", "preview settings": "Ajustes de previsualización", "preview settings note": "Si el puerto de previsualización y el puerto del servidor son diferentes, la aplicación no arrancará el servidor y en su lugar abrirá https://: en el navegador o en el navegador de la aplicación. Esto es útil cuando se está ejecutando un servidor en otro lugar.", "backup/restore note": "Sólo hará una copia de seguridad de tu configuración, tema personalizado y atajos de teclado. No hará copia de seguridad de su FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Reintentar ftp/sftp cuando falla", "more": "Más", "thank you :)": "Gracias :)", "purchase pending": "compra pendiente", "cancelled": "cancelado", "local": "Local", "remote": "Remoto", "show console toggler": "Mostrar selector de consola", "binary file": "Este archivo contiene datos binarios, ¿desea abrirlo?", "relative line numbers": "Números de línea relativos", "elastic tabstops": "Tabuladores elásticos", "line based rtl switching": "Conmutación RTL basada en líneas", "hard wrap": "ajuste de línea rígido", "spellcheck": "Corrector ortográfico", "wrap method": "Método de ajuste de línea", "use textarea for ime": "Utilizar área de texto para IME", "invalid plugin": "Extensión inválida", "type command": "Escriba el comando", "plugin": "Extensión", "quicktools trigger mode": "Modo de activación de herramientas rápidas", "print margin": "Margen de impresión", "touch move threshold": "Umbral de movimiento táctil", "info-retryremotefsafterfail": "Reintentar la conexión FTP/SFTP cuando falle", "info-fullscreen": "Ocultar la barra de título en la pantalla de inicio.", "info-checkfiles": "Comprueba los cambios en los archivos cuando la aplicación esté en segundo plano.", "info-console": "Elija la consola JavaScript. Legacy es la consola por defecto, Eruda es una consola de terceros adicional.", "info-keyboardmode": "Modo de teclado para entrada de texto, 'sin sugerencias' ocultará las sugerencias y la autocorrección. Si 'sin sugerencias' no funciona, trate de cambiar el valor a 'ninguna sugerencia, agresivo'.", "info-rememberfiles": "Recordar los archivos abiertos al cerrar la aplicación.", "info-rememberfolders": "Recordar las carpetas abiertas al cerrar la aplicación.", "info-floatingbutton": "Mostrar u ocultar el botón flotante de herramientas rápidas.", "info-openfilelistpos": "Dónde mostrar la lista de archivos activos.", "info-touchmovethreshold": "Si la sensibilidad táctil de tu dispositivo es demasiado alta, puedes aumentar este valor para evitar movimientos táctiles accidentales.", "info-scroll-settings": "Esta configuración contiene los ajustes de desplazamiento, incluido el ajuste de línea de texto.", "info-animation": "Si la aplicación va lenta, desactiva la animación.", "info-quicktoolstriggermode": "Si el botón de las herramientas rápidas no funciona, intente cambiar este valor.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "En propiedad", "api_error": "El servidor API no funciona, inténtelo después de un rato.", "installed": "Instalado", "all": "Todo", "medium": "Medio", "refund": "Reembolso", "product not available": "Producto no disponible", "no-product-info": "Este producto no está disponible en su país en este momento, por favor inténtelo más tarde.", "close": "Cerrar", "explore": "Explorar", "key bindings updated": "Atajos de teclado actualizados", "search in files": "Buscar en archivos", "exclude files": "Excluir archivos", "include files": "Incluir archivos", "search result": "{matches} resulta en {files} archivos.", "invalid regex": "Expresión regular inválida: {message}.", "bottom": "Parte inferior", "save all": "Guardar todo", "close all": "Cerrar todo", "unsaved files warning": "Algunos archivos no se han guardado. Pulsa 'Aceptar' para decidir qué hacer o 'Cancelar' para volver atrás.", "save all warning": "¿Estás seguro de que quieres guardar todos los archivos y cerrar? Esta acción no se puede revertir.", "save all changes warning": "¿Estás seguro de que quieres guardar todos los archivos?", "close all warning": "¿Estás seguro de que quieres cerrar todos los archivos? Perderá los cambios no guardados y esta acción no se puede revertir.", "refresh": "Actualizar", "shortcut buttons": "Botones de acceso rápido", "no result": "Sin resultado", "searching...": "Buscando...", "quicktools:ctrl-key": "Tecla Control/Comando", "quicktools:tab-key": "Tecla Tabulador", "quicktools:shift-key": "Tecla Mayús", "quicktools:undo": "Deshacer", "quicktools:redo": "Rehacer", "quicktools:search": "Buscar en archivo", "quicktools:save": "Guardar archivo", "quicktools:esc-key": "Tecla Escape", "quicktools:curlybracket": "Insertar llave", "quicktools:squarebracket": "Insertar corchete", "quicktools:parentheses": "Insertar paréntesis", "quicktools:anglebracket": "Insertar signo mayor que/menor que", "quicktools:left-arrow-key": "Tecla flecha izquierda", "quicktools:right-arrow-key": "Tecla flecha derecha", "quicktools:up-arrow-key": "Tecla flecha arriba", "quicktools:down-arrow-key": "Tecla flecha abajo", "quicktools:moveline-up": "Mover la línea hacia arriba", "quicktools:moveline-down": "Mover la línea hacia abajo", "quicktools:copyline-up": "Copiar la línea hacia arriba", "quicktools:copyline-down": "Copiar la línea hacia abajo", "quicktools:semicolon": "Insertar punto y coma", "quicktools:quotation": "Insertar comillas", "quicktools:and": "Insertar símbolo ampersand", "quicktools:bar": "Insertar símbolo barra vertical", "quicktools:equal": "Insertar símbolo igual", "quicktools:slash": "Insertar símbolo barra oblicua", "quicktools:exclamation": "Insertar exclamación", "quicktools:alt-key": "Tecla Alt", "quicktools:meta-key": "Tecla Windows", "info-quicktoolssettings": "Personalice los botones de acceso rápido y las teclas del teclado en el contenedor de herramientas rápidas situado debajo del editor para mejorar su experiencia de codificación.", "info-excludefolders": "Utilice el patrón **/node_modules/** para ignorar todos los archivos de la carpeta node_modules. Esto excluirá los archivos de la lista y también evitará que se incluyan en las búsquedas de archivos.", "missed files": "Se han escaneado {count} archivos después de iniciarse la búsqueda y no se incluirán en ella.", "remove": "Eliminar", "quicktools:command-palette": "Paleta de comandos", "default file encoding": "Codificación de archivo por defecto", "remove entry": "¿Estás seguro de que quieres eliminar '{name}' de las rutas guardadas? Tenga en cuenta que al eliminarlo no se borrará la ruta en sí.", "delete entry": "Confirmar eliminación: '{name}'. Esta acción no se puede deshacer. ¿Continuar?", "change encoding": "¿Reabrir '{file}' con codificación '{encoding}'? Esta acción provocará la pérdida de cualquier cambio no guardado realizado en el archivo. ¿Desea continuar con la reapertura?", "reopen file": "¿Estás seguro de que quieres reabrir '{file}'? Cualquier cambio no guardado se perderá.", "plugin min version": "{name} sólo disponible en Acode - {v-code} y superior. Haga clic aquí para actualizar.", "color preview": "Vista previa del color", "confirm": "Confirmar", "list files": "¿Listar todos los archivos en {name}? Demasiados archivos pueden bloquear la aplicación.", "problems": "Problemas", "show side buttons": "Mostrar botones laterales", "bug_report": "Submit a Bug Report", "verified publisher": "Publicador verificado", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Patrocinador", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/fr-fr.json ================================================ { "lang": "Français", "about": "À propos", "active files": "Fichiers ouverts", "alert": "Alerte", "app theme": "Thème de l'application", "autocorrect": "Activer la correction automatique ?", "autosave": "Sauvegarde automatique", "cancel": "Annuler", "change language": "Changer de langue", "choose color": "Choisir une couleur ", "clear": "Effacer", "close app": "Fermer l'application ?", "commit message": "Message de commit", "console": "Console", "conflict error": "Conflit ! Veuillez attendre avant de procéder à un autre commit.", "copy": "Copier", "create folder error": "Désolé, impossible de créer un nouveau dossier", "cut": "Couper", "delete": "Effacer", "dependencies": "Dépendances", "delay": "Temps en millisecondes", "editor settings": "Paramètres de l'éditeur", "editor theme": "Thème de l'éditeur", "enter file name": "Entrer un nom de fichier", "enter folder name": "Entrer un nom de dossier", "empty folder message": "Dossier vide", "enter line number": "Entrer le numéro de ligne", "error": "Erreur", "failed": "Échec", "file already exists": "Le fichier existe déjà", "file already exists force": "Le fichier existe déjà. L'écraser ?", "file changed": " a été modifié, recharger le fichier ?", "file deleted": "Fichier effacé", "file is not supported": "Fichier non supporté", "file not supported": "Type de fichier non supporté.", "file too large": "Le fichier est trop volumineux. La taille maximale autorisée est {size}", "file renamed": "Fichier renommé", "file saved": "Fichier sauvegardé", "folder added": "Dossier ajouté", "folder already added": "Dossier déjà ajouté", "font size": "Taille de la police", "goto": "Aller à ligne", "icons definition": "Définition des icônes", "info": "Info", "invalid value": "Valeur invalide", "language changed": "La langue a été changée avec succès", "linting": "Vérifier les erreurs de syntaxe ?", "logout": "Se déconnecter", "loading": "Chargement", "my profile": "Mon profil", "new file": "Nouveau fichier", "new folder": "Nouveau dossier", "no": "Non", "no editor message": "Ouvrir ou créer un nouveau fichier ou dossier depuis le menu", "not set": "Non défini", "unsaved files close app": "Il existe des fichiers non sauvegardés. Fermer l'application ?", "notice": "Avis", "open file": "Ouvrir un fichier", "open files and folders": "Ouvrir les fichiers et dossiers", "open folder": "Ouvrir un dossier", "open recent": "Récent", "ok": "OK", "overwrite": "Écraser", "paste": "Coller", "preview mode": "Mode d'aperçu", "read only file": "Sauvegarde impossible, fichier en lecture seule. Essayez d'enregistrer sous un autre nom", "reload": "Recharger", "rename": "Renommer", "replace": "Remplacer", "required": "Champ requis", "run your web app": "Lancer votre application web", "save": "Enregistrer", "saving": "Enregistrement", "save as": "Enregistrer sous", "save file to run": "Veuillez enregistrer ce fichier pour l'exécuter dans le navigateur", "search": "Recherche", "see logs and errors": "Voir les journaux et les erreurs", "select folder": "Choisir un dossier", "settings": "Paramètres", "settings saved": "Paramètres enregistrés", "show line numbers": "Afficher les numéros de ligne", "show hidden files": "Afficher les fichiers cachés", "show spaces": "Afficher les espaces", "soft tab": "Tabulation légère", "sort by name": "Trier par nom", "success": "Succès", "tab size": "Taille de tabulation", "text wrap": "Lignes longues sur plusieurs lignes", "theme": "Thème", "unable to delete file": "Impossible de supprimer le fichier", "unable to open file": "Désolé, impossible d'ouvrir le fichier", "unable to open folder": "Désolé, impossible d'ouvrir le dossier", "unable to save file": "Désolé, impossible d'enregistrer le fichier", "unable to rename": "Désolé, impossible de renommer", "unsaved file": "Ce fichier n'a pas été sauvegardé, le fermer quand même ?", "warning": "Avertissement", "use emmet": "Utiliser emmet", "use quick tools": "Utiliser les outils rapides", "yes": "Oui", "encoding": "Encodage du texte", "syntax highlighting": "Coloration syntaxique", "read only": "Lecture seule", "select all": "Tout sélectionner", "select branch": "Sélectionner une branche", "create new branch": "Créer une nouvelle branche", "use branch": "Utiliser une branche", "new branch": "Nouvelle branche", "branch": "Branche", "key bindings": "Raccourcis", "edit": "Modifier", "reset": "Réinitialiser", "color": "Couleur", "select word": "Sélectionner un mot", "quick tools": "Outils rapides", "select": "Sélectionner", "editor font": "Police de l'éditeur", "new project": "Nouveau projet", "format": "Format", "project name": "Nom du projet", "unsupported device": "Votre appareil ne prend pas en charge ce thème.", "vibrate on tap": "Vibrer au toucher", "copy command is not supported by ftp.": "La copie n'est pas supportée en FTP.", "support title": "Soutenir Acode", "fullscreen": "Plein écran", "animation": "Animation", "backup": "Sauvegarder", "restore": "Restaurer", "backup successful": "Sauvegarde faite avec succès !", "invalid backup file": "Fichier de sauvegarde invalide", "add path": "Ajouter un chemin d'accès", "live autocompletion": "Correction automatique", "file properties": "Propriétés du fichier", "path": "Chemin", "type": "Type", "word count": "Nombre de mots", "line count": "Nombre de lignes", "last modified": "Modifié pour la dernière fois le", "size": "Taille", "share": "Partager", "show print margin": "Afficher les marges d'impression", "login": "Se connecter", "scrollbar size": "Taille de la barre de défilement", "cursor controller size": "Taille du curseur de contrôle", "none": "Aucun", "small": "Petit", "large": "Grand", "floating button": "Bouton flottant", "confirm on exit": "Confirmer pour quitter", "show console": "Afficher la console", "image": "Image", "insert file": "Insérer un fichier", "insert color": "Insérer une couleur", "powersave mode warning": "Désactiver le mode d'économie d'énergie pour l'aperçu dans un navigateur externe.", "exit": "Quitter", "custom": "Personnalisé", "reset warning": "Voulez-vous vraiment réinitialiser le thème ?", "theme type": "Type de thème", "light": "Clair", "dark": "Sombre", "file browser": "Gestionnaire de fichiers", "operation not permitted": "Opération non autorisée", "no such file or directory": "Aucun fichier ou répertoire ne porte ce nom", "input/output error": "Erreur d'entrée/sortie", "permission denied": "Autorisation refusée", "bad address": "Mauvaise adresse", "file exists": "Le fichier existe", "not a directory": "N'est pas un répertoire", "is a directory": "Est un répertoire", "invalid argument": "Argument invalide", "too many open files in system": "Trop de fichiers ouverts dans le système", "too many open files": "Trop de fichiers ouverts", "text file busy": "Fichier texte occupé", "no space left on device": "Pas d'espace libre disponible sur le périphérique", "read-only file system": "Système de fichiers en lecture seule", "file name too long": "Nom de fichier trop long", "too many users": "Trop d'utilisateurs", "connection timed out": "La connexion a expiré", "connection refused": "Connexion rejetée", "owner died": "Le propriétaire a disparu", "an error occurred": "Une erreur s'est produite", "add ftp": "Ajouter FTP", "add sftp": "Ajouter SFTP", "save file": "Enregistrer le fichier", "save file as": "Enregistrer le fichier sous", "files": "Dossiers", "help": "Aide", "file has been deleted": "{file} a été supprimé !", "feature not available": "Cette fonctionnalité n'est disponible que dans la version payante de l'application.", "deleted file": "Fichier supprimé", "line height": "Hauteur de ligne", "preview info": "Si vous voulez exécuter le fichier actif, appuyez longuement sur l'icône de lecture.", "manage all files": "Autorisez l'éditeur Acode à gérer tous les fichiers dans les paramètres pour modifier facilement les fichiers sur votre appareil.", "close file": "Fermer le fichier", "reset connections": "Réinitialiser les connexions", "check file changes": "Vérifier les modifications du fichier", "open in browser": "Ouvrir dans le navigateur", "desktop mode": "Mode bureau", "toggle console": "Activer/désactiver la console", "new line mode": "Mode nouvelle ligne", "add a storage": "Ajouter un stockage", "rate acode": "Évaluer Acode", "support": "Soutenir", "downloading file": "Téléchargement de {file}", "downloading...": "Téléchargement...", "folder name": "Nom du dossier", "keyboard mode": "Mode de saisie", "normal": "Normal", "app settings": "Paramètres de l'application", "disable in-app-browser caching": "Désactiver la mise en cache dans le navigateur de l'application", "copied to clipboard": "Copié dans le presse-papiers", "remember opened files": "Mémoriser les fichiers ouverts", "remember opened folders": "Mémoriser les dossiers ouverts", "no suggestions": "Aucune suggestion", "no suggestions aggressive": "Aucune suggestion agressive", "install": "Installer", "installing": "Installation...", "plugins": "Extensions", "recently used": "Récemment utilisé", "update": "Mise à jour", "uninstall": "Désinstaller", "download acode pro": "Télécharger Acode pro", "loading plugins": "Charger les extensions", "diagonal scrolling": "Défilement en diagonale", "reverse scrolling": "Défilement inversé", "formatter": "Formateur de code", "format on save": "Formater à l'enregistrement", "remove ads": "Supprimer les pubs", "faqs": "FAQ", "feedback": "Commentaires", "header": "Barre supérieure", "sidebar": "Barre latérale", "inapp": "Intégré", "browser": "Navigateur", "fast": "Rapide", "slow": "Lent", "scroll settings": "Paramètres du défilement", "scroll speed": "Vitesse du défilement", "loading...": "Chargement...", "no plugins found": "Aucune extension trouvée", "name": "Nom", "username": "Nom d'utilisateur", "optional": "optionel", "hostname": "Nom du serveur", "password": "Mot de passe", "security type": "Type de sécurité", "connection mode": "Mode de connexion", "port": "Port", "key file": "Fichier de clé", "select key file": "Sélectionner le fichier de clé", "passphrase": "Passphrase", "connecting...": "Connection...", "type filename": "Entrer le nom du fichier", "unable to load files": "Impossible de charger les fichiers", "preview port": "Port pour l'aperçu", "find file": "Rechercher un fichier", "system": "Système", "please select a formatter": "Sélectionnez un formateur de code", "case sensitive": "Sensible à la casse", "regular expression": "Expression rationnelle", "whole word": "Mot entier", "edit with": "Modifier avec", "open with": "Ouvrir avec", "no app found to handle this file": "Aucune appli trouvée pour utiliser ce fichier", "restore default settings": "Rétablir les paramètres par défaut", "server port": "Port du serveur", "preview settings": "Paramètres des aperçus", "preview settings note": "Si le port d'aperçu et le port du serveur sont différents, l'appli ne démarrera pas le serveur. Au lieu de ça, elle ouvrira https://: dans le navigateur (externe ou intégré). C'est pratique si vous avez un serveur distant.", "backup/restore note": "Cela ne sauvegardera que vos paramètres, votre thème personnalisé et vos raccourcis clavier. Vos réglages FTP/SFTP ne seront pas sauvegardés.", "host": "Serveur", "retry ftp/sftp when fail": "Réessayer FTP/SFTP après échec", "more": "Plus", "thank you :)": "Merci :)", "purchase pending": "achat en cours", "cancelled": "annulé", "local": "Local", "remote": "Distant", "show console toggler": "Afficher l'interrupteur de la console", "binary file": "Ce fichier contient des données binaires. Voulez-vous vraiment l'ouvrir ?", "relative line numbers": "Numéros de ligne relatifs", "elastic tabstops": "Taquets élastiques", "line based rtl switching": "Texte de droite à gauche par ligne", "hard wrap": "Retour à la ligne dur", "spellcheck": "Vérification de l'orthographe", "wrap method": "Gestion du débordement des lignes", "use textarea for ime": "Utiliser une textarea pour écrire", "invalid plugin": "Extension invalide", "type command": "Entrer une commande", "plugin": "Extension", "quicktools trigger mode": "Mode de déclenchement des outils rapides", "print margin": "Marge de droite", "touch move threshold": "Seuil de déplacement tactile", "info-retryremotefsafterfail": "Réessayer la connexion FTP/SFTP après échec.", "info-fullscreen": "Masquer la barre de titre dans l'écran d'accueil.", "info-checkfiles": "Vérifier si les fichiers ont été modifiés quand l'appli est passée à l'arrière-plan.", "info-console": "Choisir la console JavaScript. Legacy est la console par défaut, eruda est une console tierce.", "info-keyboardmode": "Mode du clavier pour la saisie. « Aucune suggestion » masque les suggestions et l'autocorrection. Si les suggestions ne fonctionnent pas, essayez « Aucune suggestion agressive ».", "info-rememberfiles": "Mémoriser les fichiers ouverts lorsque l'appli est fermée.", "info-rememberfolders": "Mémoriser les dossiers ouverts lorsque l'appli est fermée.", "info-floatingbutton": "Afficher ou masquer le bouton flottant des outils rapides.", "info-openfilelistpos": "Où afficher la liste des fichiers ouverts.", "info-touchmovethreshold": "Si la sensibilité tactile de votre appareil est trop élevée, vous pouvez augmenter cette valeur pour empêcher des déplacements accidentels.", "info-scroll-settings": "Ces paramètres permettent de configurer le défilement et la gestion des longues lignes.", "info-animation": "Si l'appli est lente, désactivez les animations.", "info-quicktoolstriggermode": "Si le bouton des outils rapides ne fonctionne pas, essayez de changer cette valeur.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Propriété", "api_error": "Serveur d'API éteint, veuillez réessayer plus tard.", "installed": "Installé", "all": "Tout", "medium": "Moyen", "refund": "Remboursement", "product not available": "Produit non disponible", "no-product-info": "Ce produit n'est pas disponible dans votre pays à l'heure actuelle, veuillez réessayer plus tard.", "close": "Fermer", "explore": "Explorer", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin activé", "plugin_disabled": "Plugin désactivé", "enable_plugin": "Activer ce plugin", "disable_plugin": "Désactiver ce plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Parrainer", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/he-il.json ================================================ { "lang": "עברית", "about": "אודות", "active files": "קבצים פעילים", "alert": "התראה", "app theme": "עיצוב", "autocorrect": "להפעיל תיקון אוטומטי?", "autosave": "שמירה אוטומטית", "cancel": "ביטול", "change language": "שינוי שפה", "choose color": "בחירת צבע", "clear": "ניקוי", "close app": "לסגור את האפלקציה?", "commit message": "הודעת commit", "console": "קונסול", "conflict error": "התנגשות! אנא המתן לפני ביצוע commit נוסף.", "copy": "העתק", "create folder error": "מצטערים, לא הצלחנו ליצור תיקיה חדשה", "cut": "חתוך", "delete": "מחק", "dependencies": "תלויות", "delay": "זמן במילישניות", "editor settings": "הגדרות העורך", "editor theme": "ערוך עיצוב", "enter file name": "הקלד שם קובץ", "enter folder name": "הקלד שם תיקיה", "empty folder message": "תיקה ריקה", "enter line number": "הקלד מספר שורה", "error": "שגיאה", "failed": "נכשל", "file already exists": "קובץ כבר קיים", "file already exists force": "קובץ כבר קיים, לדרוס אותו?", "file changed": "הקובץ השתנה, לטעון את הקובץ המעודכן?", "file deleted": "קובץ נמחק", "file is not supported": "קובץ לא נתמך", "file not supported": "סוג קובץ זה אינו נתמך", "file too large": "קובץ גדול מידי, מקסימום גודל קובץ מותר {size}", "file renamed": "שם הקובץ השתנה", "file saved": "קובץ נשמר", "folder added": "תיקיה נוספה", "folder already added": "תיקיה כבר נוספה", "font size": "גודל גופן", "goto": "עבור לשורה", "icons definition": "הגדרת סמלים", "info": "מידע", "invalid value": "ערך לא חוקי", "language changed": "שפה שונתה בהצלחה", "linting": "בדיקת שגיאת תחביר", "logout": "התנתק", "loading": "טוען", "my profile": "הפרופיל שלי", "new file": "קובץ חדש", "new folder": "תיקיה חדשה", "no": "לא", "no editor message": "פתח או צור קובץ ותיקייה חדשים מהתפריט", "not set": "לא הוגדר", "unsaved files close app": "ישנם מספר קבצים שלא נשמרו, לסגור את האפליקציה?", "notice": "לידיעתך", "open file": "פתח קובץ", "open files and folders": "פתח קבצים ותקיות", "open folder": "פתח תיקיה", "open recent": "פתח אחרונים", "ok": "בסדר", "overwrite": "דריסה", "paste": "הדבק", "preview mode": "מצב תצוגה מקדימה", "read only file": "לא ניתן לשמור קובץ לקריאה בלבד נא לשמור כ", "reload": "טעינה מחדש", "rename": "שנה שם", "replace": "החלף", "required": "שם זה נדרש", "run your web app": "הפעל את אפליקציית האינטרנט שלך", "save": "שמור", "saving": "שומר", "save as": "שמור כ...", "save file to run": "נא לשמור את הקובץ כדי להריץ בדפדפן", "search": "חיפוש", "see logs and errors": "הצג יומנים ושגיאות", "select folder": "בחר תיקיה", "settings": "הגדרות", "settings saved": "הגדרות נשמרו", "show line numbers": "הצג מספרי שורה", "show hidden files": "הצגת קבצים מוסתרים", "show spaces": "הצגת רווחים", "soft tab": "לשונית רכה", "sort by name": "סדר לפי שם", "success": "הצליח", "tab size": "גודל טאב", "text wrap": "גלישת טקסט", "theme": "עיצוב", "unable to delete file": "לא ניתן למחוק קובץ", "unable to open file": "מצטערים, לא הצלחנו לפתוח את הקובץ", "unable to open folder": "מצטערים, לא הצלחנו לפתוח את התיקיה", "unable to save file": "מצטערים, לא הצלחנו לשמור את הקובץ", "unable to rename": "מצטערים, לא הצלחנו לשנות את שם הקובץ", "unsaved file": "הקובץ עדיין לא נשמר, לצאת בכל זאת?", "warning": "אזהרה", "use emmet": "השתמש ב- emmet", "use quick tools": "השתמש בכלים מהירים", "yes": "כן", "encoding": "קידוד טקסט", "syntax highlighting": "הדגשת תחביר", "read only": "קריאה בלבד", "select all": "בחר הכל", "select branch": "בחר בראנץ", "create new branch": "צור בראנץ חדש", "use branch": "השתמש בראנץ", "new branch": "בראנץ חדש", "branch": "בראנץ", "key bindings": "Key bindings", "edit": "ערוך", "reset": "איפוס", "color": "צבע", "select word": "בחר מילה", "quick tools": "כלים מהירים", "select": "בחר", "editor font": "עורך פונטים", "new project": "פרוייקט חדש", "format": "פורמט", "project name": "שם פרוייקט", "unsupported device": "המכשיר שלך לא תומך בעיצובים.", "vibrate on tap": "רטט במגע", "copy command is not supported by ftp.": "פקודת ההעתקה אינה נתמכת על ידי FTP.", "support title": "תמוך ב- Acode", "fullscreen": "מסך מלא", "animation": "אנימציה", "backup": "גיבוי", "restore": "שיחזור", "backup successful": "גיבוי הצליח", "invalid backup file": "קובץ גיבוי לא תקין", "add path": "הוסף נתיב", "live autocompletion": "השלמה אוטומטית בזמן אמת", "file properties": "מאפייני קובץ", "path": "נתיב", "type": "סוג", "word count": "ספירת מילים", "line count": "ספירת שורות", "last modified": "נערך לאחרונה", "size": "גודל", "share": "שיתוף", "show print margin": "הצג שולי הדפסה", "login": "התחברות", "scrollbar size": "גודל סרגל הגלילה", "cursor controller size": "גודל בקר הסמן", "none": "ללא", "small": "קטן", "large": "גדול", "floating button": "כפתור צף", "confirm on exit": "אמת יציאה", "show console": "הצג קונסולה", "image": "תמונה", "insert file": "הכנס קובץ", "insert color": "הכנס צבע", "powersave mode warning": "כבה את מצב חיסכון באנרגיה כדי להציג תצוגה מקדימה בדפדפן חיצוני.", "exit": "יציאה", "custom": "מותאם אישית", "reset warning": "האם אתה בטוח שברצונך לאפס את העיצוב?", "theme type": "סוג עיצוב", "light": "מואר", "dark": "כהה", "file browser": "עיון הקבצים", "operation not permitted": "פעולה אסורה", "no such file or directory": "לא נמצא קובץ או תיקיה כזו", "input/output error": "שגיאת קלט/פלט", "permission denied": "ההרשאה נדחתה", "bad address": "כתובת לא תקינה", "file exists": "קובץ קיים", "not a directory": "לא תיקיה", "is a directory": "תיקיה", "invalid argument": "ארגומנט לא חוקי", "too many open files in system": "יותר מידי קבצים פתוחים במכשיר", "too many open files": "יותר מידי קבצים פתוחים", "text file busy": "קובץ טקסט עסוק", "no space left on device": "לא נשאר אחסון במכשיר", "read-only file system": "קבצי מערכת לצפיה בלבד", "file name too long": "שם הקובץ ארוך מידי", "too many users": "יותר מידי משתמשים", "connection timed out": "תם הזמן שהוקצב לחיבור", "connection refused": "חיבור נדחה", "owner died": "הבעלים נפטר", "an error occurred": "אירעה שגיאה", "add ftp": "הוסף FTP", "add sftp": "הוסף SFTP", "save file": "שמור קובץ", "save file as": "שמור קובץ כ", "files": "קבצים", "help": "עזרה", "file has been deleted": "{file} נמחק!", "feature not available": "תכונה זו זמינה רק בגרסה בתשלום של האפליקציה.", "deleted file": "קובץ מחוק", "line height": "Line height", "preview info": "אם ברצונך להפעיל את הקובץ הפעיל, לחץ והחזק את סמל ההפעלה.", "manage all files": "אפשר לעורך Acode לנהל את כל הקבצים שלך כדי לערוך קבצים במכשיר שלך בקלות.", "close file": "סגור קובץ", "reset connections": "חיבורים אחרונים", "check file changes": "בדוק שינוי בקבצים", "open in browser": "פתח בדפדפן", "desktop mode": "מצב שולחן עבודה", "toggle console": "הפעלה/כיבוי קונסולה", "new line mode": "מצב שורה חדשה", "add a storage": "הוסף אחסון", "rate acode": "דרג את Acode", "support": "תמיכה", "downloading file": "מוריד את {file}", "downloading...": "מוריד...", "folder name": "שם תיקיה", "keyboard mode": "מצב מקלדת", "normal": "רגיל", "app settings": "הגדרות האפליקציה", "disable in-app-browser caching": "השבתת אחסון במטמון בדפדפן בתוך האפליקציה", "Should use Current File For preview instead of default (index.html)": "יש להשתמש בקובץ הנוכחי לתצוגה מקדימה במקום ברירת המחדל (index.html)", "copied to clipboard": "הועתק", "remember opened files": "זכור קבצים שנפתחו", "remember opened folders": "זכור תקיות שנפתחו", "no suggestions": "אין הצעות", "no suggestions aggressive": "אין הצעות אגרסיביות", "install": "התקנה", "installing": "מתקין...", "plugins": "תוספים", "recently used": "בשימוש לאחרונה", "update": "עדכן", "uninstall": "הסר התקנה", "download acode pro": "מוריד Acode pro", "loading plugins": "טוען תוספים", "faqs": "FAQs", "feedback": "משוב", "header": "כותרת", "sidebar": "סרגל צד", "inapp": "באפליקציה", "browser": "דפדפן", "diagonal scrolling": "גלילה אלכסונית", "reverse scrolling": "גלילה הפוכה", "formatter": "מעצב", "format on save": "עיצוב בעת שמירה", "remove ads": "הסר פרסומות", "fast": "מהיר", "slow": "איטי", "scroll settings": "הגדרות גלילה", "scroll speed": "מהירות גלילה", "loading...": "טוען...", "no plugins found": "לא נמצאו תוספים", "name": "שם", "username": "שם משתמש", "optional": "אפשרי", "hostname": "מארח", "password": "סיסמא", "security type": "סוג אבטחה", "connection mode": "סוג חיבור", "port": "פורט", "key file": "קובץ מפתח", "select key file": "בחר קובץ מפתח", "passphrase": "ביטוי סיסמה", "connecting...": "מתחבר...", "type filename": "שם סוג קובץ", "unable to load files": "לא הצלחנו לפתוח את הקובץ", "preview port": "פורט תצוגה מקדימה", "find file": "מצא קובץ", "system": "מערכת", "please select a formatter": "Please select a formatter", "case sensitive": "תלוי רישיות", "regular expression": "ביטוי רגולרי", "whole word": "מילה שלמה", "edit with": "ערוך עם...", "open with": "פתח עם...", "no app found to handle this file": "לא נמצאה אפליקציה לפתיחת הקובץ הזה", "restore default settings": "שחזר הגדרות ברירת מחדל", "server port": "פורט שרת", "preview settings": "הגדרות תצוגה מקדימה", "preview settings note": "אם פורט התצוגה מקדימה ופורט השרת שונים, האפליקציה לא תפעיל את השרת ובמקום זאת תיפתח https://: בדפדפן או בדפדפן בתוך האפליקציה. זה שימושי כשאתה מפעיל שרת במקום אחר.", "backup/restore note": "זה יגבה רק את ההגדרות שלך, ערכת נושא מותאמת אישית, תוספים מותקנים וקישורי מקשים. זה לא יגבה את מצב ה-FTP/SFTP או האפליקציה שלך.", "host": "מארח", "retry ftp/sftp when fail": "נסה שוב כש- ftp/sftp נכשל", "more": "עוד", "thank you :)": "תודה לך :)", "purchase pending": "רכישה ממתינה", "cancelled": "בוטל", "local": "local", "remote": "מרוחק", "show console toggler": "הצג מתג קונסולה", "binary file": "קובץ זה מכיל נתונים בינאריים, האם ברצונך לפתוח אותו?", "relative line numbers": "מספרי שורות יחסיים", "elastic tabstops": "עצירות טאבים אלסטיות", "line based rtl switching": "מיתוג RTL מבוסס קו", "hard wrap": "עטיפה קשה", "spellcheck": "בדיקת איות", "wrap method": "שיטת עטיפת", "use textarea for ime": "השתמש באזור טקסט עבור IME", "invalid plugin": "תוסף לא חוקי", "type command": "הקלד פקודה", "plugin": "תוספים", "quicktools trigger mode": "מצב טריגר של כלים מהירים", "print margin": "שולי הדפסה", "touch move threshold": "סף תנועה במגע", "info-retryremotefsafterfail": "נסה שוב להתחבר ל FTP/SFTP אם נכשל.", "info-fullscreen": "הסתר את שורת הכותרת במסך הבית.", "info-checkfiles": "האזנה לשינוי קבצים כשאפלקציה ברקע.", "info-console": "בחר קונסולת JavaScript. קונסולת Legacy היא ברירת המחדל, Eruda היא קונסולת צד שלישי..", "info-keyboardmode": "מצב מקלדת להזנת טקסט, ללא הצעות יסתיר את ההצעות ותיקון אוטומטי יתבצע. אם ללא הצעות לא יעבוד, נסה לשנות את הערך ללא הצעות אגרסיביות..", "info-rememberfiles": "זכור קבצים פתוחים כאשר האפליקציה סגורה.", "info-rememberfolders": "זכור תיקיות פתוחות כאשר האפליקציה סגורה.", "info-floatingbutton": "הצג או הסתר את הכפתור הצף של הכלים המהירים.", "info-openfilelistpos": "היכן להציג את רשימת הקבצים הפעילים.", "info-touchmovethreshold": "אם רגישות המגע של המכשיר שלך גבוהה מידי, תוכל להגדיל ערך זה כדי למנוע תנועה מקרית של מגע.", "info-scroll-settings": "הגדרות אלה מכילות הגדרות גלילה כולל גלישת טקסט.", "info-animation": "אם האפליקציה מרגישה לאגית, השבת אנימציה.", "info-quicktoolstriggermode": "אם הכפתור בכלים המהירים אינו פועל, נסה לשנות ערך זה.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "בבעלות", "api_error": "שרת ה-API מושבת, אנא נסה שוב בעוד מספר דקות.", "installed": "מותקן", "all": "הכל", "medium": "בינוני", "refund": "החזר", "product not available": "מוצר לא זמין", "no-product-info": "מוצר זה אינו זמין במדינתך כרגע, נא לנסות פעם אחרת.", "close": "סגור", "explore": "חקור", "key bindings updated": "Key bindings updated", "search in files": "חפש בקבצים", "exclude files": "החרגת קבצים", "include files": "הכללת קבצים", "search result": "{matches} תוצאות ב- {files} קבצים.", "invalid regex": "ביטוי רגולרי לא חוקי: {message}.", "bottom": "תחתית", "save all": "שמור הכל", "close all": "סגור הכל", "unsaved files warning": "חלק מהקבצים לא נשמרו. לחץ על 'אישור' ובחר מה לעשות או לחץ על 'ביטול' כדי לחזור אחורה.", "save all warning": "האם אתה בטוח שברצונך לשמור את כל הקבצים ולסגור? פעולה זו אינה ניתנת לביטול.", "save all changes warning": "האם אתה בטוח שברצונך לשמור את כל הקבצים?", "close all warning": "האם אתה בטוח שברצונך לסגור את כל הקבצים? שינויים שלא נשמרו יאבדו ולא יהיה ניתן לשחזר אותם.", "refresh": "רענן", "shortcut buttons": "כפתורי קיצור דרך", "no result": "אין תוצאות", "searching...": "מחפש...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab מקש", "quicktools:shift-key": "Shift מקש", "quicktools:undo": "בטל", "quicktools:redo": "בצע שוב", "quicktools:search": "חפש בקובץ", "quicktools:save": "שמור קובץ", "quicktools:esc-key": "Escape מקש", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow מקש", "quicktools:right-arrow-key": "Right arrow מקש", "quicktools:up-arrow-key": "Up arrow מקש", "quicktools:down-arrow-key": "Down arrow מקש", "quicktools:moveline-up": "הזזת שורה למעלה", "quicktools:moveline-down": "הזזת שורה למטה", "quicktools:copyline-up": "העתק שורה למעלה", "quicktools:copyline-down": "העתק שורה למעלה", "quicktools:semicolon": "הוסף פסיק", "quicktools:quotation": "הוסף סימן שאלה", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "הוסף סימן שווה", "quicktools:slash": "הוסף סימן אלכסון", "quicktools:exclamation": "הוסף סימן קריאה", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "התאם אישית לחצני קיצורי דרך ומקשי מקלדת בכלים המהירים שמתחת לעורך כדי לשפר את חוויית הקידוד שלך.", "info-excludefolders": "השתמשו בתבנית **/node_modules/** כדי להתעלם מכל הקבצים מהתיקייה node_modules. פעולה זו תמנע את הכללת הקבצים בחיפושי קבצים.", "missed files": "נסרקו {count} קבצים לאחר תחילת החיפוש ולא ייכללו בחיפוש.", "remove": "הסר", "quicktools:command-palette": "לוח פקודות", "default file encoding": "קידוד קובץ ברירת מחדל", "remove entry": "האם אתה בטוח שברצונך להסיר את '{name}' מהנתיבים השמורים? שים לב שהסרתו לא תמחק את הנתיב עצמו.", "delete entry": "אשר מחיקה: '{name}'. לא ניתן לבטל פעולה זו. להמשיך?", "change encoding": "לפתוח מחדש את '{file}' עם קידוד '{encoding}'? פעולה זו תגרום לאובדן כל השינויים שלא נשמרו בקובץ. האם ברצונך להמשיך בפתיחה מחדש?", "reopen file": "אתה בטוח שברצונך לפתוח מחדש את הקובץ '{file}'? כל השינויים שלא נשמרו ימחקו.", "plugin min version": "{name} זמין רק ב Acode - {v-code} ומעלה. לחץ פה לעידכון.", "color preview": "צבע תצוגה מקדימה", "confirm": "אישור", "list files": "רשימת כל הקבצים ב {name}? יותר מידי קבצים עלולים להוביל לקריסות.", "problems": "בעיות", "show side buttons": "הצג כפתורי צד", "bug_report": "דיווח באג", "verified publisher": "מפרסם מאומת", "most_downloaded": "הכי הרבה הורדות", "newly_added": "נוסף לאחרונה", "top_rated": "דירוג גבוהה", "rename not supported": "שינוי שם בספריית termux אינו נתמך", "compress": "דחוס", "copy uri": "העתק Uri", "delete entries": "אתה בטוח שברצונך למחוק {count} פריטים?", "deleting items": "מוחק {count} פריטים...", "import project zip": "ייבא פרוייקט(zip)", "changelog": "יומן שינויים", "notifications": "התראות", "no_unread_notifications": "אין התראות שלא נקראו", "should_use_current_file_for_preview": "יש להשתמש בקובץ הנוכחי לתצוגה מקדימה במקום ברירת המחדל (index.html)", "fade fold widgets": "ווידג'טים של קיפול דהייה", "quicktools:home-key": "Home מקש", "quicktools:end-key": "End מקש", "quicktools:pageup-key": "PageUp מקש", "quicktools:pagedown-key": "PageDown מקש", "quicktools:delete-key": "Delete מקש", "quicktools:tilde": "הוסף טילדה", "quicktools:backtick": "הוסף גרש הפוך", "quicktools:hash": "הוסף סמל גיבוב", "quicktools:dollar": "הוסף סימן דולר", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "הפלאגין הופעל", "plugin_disabled": "הפלאגין הושבת", "enable_plugin": "הפעל תוסף זה", "disable_plugin": "השבת תוסף זה", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "לָתֵת חָסוּת", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/hi-in.json ================================================ { "lang": "हिंदी", "about": "एप्लीकेशन के बारे में", "active files": "सक्रिय फ़ाइलें", "alert": "चेतावनी", "app theme": "एप्लीकेशन का थीम", "autocorrect": "स्वत: सुधार सक्षम करें?", "autosave": "स्वरक्षण", "cancel": "रद्द करें", "change language": "भाषा बदलें", "choose color": "रंग चुनें", "create folder error": "क्षमा करें, नया फ़ोल्डर बनाने में असमर्थ", "clear": "साफ करें", "close app": "एप्लिकेशन बंद करें?", "commit message": "Commit message", "console": "कंसोल", "conflict error": "Conflict! Please wait before another commit.", "copy": "कापी", "cut": "कट", "delete": "इसे हटाएं", "dependencies": "निर्भरता", "delay": "मिलीसेकंड में समय", "editor settings": "एडिटर सेटिंग्स", "editor theme": "एडिटर का थीम", "enter file name": "फ़ाइल का नाम दर्ज करें", "enter folder name": "फ़ोल्डर का नाम दर्ज करें", "empty folder message": "खाली फ़ोल्डर", "enter line number": "लाइन नंबर दर्ज करें", "error": "एरर", "failed": "असफल", "file already exists": "फ़ाइल पहले से ही मौजूद है", "file already exists force": "फ़ाइल पहले से ही मौजूद है। ओवरराइट करें?", "file changed": " बदल दी गई है, फ़ाइल पुनः लोड करें?", "file deleted": "फ़ाइल डिलीट कर दि गई है", "file is not supported": "फ़ाइल समर्थित नहीं है", "file too large": "संभाल करने के लिए फ़ाइल बड़ी है। अधिकतम फ़ाइल आकार की अनुमति है {size}", "file renamed": "फ़ाइल का नाम बदल दिया गया है", "file saved": "फाइल सेव हो गया", "folder added": "फ़ोल्डर जोड़ा गया", "folder already added": "फ़ोल्डर पहले से ही जोड़ा गया", "goto": "गोटू लाइन", "icons definition": "आइकन स्पष्टीकरण", "info": "जानकारी", "invalid value": "अमान्य मूल्य", "language changed": "भाषा को सफलतापूर्वक बदल दिया गया है", "linting": "वाक्यविन्यास त्रुटि की जाँच करें", "logout": "लॉग आउट", "loading": "लोड हो रहा है", "my profile": "मेरी प्रोफाइल", "new file": "नई फ़ाइल", "new folder": "नया फोल्डर", "no editor message": "मेनू से नई फ़ाइल और फ़ोल्डर खोलें या बनाएँ", "no": "नहीं", "not set": "सेट नहीं है", "unsaved files close app": "बिना सेव की गई फ़ाइलें हैं। फिर भी एप्लिकेशन को बंद करें?", "notice": "नोटिस", "open file": "फ़ाइल खोलें", "open files and folders": "फ़ाइल और फ़ोल्डर खोलें", "open folder": "फ़ोल्डर खोलें", "open recent": "हाल ही का खोलें", "ok": "ठीक", "overwrite": "ओवरराइट करें", "paste": "पेस्ट", "preview mode": "पूर्वावलोकन मोड", "read only file": "यह फाइल सेव नहीं की सकती, किर्प्या इसे 'सेव एज' से सेव करे", "redo": "रीडू", "replace": "इससे बदलें", "reload": "रिलोड", "rename": "नाम बदलने", "required": "यह फ़ील्ड आवश्यक है", "run your web app": "अपना वेब ऐप चलाएं", "save": "सेव", "saving": "सेव हो रहा है", "save as": "सेव ऐज", "save file to run": "कृपया इस फाइल को ब्राउजर में चलाने के लिए सेव करें", "search": "खोज", "see logs and errors": "लॉग और त्रुटियों को दिखाएं", "select folder": "फोल्डर का चयन करें", "settings": "सेटिंग्स", "settings saved": "सेटिंग्स सेव हो गया", "show line numbers": "लाइन नंबर्स दिखाएं", "show hidden files": "छिपी फ़ाइलें दिखाएं", "show spaces": "रिक्त स्थान दिखाएं", "soft tab": "सॉफ्ट टैब", "sort by name": "नाम द्वारा क्रमबद्ध करें", "success": "सफल", "tab size": "टैब साइज", "text wrap": "टेक्स्ट व्रैप", "theme": "थीम", "unable to delete file": "फाइल डिलीट नहीं हो पा रहा है", "unable to open file": "क्षमा करें, फ़ाइल खोलने में असमर्थ", "unable to open folder": "क्षमा करें, फ़ोल्डर खोलने में असमर्थ", "unable to save file": "क्षमा करें, फ़ाइल सेव करने में असमर्थ", "unable to rename": "क्षमा करें, नाम बदलने में असमर्थ", "unsaved file": "यह फ़ाइल सेव नहीं गई है, फिर भी फ़ाइल बंद करें", "warning": "ध्यान दे", "use emmet": "इमेट का प्रयोग करें", "use quick tools": "त्वरित साधनों का उपयोग करें", "yes": "हाँ", "encoding": "टेक्स्ट एन्कोडिंग", "syntax highlighting": "सिंटेक्स हाइलाइटिंग", "read only": "केवल पढ़ने के लिए", "select all": "सेलेक्ट आल", "select branch": "शाखा का चयन करें", "create new branch": "नई शाखा बनाएं", "use branch": "शाखा का उपयोग करें", "new branch": "नई शाखा", "branch": "branch", "key bindings": "की बिंडिंग्स", "edit": "संपादित करें", "reset": "रीसेट", "color": "रंग", "select word": "शब्द का चयन करें", "quick tools": "त्वरित उपकरण", "select": "चयन", "editor font": "एडिटर फ़ॉन्ट", "new project": "नया प्रोजेक्ट", "format": "फॉर्मेट", "project name": "प्रोजेक्ट का नाम", "unsupported device": "आपका डिवाइस थीम का समर्थन नहीं करता है।", "vibrate on tap": "टैप पर कंपन करें", "copy command is not supported by ftp.": "कॉपी कमांड एफ़टीपी द्वारा समर्थित नहीं है।", "support title": "Support Acode", "fullscreen": "पूर्ण स्क्रीन", "animation": "एनीमेशन", "backup": "बैकअप", "restore": "पुनर्स्थापित करना", "backup successful": "बैकअप सफल", "invalid backup file": "अमान्य बैकअप फ़ाइल", "add path": "पाथ जोड़ें", "live autocompletion": "लाइव स्वतः‑पूर्ण", "file properties": "फ़ाइल गुण", "path": "पथ", "type": "टाइप", "word count": "शब्द गणना", "line count": "लाइन काउंट", "last modified": "अंतिम बार संशोधित", "size": "आकार", "share": "शेयर", "show print margin": "प्रिंट मार्जिन दिखाएँ", "login": "लॉग इन करें", "scrollbar size": "स्क्रॉलबार का आकार", "cursor controller size": "कर्सर नियंत्रक आकार", "none": "कोई भी नहीं", "small": "छोटा", "large": "विशाल", "floating button": "फ्लोटिंग बटन", "confirm on exit": "बाहर निकलने पर पुष्टि करें", "show console": "कंसोल दिखाएँ", "image": "इमेज", "insert file": "फ़ाइल डालें", "insert color": "रंग डालें", "powersave mode warning": "बाहरी ब्राउज़र में पूर्वावलोकन करने के लिए पावर सेविंग मोड बंद करें।", "exit": "बाहर निकलें", "custom": "custom", "reset warning": "क्या आप वाकई थीम रीसेट करना चाहते हैं?", "theme type": "थीम प्रकार", "light": "हल्का रंग", "dark": "गाढ़ा रंग", "file browser": "फ़ाइल ब्राउज़र", "file not supported": "यह फ़ाइल प्रकार समर्थित नहीं है।", "font size": "फॉण्ट साइज", "operation not permitted": "कार्रवाई की अनुमति नहीं", "no such file or directory": "ऐसी कोई फ़ाइल या डायरेक्टरी नहीं है", "input/output error": "इनपुट/आउटपुट त्रुटि", "permission denied": "अनुमति नहीं मिली", "bad address": "खराब पता", "file exists": "फ़ाइल मौजूद", "not a directory": "डायरेक्टरी नहीं है", "is a directory": "डायरेक्टरी है", "invalid argument": "अवैध तर्क", "too many open files in system": "सिस्टम में बहुत अधिक खुली फ़ाइलें", "too many open files": "बहुत अधिक खुली फ़ाइलें", "text file busy": "टेक्स्ट फ़ाइल व्यस्त", "no space left on device": "डिवाइस पर जगह समाप्त", "read-only file system": "रीड ओन्ली फ़ाइल सिस्टम", "file name too long": "फ़ाइल का नाम बहुत लंबा", "too many users": "बहुत अधिक उपयोगकर्ता", "connection timed out": "कनेक्शन का समय समाप्त", "connection refused": "कनेक्शन नहीं हो सका", "owner died": "मालिक मर गया", "an error occurred": "एक त्रुटि पाई गई", "add ftp": "FTP जोड़ें", "add sftp": "SFTP जोड़ें", "save file": "फ़ाइल सहेजें", "save file as": "फ़ाइल को इस नाम से सहेजें", "files": "फाइल्स", "help": "हेल्प", "file has been deleted": "{file} हटा दी गई है!", "feature not available": "यह सुविधा ऐप के केवल भुगतान किए गए संस्करण में उपलब्ध है।", "deleted file": "हटाई गई फ़ाइल", "line height": "Line height", "preview info": "यदि आप सक्रिय फ़ाइल चलाना चाहते हैं, तो प्ले आइकन पर टैप करके रखें।", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "फ़ाइल बंद करें", "reset connections": "कनेक्शन रीसेट करें", "check file changes": "फ़ाइल परिवर्तनों की जाँच करें", "open in browser": "ब्राउज़र में खोलें", "desktop mode": "डेस्कटॉप मोड", "toggle console": "टॉगल कंसोल", "new line mode": "नई लाइन मोड", "add a storage": "एक स्टोरेज जोड़ें", "rate acode": "Acode को रेट करें", "support": "सहायता", "downloading file": "डौन्लोडिंग {file}", "downloading...": "डौन्लोडिंग...", "folder name": "फोल्डर का नाम", "keyboard mode": "कीबोर्ड मोड", "normal": "सामान्य", "app settings": "एप्लिकेशन सेटिंग", "disable in-app-browser caching": "इन-ऐप-ब्राउज़र कैशिंग बंद करें", "copied to clipboard": "क्लिपबोर्ड पर कॉपी किया गया", "remember opened files": "खोली गई फ़ाइलें याद रखें", "remember opened folders": "खोले गए फोल्डर याद रखें", "no suggestions": "कोई सुझाव नहीं", "no suggestions aggressive": "कोई सुझाव नहीं आक्रामक", "install": "इनस्टॉल", "installing": "इनस्टॉल हो रहा है...", "plugins": "प्लगिन्स", "recently used": "हाल ही में उपयोग किए गए", "update": "अपडेट", "uninstall": "हटाएँ", "download acode pro": "एकोड प्रो डाउनलोड करें", "loading plugins": "प्लगइन्स लोड हो रहा है", "faqs": "पूछे जाने वाले प्रश्न", "feedback": "प्रतिपुष्टि", "header": "Header", "sidebar": "साइडबार", "inapp": "Inapp", "browser": "ब्राउज़र", "diagonal scrolling": "विकर्ण स्क्रॉलिंग", "reverse scrolling": "रिवर्स स्क्रॉलिंग", "formatter": "फॉर्मेटर", "format on save": "सहेजने पर प्रारूपित करें", "remove ads": "विज्ञापन हटाएँ", "fast": "तेज़", "slow": "धीमा", "scroll settings": "स्क्रॉल सेटिंग्स", "scroll speed": "स्क्रोल गति", "loading...": "लोड हो रहा है...", "no plugins found": "कोई प्लगइन्स नहीं मिला", "name": "नाम", "username": "उपयोगकर्ता नाम", "optional": "वैकल्पिक", "hostname": "होस्ट नाम", "password": "पासवर्ड", "security type": "सुरक्षा प्रकार", "connection mode": "कनेक्शन मोड", "port": "पोर्ट", "key file": "की फाइल", "select key file": "की फ़ाइल का चयन करें", "passphrase": "पसफ्रेज़", "connecting...": "कनेक्टिंग...", "type filename": "फ़ाइल नाम लिखें", "unable to load files": "फ़ाइलें लोड करने में असमर्थ", "preview port": "प्रीव्यू पोर्ट", "find file": "फ़ाइल खोजें", "system": "सिस्टम", "please select a formatter": "कृपया एक फॉर्मेटर चुनें", "case sensitive": "केस सेंसिटिव", "regular expression": "रेगुलर एक्सप्रेशन", "whole word": "पूर्ण शब्द", "edit with": "के साथ संपादित करें", "open with": "के साथ खोलें", "no app found to handle this file": "इस फ़ाइल को संभालने के लिए कोई ऐप नहीं मिला", "restore default settings": "डिफ़ॉल्ट सेटिंग्स को पुनर्स्थापित करें", "server port": "सर्वर पोर्ट", "preview settings": "प्रीव्यू सेटिंग्स", "preview settings note": "यदि प्रीव्यू पोर्ट और सर्वर पोर्ट भिन्न हैं, तो ऐप सर्वर शुरू नहीं करेगा और इसके बजाय ब्राउज़र या इन-ऐप ब्राउज़र में https://: खोलेगा। यह तब उपयोगी है जब आप कहीं और सर्वर चला रहे हों।", "backup/restore note": "यह केवल आपकी सेटिंग्स, कस्टम थीम और की बाइंडिंग्स का बैकअप लेगा। यह आपके FPT/SFTP का बैकअप नहीं लेगा।", "host": "होस्ट", "retry ftp/sftp when fail": "विफल होने पर FTP/SFTP पुनः प्रयास करें", "more": "अधिक", "thank you :)": "धन्यवाद :)", "purchase pending": "खरीदारी लंबित", "cancelled": "रद्द", "local": "लोकल", "remote": "रिमोट", "show console toggler": "कंसोल टॉगलर दिखाएँ", "binary file": "इस फ़ाइल में बाइनरी डेटा है, क्या आप इसे खोलना चाहते हैं?", "relative line numbers": "सापेक्ष पंक्ति संख्या", "elastic tabstops": "इलास्टिक टैबस्टॉप्स", "line based rtl switching": "लाइन आधारित RTL स्विचिंग", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "कमांड पैलेट", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "रंग पूर्वावलोकन", "confirm": "पुष्टि करें", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "समस्याएं", "show side buttons": "साइड बटन दिखाएं", "bug_report": "बग रिपोर्ट सबमिट करें", "verified publisher": "सत्यापित प्रकाशक", "most_downloaded": "सर्वाधिक डाउनलोड", "newly_added": "नया नया़ा", "top_rated": "टॉप रेटेड", "rename not supported": "Termux डायरेक्टरी में रीनेम करना संभव नहीं है", "compress": "कम्प्रेस करें", "copy uri": "URI कॉपी करें", "delete entries": "क्या आप वाकई {count} आइटम हटाना चाहते हैं?", "deleting items": "हटाए जा रहे {count} आइटम...", "import project zip": "प्रोजेक्ट आयात करें (zip)", "changelog": "चेंज लॉग", "notifications": "सूचनाएँ", "no_unread_notifications": "कोई अवांछित सूचनाएँ नहीं", "should_use_current_file_for_preview": "डिफ़ॉल्ट (index.html) के बजाय पूर्वावलोकन के लिए वर्तमान फ़ाइल का उपयोग करना चाहिए", "fade fold widgets": "फेड फोल्ड विजेट्स", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "प्लगइन सक्रिय है", "plugin_disabled": "प्लगइन निष्क्रिय है", "enable_plugin": "इस प्लगइन को सक्षम करें", "disable_plugin": "इस प्लगइन को अक्षम करें", "open_source": "ओपन सोर्स", "terminal settings": "टर्मिनल सेटिंग्स", "font ligatures": "फॉन्ट लिगेचर्स", "letter spacing": "लेटर स्पेसिंग", "terminal:tab stop width": "टैब स्टॉप चौड़ाई", "terminal:scrollback": "स्क्रॉलबैक लाइन्स", "terminal:cursor blink": "कर्सर ब्लिंक", "terminal:font weight": "फ़ॉन्ट मोटाई", "terminal:cursor inactive style": "इनएक्टिव कर्सर स्टाइल", "terminal:cursor style": "कर्सर स्टाइल", "terminal:font family": "फ़ॉन्ट फैमिली", "terminal:convert eol": "EOL रूपांतरित करें", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "टर्मिनल", "allFileAccess": "ऑल फ़ाइल एक्सेस", "fonts": "फॉन्ट्स", "sponsor": "स्पॉन्सर", "downloads": "डाउनलोड", "reviews": "समीक्षाएँ", "overview": "ओवरव्यू", "contributors": "सहयोगी", "quicktools:hyphen": "हाइफ़न प्रतीक डालें", "check for app updates": "ऐप अपडेट की जांच करें", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/hu-hu.json ================================================ { "lang": "Magyar", "about": "Névjegy", "active files": "Megnyitott fájlok", "alert": "Figyelmeztetés", "app theme": "Alkalmazás témája", "autocorrect": "Engedélyezi az automatikus javítást?", "autosave": "Automatikus mentés", "cancel": "Mégse", "change language": "Nyelv módosítása", "choose color": "Válasszon színt", "clear": "Tisztítás", "close app": "Biztosan bezárja az alkalmazást?", "commit message": "Véglegesítési üzenet", "console": "Konzol", "conflict error": "Ütközés! Várjon egy újabb véglegesítés előtt.", "copy": "Másolás", "create folder error": "Nem sikerült új mappát létrehozni", "cut": "Kivágás", "delete": "Törlés", "dependencies": "Függőségek", "delay": "Idő ezredmásodpercben", "editor settings": "Szerkesztő beállításai", "editor theme": "Szerkesztő témája", "enter file name": "Adja meg a fájl nevét", "enter folder name": "Adja meg a mappa nevét", "empty folder message": "Üres mappa", "enter line number": "Adja meg a sor számát", "error": "Hiba", "failed": "Nem sikerült", "file already exists": "A fájl már létezik", "file already exists force": "A fájl már létezik. Felülírja?", "file changed": " A fájl módosult, újratölti?", "file deleted": "Fájl törölve", "file is not supported": "A fájl nem támogatott", "file not supported": "Ez a fájltípus nem támogatott.", "file too large": "A fájl túl nagy a kezeléshez. A maximum fájlméret: {size}", "file renamed": "Fájl átnevezve", "file saved": "Fájl mentve", "folder added": "Mappa hozzáadva", "folder already added": "A mappa már hozzá van adva", "font size": "Betűméret", "goto": "Ugrás a sorhoz", "icons definition": "Ikonok definíciója", "info": "Információ", "invalid value": "Érvénytelen érték", "language changed": "A nyelv sikeresen módosítva lett", "linting": "Szintaxishiba ellenőrzése", "logout": "Kijelentkezés", "loading": "Betöltés", "my profile": "Saját profil", "new file": "Új fájl", "new folder": "Új mappa", "no": "Nem", "no editor message": "Nyisson meg vagy hozzon létre egy új fájlt vagy mappát a menüből", "not set": "Nincs beállítva", "unsaved files close app": "Mentetlen fájlok vannak megnyitva. Biztosan bezárja az alkalmazást?", "notice": "Megjegyzés", "open file": "Fájl megnyitása", "open files and folders": "Fájlok és mappák megnyitása", "open folder": "Mappa megnyitása", "open recent": "Legutóbbi megnyitása", "ok": "OK", "overwrite": "Felülírás", "paste": "Beillesztés", "preview mode": "Előnézeti mód", "read only file": "Nem menthet csak olvasható fájlt. Próbálja menteni másként", "reload": "Újratöltés", "rename": "Átnevezés", "replace": "Csere", "required": "Ez a mező kötelező", "run your web app": "Webalkalmazás futtatása", "save": "Mentés", "saving": "Mentés…", "save as": "Mentés másként", "save file to run": "Mentse el a fájlt a böngészőben való futtatáshoz", "search": "Keresés", "see logs and errors": "Naplók és hibák megjelenítése", "select folder": "Mappa kiválasztása", "settings": "Beállítások", "settings saved": "Beállítások mentve", "show line numbers": "Sorszámok megjelenítése", "show hidden files": "Rejtett fájlok megjelenítése", "show spaces": "Szóközök megjelenítése", "soft tab": "Szóköztabulátor", "sort by name": "Rendezés név szerint", "success": "Siker", "tab size": "Tabulátor mérete", "text wrap": "Szövegtördelés", "theme": "Téma", "unable to delete file": "Nem lehet törölni a fájlt", "unable to open file": "Nem lehet megnyitni a fájlt", "unable to open folder": "Nem lehet megnyitni a mappát", "unable to save file": "Nem lehet menteni a fájlt", "unable to rename": "Nem lehet átnevezni", "unsaved file": "A fájl nincs mentve, biztosan bezárja?", "warning": "Figyelmeztetés", "use emmet": "Emmet használata", "use quick tools": "Gyors-eszközök használata", "yes": "Igen", "encoding": "Szövegkódolás", "syntax highlighting": "Szintaxiskiemelés", "read only": "Csak olvasható", "select all": "Összes kijelölése", "select branch": "Ág kiválasztása", "create new branch": "Új ág létrehozása", "use branch": "Ág használata", "new branch": "Új ág", "branch": "Ág", "key bindings": "Billentyűparancsok", "edit": "Szerkesztés", "reset": "Visszaállítás", "color": "Szín", "select word": "Szó kiválasztása", "quick tools": "Gyors-eszközök", "select": "Kiválasztás", "editor font": "Szerkesztő betűtípusa", "new project": "Új projekt", "format": "Formátum", "project name": "Projekt neve", "unsupported device": "Ez az eszköz nem támogatja a témát.", "vibrate on tap": "Rezgés érintésre", "copy command is not supported by ftp.": "Az FTP nem támogatja a másolást.", "support title": "Támogatás", "fullscreen": "Teljes képernyő", "animation": "Animáció", "backup": "Biztonsági mentés", "restore": "Visszaállítás", "backup successful": "Sikeres biztonsági mentés", "invalid backup file": "Érvénytelen mentési fájl", "add path": "Útvonal hozzáadása", "live autocompletion": "Élő automatikus kiegészítés", "file properties": "Fájl tulajdonságai", "path": "Útvonal", "type": "Típus", "word count": "Szavak száma", "line count": "Sorok száma", "last modified": "Utoljára módosítva", "size": "Méret", "share": "Megosztás", "show print margin": "Nyomtatási margó megjelenítése", "login": "Bejelentkezés", "scrollbar size": "Görgetősáv mérete", "cursor controller size": "Kurzorirányító mérete", "none": "Egyik sem", "small": "Kicsi", "large": "Nagy", "floating button": "Lebegő gomb", "confirm on exit": "Megerősítés kérése kilépéskor", "show console": "Konzol megjelenítése", "image": "Kép", "insert file": "Kép beszúrása", "insert color": "Szín beszúrása", "powersave mode warning": "Kapcsolja ki az energiatakarékos üzemmódot a külső böngészőben történő előnézethez.", "exit": "Kilépés", "custom": "Egyedi", "reset warning": "Biztosan visszaállítja a témát?", "theme type": "Tématípus", "light": "Világos", "dark": "Sötét", "file browser": "Fájlböngésző", "operation not permitted": "A művelet nem engedélyezett", "no such file or directory": "Nincs ilyen fájl vagy könyvtár", "input/output error": "Be-/kimeneti hiba", "permission denied": "Hozzáférés megtagadva", "bad address": "Hibás cím", "file exists": "A fájl már létezik", "not a directory": "Nem egy könyvtár", "is a directory": "Ez egy könyvtár", "invalid argument": "Érvénytelen argumentum", "too many open files in system": "Túl sok a megnyitott fájl a rendszerben", "too many open files": "Túl sok a megnyitott fájl", "text file busy": "A szöveges fájl foglalt", "no space left on device": "Nincs több hely az eszközön", "read-only file system": "Csak olvasható fájlrendszer", "file name too long": "Túl hosszú fájlnév", "too many users": "Túl sok felhasználó", "connection timed out": "Lejárt kapcsolat", "connection refused": "Visszautasított kapcsolat", "owner died": "A tulajdonos meghalt", "an error occurred": "Hiba történt", "add ftp": "FTP hozzáadása", "add sftp": "SFTP hozzáadása", "save file": "Fájl mentése", "save file as": "Fájl mentése másként", "files": "Fájlok", "help": "Súgó", "file has been deleted": "A(z) {file} fájl törölve lett!", "feature not available": "Ez a funkció csak az alkalmazás fizetős verziójában érhető el.", "deleted file": "Törölt fájl", "line height": "Sormagasság", "preview info": "Ha az aktív fájlt szeretné futtatni, koppintson és tartsa lenyomva a lejátszás ikont.", "manage all files": "Engedélyezze az Acode szerkesztőnek az összes fájl kezelését a beállításokban, hogy könnyen szerkeszthesse a fájlokat az eszközén.", "close file": "Fájl bezárása", "reset connections": "Kapcsolatok visszaállítása", "check file changes": "Fájlmódosítások ellenőrzése", "open in browser": "Megnyitás böngészőben", "desktop mode": "Asztali mód", "toggle console": "Konzol átkapcsolása", "new line mode": "Új sor mód", "add a storage": "Tárhely hozzáadása", "rate acode": "Acode értékelése", "support": "Támogatás", "downloading file": "A(z) {file} fájl letöltése", "downloading...": "Letöltés…", "folder name": "Mappanév", "keyboard mode": "Billentyűzetmód", "normal": "Normál", "app settings": "Alkalmazás-beállítások", "disable in-app-browser caching": "Az alkalmazáson belüli böngésző gyorsítótárazásának letiltása", "copied to clipboard": "Vágólapra másolva", "remember opened files": "Megnyitott fájlok megjegyzése", "remember opened folders": "Megnyitott mappák megjegyzése", "no suggestions": "Javaslatok nélkül", "no suggestions aggressive": "Javaslatok nélkül (agresszív)", "install": "Telepítés", "installing": "Telepítés…", "plugins": "Bővítmények", "recently used": "Legutóbb használt", "update": "Frissítés", "uninstall": "Eltávolítás", "download acode pro": "Acode Pro letöltése", "loading plugins": "Bővítmények betöltése", "faqs": "GYIK", "feedback": "Visszajelzés", "header": "Fejléc", "sidebar": "Oldalsáv", "inapp": "Alkalmazáson belüli", "browser": "Böngésző", "diagonal scrolling": "Átlós görgetés", "reverse scrolling": "Fordított görgetés", "formatter": "Formátumkészítő", "format on save": "Formátum mentéskor", "remove ads": "Reklámok eltávolítása", "fast": "Gyors", "slow": "Lassú", "scroll settings": "Görgetési beállítások", "scroll speed": "Görgetési sebesség", "loading...": "Betöltés…", "no plugins found": "Nem található bővítmény", "name": "Név", "username": "Felhasználónév", "optional": "nem kötelező", "hostname": "Kiszolgálónév", "password": "Jelszó", "security type": "Biztonsági típus", "connection mode": "Kapcsolati mód", "port": "Port", "key file": "Kulcsfájl", "select key file": "Kulcsfájl kiválasztása", "passphrase": "Jelmondat", "connecting...": "Kapcsolódás…", "type filename": "Fájlnév megadása", "unable to load files": "Nem sikerült betölteni a fájlokat", "preview port": "Port előnézete", "find file": "Fájl keresése", "system": "Rendszer", "please select a formatter": "Válasszon formátumkészítőt", "case sensitive": "Kis- és nagybetűk megkülönböztetése", "regular expression": "Reguláris kifejezések", "whole word": "Illesztés csak teljes szóra", "edit with": "Szerkesztés ezzel", "open with": "Megnyitás ezzel", "no app found to handle this file": "Nem található alkalmazás a fájl kezelésére", "restore default settings": "Alapértelmezett beállítások visszaállítása", "server port": "Kiszolgáló port", "preview settings": "Előnézet-beállítások", "preview settings note": "Ha az „Port előnézete” és a „Kiszolgáló portja” különbözik, az alkalmazás nem indítja el a kiszolgálót, hanem megnyitja a https://: címet a böngészőben vagy az alkalmazáson belüli böngészőben. Ez akkor hasznos, ha máshol futtatja a kiszolgálót.", "backup/restore note": "Ez csak a beállításokat, az egyéni témát és a billentyűparancsokat fogja menteni. Nem készít biztonsági mentést az FTP/SFTP-kről.", "host": "Kiszolgáló", "retry ftp/sftp when fail": "Újrapróbálkozás FTP/SFTP-sikertelenség esetén.", "more": "Több", "thank you :)": "Köszönöm! :)", "purchase pending": "Vásárlás folyamatban…", "cancelled": "Megszakítva", "local": "Helyi", "remote": "Távoli", "show console toggler": "Konzolkapcsoló megjelenítése", "binary file": "Ez a fájl bináris adatokat tartalmaz, biztosan meg akarja nyitni?", "relative line numbers": "Relatív sorszámok", "elastic tabstops": "Rugalmas tabulátor", "line based rtl switching": "Sor alapú RTL-váltás", "hard wrap": "Kemény törés", "spellcheck": "Helyesírás-ellenőrzés", "wrap method": "Tördelési módszer", "use textarea for ime": "Szövegterület használata az IME-hez", "invalid plugin": "Érvénytelen bővítmény", "type command": "Parancs beírása", "plugin": "Bővítmény", "quicktools trigger mode": "Gyors-eszközök aktiválási módja", "print margin": "Nyomtatási margó", "touch move threshold": "Érintéses mozgatás küszöbértéke", "info-retryremotefsafterfail": "Újrapróbálkozás FTP/SFTP-sikertelenség esetén", "info-fullscreen": "Címsor elrejtése a kezdőképernyőn.", "info-checkfiles": "Fájlmódosítások ellenőrzése, amikor az alkalmazás a háttérben van.", "info-console": "Válassza a JavaScript konzolt. A Legacy az alapértelmezett konzol, az Eruda egy harmadik fél konzolja.", "info-keyboardmode": "Billentyűzetmód a szövegbevitelhez, a „Javaslatok nélkül” elrejti a javaslatokat és az automatikus javítást. Ha a „Javaslatok nélkül” nem működik, módosítsa az értéket a következőre: „Javaslatok nélkül (agresszív)”.", "info-rememberfiles": "Megnyitott fájlok megjegyzése az alkalmazás bezárásakor.", "info-rememberfolders": "Megnyitott mappák megjegyzése az alkalmazás bezárásakor.", "info-floatingbutton": "Gyors-eszközök lebegő gombjának megjelenítése vagy elrejtése.", "info-openfilelistpos": "Hol jelenjen meg az aktív fájlok listája.", "info-touchmovethreshold": "Ha a készülék érintésérzékenysége túl magas, növelheti ezt az értéket, hogy megakadályozza a véletlen mozgatást.", "info-scroll-settings": "Ez a beállítás tartalmazza a görgetési beállításokat, beleértve a szövegtördelést is.", "info-animation": "Ha az alkalmazás késedelmesnek tűnik, tiltsa le az animációt.", "info-quicktoolstriggermode": "Ha a „Gyors-eszközök” gombja nem működik, akkor módosítsa ezt az értéket.", "info-checkForAppUpdates": "Alkalmazás-frissítések automatikus ellenőrzése.", "info-quickTools": "Gyors-eszközök megjelenítése vagy elrejtése.", "info-showHiddenFiles": "Rejtett fájlok és mappák megjelenítése (, amelyek nevei „.” ponttal kezdődnek)", "info-all_file_access": "Hozzáférés engedélyezése az „/sdcard” és „/storage” mappához a terminálban.", "info-fontSize": "Szöveg rendereléséhez használt betűméret.", "info-fontFamily": "Szöveg rendereléséhez használt betűtípus.", "info-theme": "Terminál színtémája.", "info-cursorStyle": "Kurzor stílusa, amikor a terminál van fókuszban.", "info-cursorInactiveStyle": "Kurzor stílusa, amikor nem a terminál van fókuszban.", "info-fontWeight": "Nem félkövér szöveg rendereléséhez használt betűvastagság.", "info-cursorBlink": "Független attól, hogy a kurzor villog-e.", "info-scrollback": "Sorok visszagörgetésének (előzmények) száma a terminálban. A visszagörgetés (előzmények) az a sormennyiség, amely megmarad, miután a sorok a kiinduló munkalapon túlra görgetődnek.", "info-tabStopWidth": "Tabulátor mérete a terminálban.", "info-letterSpacing": "Karakterek közötti térköz egész pixelekben megadva.", "info-imageSupport": "Független attól, hogy a terminál támogatja-e a képeket.", "info-fontLigatures": "Független attól, hogy a betűtípus-ligatúrák engedélyezve vannak-e a terminálban.", "info-confirmTabClose": "Megerősítés kérése a terminál lapjainak bezárása előtt.", "info-backup": "Biztonsági mentést készít a telepített terminálról.", "info-restore": "Visszaállít egy biztonsági mentést a telepített terminálról.", "info-uninstall": "Eltávolítja a jelenleg telepített terminált.", "owned": "Saját tulajdonú", "api_error": "Az API-kiszolgáló leállt, próbálja meg később.", "installed": "Telepített", "all": "Összes", "medium": "Közepes", "refund": "Visszatérítés", "product not available": "A termék nem érhető el", "no-product-info": "Ez a termék jelenleg nem érhető el az Ön országában, próbálja meg később.", "close": "Bezárás", "explore": "Felfedezés", "key bindings updated": "Billentyűparancsok frissítve", "search in files": "Keresés a fájlokban", "exclude files": "Fájlok kizárása", "include files": "Fájlok felvétele", "search result": "{matches} eredmény {files} fájlban.", "invalid regex": "Érvénytelen reguláris kifejezés: {message}.", "bottom": "Alul", "save all": "Összes mentése", "close all": "Összes bezárása", "unsaved files warning": "Egyes fájlok nem kerülnek mentésre. Koppintson az „OK” gombra, hogy mit tegyen, vagy koppintson a „Vissza” gombra a visszatéréshez.", "save all warning": "Biztosan el akarja menteni az összes fájlt és be akarja zárni? Ezt a műveletet nem lehet visszafordítani.", "save all changes warning": "Biztosan menti az összes fájlt?", "close all warning": "Biztosan bezárja az összes fájlt? Elveszíti a nem mentett módosításokat. Ez a művelet nem vonható vissza.", "refresh": "Frissítés", "shortcut buttons": "Gyors gombok", "no result": "Nincs eredmény", "searching...": "Keresés…", "quicktools:ctrl-key": "Ctrl-billentyű", "quicktools:tab-key": "Tabulátor-billentyű", "quicktools:shift-key": "Shift-billentyű", "quicktools:undo": "Visszavonás", "quicktools:redo": "Mégis", "quicktools:search": "Keresés fájlban", "quicktools:save": "Fájl mentése", "quicktools:esc-key": "Esc-billentyű", "quicktools:curlybracket": "Kapcsos zárójel beszúrása", "quicktools:squarebracket": "Szögletes zárójel beszúrása", "quicktools:parentheses": "Zárójel beszúrása", "quicktools:anglebracket": "Kúpos zárójel beszúrása", "quicktools:left-arrow-key": "Balra nyíl-billentyű", "quicktools:right-arrow-key": "Jobbra nyíl-billentyű", "quicktools:up-arrow-key": "Fel nyíl-billentyű", "quicktools:down-arrow-key": "Le nyíl-billentyű", "quicktools:moveline-up": "Sor mozgatása felfelé", "quicktools:moveline-down": "Sor mozgatása lefelé", "quicktools:copyline-up": "Sor másolása felfelé", "quicktools:copyline-down": "Sor másolása lefelé", "quicktools:semicolon": "Pontosvessző beszúrása", "quicktools:quotation": "Idézőjel beszúrása", "quicktools:and": "„ÉS” szimbólum beszúrása", "quicktools:bar": "Függőleges vonal beszúrása", "quicktools:equal": "Egyenlőségjel beszúrása", "quicktools:slash": "Perjel beszúrása", "quicktools:exclamation": "Felkiáltójel beszúrása", "quicktools:alt-key": "Alt-billentyű", "quicktools:meta-key": "Windows/Meta-billentyű", "info-quicktoolssettings": "Testre szabhatja a „Gyors-gombokat” és a billentyűket a szerkesztő alatti „Gyors-eszközök” tárolóban, hogy javítsa a kódolási élményt.", "info-excludefolders": "A **/node_modules/** minta használatával figyelmen kívül hagyhatja a node_modules mappában található összes fájlt. Ez kizárja a fájlokat a listából, és megakadályozza, hogy a fájlkeresésekben is szerepeljenek.", "missed files": "{count} fájl beolvasása a keresés megkezdése után, így nem lesznek benne a keresésben.", "remove": "Eltávolítás", "quicktools:command-palette": "Parancspaletta", "default file encoding": "Alapértelmezett fájlkódolás", "remove entry": "Biztosan el akarja távolítani a(z) „{name}” szót a mentett elérési utakból? Vegye figyelembe, hogy az eltávolítása nem törli magát az elérési utat.", "delete entry": "A(z) „{name}” törlésének megerősítése. Ez a művelet nem vonható vissza! Folytatja?", "change encoding": "A(z) „{file}” újranyitása „{encoding}” kódolással? Ez a művelet a fájlban végrehajtott, el nem mentett módosítások elvesztését eredményezi. Biztosan folytatja az újranyitást?", "reopen file": "Biztosan újranyitja a(z) „{file}” fájlt? Minden el nem mentett módosítás elvész.", "plugin min version": "A(z) „{name}” nevű bővítmény csak az Acode következő verziójától érhető el: {v-code}. Koppintson ide a frissítéshez.", "color preview": "Szín előnézete", "confirm": "Megerősítés", "list files": "Az összes fájl kilistázása itt: {name}? Túl sok fájl az alkalmazás összeomlását eredményezheti.", "problems": "Problémák", "show side buttons": "Oldalsó gombok megjelenítése", "bug_report": "Hibajelentés küldése", "verified publisher": "Hitelesített közzétevő", "most_downloaded": "Legtöbbször letöltött", "newly_added": "Újonnan hozzáadott", "top_rated": "Legjobbra értékelt", "rename not supported": "A Termux könyvtár átnevezése nem támogatott", "compress": "Tömörítés", "copy uri": "Uri másolása", "delete entries": "Biztosan töröl {count} elemet?", "deleting items": "{count} elem törlése…", "import project zip": "Projekt importálása zip-ből", "changelog": "Változáslista", "notifications": "Értesítések", "no_unread_notifications": "Nincsenek olvasatlan értesítések", "should_use_current_file_for_preview": "A jelenlegi fájl használata az előnézethez az alapértelmezett (index.html) helyett", "fade fold widgets": "Elhalványulás a modulok bezárásakor", "quicktools:home-key": "Ugrás az elejére-billentyű", "quicktools:end-key": "Ugrás a végére-billentyű", "quicktools:pageup-key": "Lapozás felfelé-billentyű", "quicktools:pagedown-key": "Lapozás lefelé-billentyű", "quicktools:delete-key": "Törlés-billentyű", "quicktools:tilde": "Hullámvonal beszúrása", "quicktools:backtick": "Fordított félidézőjel beszúrása", "quicktools:hash": "Számjel beszúrása", "quicktools:dollar": "Dollárjel beszúrása", "quicktools:modulo": "Százalékjel beszúrása", "quicktools:caret": "Hatványjel beszúrása", "plugin_enabled": "Bővítmény engedélyezve", "plugin_disabled": "Bővítmény letiltva", "enable_plugin": "Bővítmény engedélyezése", "disable_plugin": "Bővítmény letiltása", "open_source": "Nyílt forráskódú", "terminal settings": "Terminálbeállítások", "font ligatures": "Betűtípus-ligatúrák", "letter spacing": "Betűköz", "terminal:tab stop width": "Tabulátor szélessége", "terminal:scrollback": "Sorok visszagörgetése (előzmények)", "terminal:cursor blink": "Kurzor villogása", "terminal:font weight": "Betűvastagság", "terminal:cursor inactive style": "Inaktív kurzor stílusa", "terminal:cursor style": "Kurzor stílusa", "terminal:font family": "Betűtípuscsalád", "terminal:convert eol": "Sorvégződések átalakítása", "terminal:confirm tab close": "Megerősítés kérése a terminál lapjainak bezárásakor", "terminal:image support": "Képek támogatása", "terminal": "Terminál", "allFileAccess": "Hozzáférés az összes fájlhoz", "fonts": "Betűtípusok", "sponsor": "Szponzor", "downloads": "letöltés", "reviews": "megjegyzés", "overview": "Áttekintés", "contributors": "Közreműködők", "quicktools:hyphen": "Kötőjel beszúrása", "check for app updates": "Alkalmazásfrissítések ellenőrzése", "prompt update check consent message": "Internetkapcsolat esetén az Acode ellenőrizheti az új alkalmazásfrissítéseket. Engedélyezi a frissítések ellenőrzését?", "keywords": "Kulcsszavak", "author": "Szerző", "filtered by": "Szűrési szempont", "clean install state": "Tiszta telepítési állapot", "backup created": "Biztonsági mentés létrehozva", "restore completed": "Helyreállítás kész", "restore will include": "Ez helyreállítja", "restore warning": "Ez a művelet nem vonható vissza. Folytatja?", "reload to apply": "Újratölti az alkalmazást a változtatások érvényesítéséhez?", "reload app": "Alkalmazás újratöltése", "preparing backup": "Felkészülés a biztonsági mentésre", "collecting settings": "Beállítások gyűjtése", "collecting key bindings": "Billentyűparancsok gyűjtése", "collecting plugins": "Bővítményinformációk gyűjtése", "creating backup": "Biztonsági mentési fájl létrehozása", "validating backup": "Biztonsági mentés érvényesítése", "restoring key bindings": "Billentyűparancsok helyreállítása", "restoring plugins": "Bővítmények helyreállítása", "restoring settings": "Beállítások helyreállítása", "legacy backup warning": "Ez egy régebbi biztonsági mentési formátum. Egyes funkciók korlátozottak lehetnek.", "checksum mismatch": "Ellenőrzőösszeg-eltérés - a biztonsági mentés fájlja módosult vagy megsérült.", "plugin not found": "Nem található bővítmény a rendszerleíró adatbázisban", "paid plugin skipped": "Fizetős bővítmény - nem található vásárlás", "source not found": "Már nem létezik a forrásfájl", "restored": "Helyreállítva", "skipped": "Kihagyva", "backup not valid object": "Nem érvényes objektum a biztonsági mentési fájl", "backup no data": "Nem tartalmaz helyreállítandó adatokat a biztonsági mentési fájl.", "backup legacy warning": "Ez egy régebbi biztonsági mentési formátum (v1). Egyes funkciók korlátozottak lehetnek.", "backup missing metadata": "Hiányzó biztonsági mentési metaadatok - egyes információk nem érhetők el", "backup checksum mismatch": "Ellenőrzőösszeg-eltérés - a biztonsági mentés fájlja módosult vagy megsérült. Óvatosan járjon el.", "backup checksum verify failed": "Nem ellenőrizhető az ellenőrző összeg", "backup invalid settings": "Érvénytelen a beállítások formátuma", "backup invalid keybindings": "Érvénytelen a billentyűparancsok formátuma", "backup invalid plugins": "Érvénytelen a telepített bővítmények formátuma", "issues found": "Problémák találhatók", "error details": "Hiba részletei", "active tools": "Aktív eszközök", "available tools": "Elérhető eszközök", "recent": "Legutóbbi fájlok", "command palette": "Parancspaletta megnyitása", "change theme": "Téma módosítása", "documentation": "Dokumentáció", "open in terminal": "Megnyitás terminálban", "developer mode": "Fejlesztői mód", "info-developermode": "Engedélyezze a fejlesztői eszközöket (Eruda) a bővítmények hibakereséséhez és az alkalmazás állapotának megfigyeléséhez. A megfigyelő az alkalmazás indításakor előkészítődik.", "developer mode enabled": "Fejlesztői mód engedélyezve. A parancspaletta használatával kapcsolhatja be/ki a megfigyelőt (Ctrl+Shift+I).", "developer mode disabled": "Fejlesztői mód letiltva", "copy relative path": "Relatív elérési útvonal másolása", "shortcut request sent": "Parancsikon-kérelem megnyitva. A befejezéshez koppintson a hozzáadás gombra.", "add to home screen": "Hozzáadás a kezdőképernyőhöz", "pin shortcuts not supported": "Ez az eszköz nem támogatja a kezdőképernyő-parancsikonokat.", "save file before home shortcut": "A kezdőképernyőhöz való hozzáadás előtt mentse el a fájlt.", "terminal_required_message_for_lsp": "A Terminál nincs telepítve. Először telepítse a Terminált, hogy használni tudja az LSP-kiszolgálókat.", "shift click selection": "Shift + koppintás/kattintás a kiválasztáshoz", "earn ad-free time": "Reklámmentesség szerzése egy kis időre", "indent guides": "Behúzási segédvonalak", "language servers": "Nyelvi kiszolgálók", "lint gutter": "Szintaxisellenőrzési margó megjelenítése", "rainbow brackets": "Szivárványszínű zárójelek", "lsp-add-custom-server": "Egyéni kiszolgáló hozzáadása", "lsp-binary-args": "Bináris argumentumok (JSON-tömb)", "lsp-binary-command": "Bináris parancs", "lsp-binary-path-optional": "Bináris útvonal (nem kötelező)", "lsp-check-command-optional": "Ellenőrző parancs (nem kötelező felülbírálás)", "lsp-checking-installation-status": "Telepítési állapot ellenőrzése…", "lsp-configured": "Beállítva", "lsp-custom-server-added": "Egyéni kiszolgáló hozzáadva", "lsp-default": "Alapértelmezett", "lsp-details-line": "Részletek: {details}", "lsp-edit-initialization-options": "Előkészítési beállítások szerkesztése", "lsp-empty": "Üres", "lsp-enabled": "Engedélyezve", "lsp-error-add-server-failed": "Nem sikerült hozzáadni a kiszolgálót", "lsp-error-args-must-be-array": "Az argumentumoknak JSON-tömbnek kell lenniük", "lsp-error-binary-command-required": "A bináris parancs megadása kötelező", "lsp-error-language-id-required": "Legalább egy nyelvazonosító szükséges", "lsp-error-package-required": "Legalább egy csomag szükséges", "lsp-error-server-id-required": "Kiszolgálóazonosító szükséges", "lsp-feature-completion": "Kódkiegészítés", "lsp-feature-completion-info": "Automatikus kiegészítési javaslatok engedélyezése a kiszolgálótól.", "lsp-feature-diagnostics": "Diagnosztika", "lsp-feature-diagnostics-info": "Hibák és figyelmeztetések megjelenítése a nyelvi kiszolgálótól.", "lsp-feature-formatting": "Formázás", "lsp-feature-formatting-info": "Kódformázás engedélyezése a nyelvi kiszolgálótól.", "lsp-feature-hover": "Felugró információk", "lsp-feature-hover-info": "Típusinformációk és dokumentáció megjelenítése rámutatáskor.", "lsp-feature-inlay-hints": "Beágyazott tippek", "lsp-feature-inlay-hints-info": "Beágyazott típustippek megjelenítése a szerkesztőben.", "lsp-feature-signature": "Szignatúra-súgó", "lsp-feature-signature-info": "Függvényparaméter-tippek megjelenítése gépelés közben.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Előkészítési beállítások", "lsp-initialization-options-json": "Előkészítési beállítások (JSON)", "lsp-initialization-options-updated": "Előkészítési beállítások frissítve", "lsp-install-command": "Telepítési parancs", "lsp-install-command-unavailable": "Nem érhető el a telepítési parancs", "lsp-install-info-check-failed": "Az Acode nem tudta ellenőrizni a telepítési állapotot.", "lsp-install-info-missing": "A nyelvi kiszolgáló nincs telepítve a terminálkörnyezetben.", "lsp-install-info-ready": "A nyelvi kiszolgáló telepítve van és készen áll.", "lsp-install-info-unknown": "A telepítési állapot automatikus ellenőrzése nem lehetséges.", "lsp-install-info-version-available": "A {version} verzió elérhető.", "lsp-install-method-apk": "APK-csomag", "lsp-install-method-cargo": "Cargo-crate", "lsp-install-method-manual": "Kézi bináris", "lsp-install-method-npm": "npm-csomag", "lsp-install-method-pip": "pip-csomag", "lsp-install-method-shell": "Egyéni parancsértelmező", "lsp-install-method-title": "Telepítési mód", "lsp-install-repair": "Telepítés / javítás", "lsp-installation-status": "Telepítési állapot", "lsp-installed": "Telepítve", "lsp-invalid-timeout": "Érvénytelen időtúllépési érték", "lsp-language-ids": "Nyelvazonosítók (vesszővel elválasztva)", "lsp-packages-prompt": "{method}-csomagok (vesszővel elválasztva)", "lsp-remove-installed-files": "Eltávolítja a(z) {server} telepített fájljait?", "lsp-server-disabled-toast": "Kiszolgáló letiltva", "lsp-server-enabled-toast": "Kiszolgáló engedélyezve", "lsp-server-id": "Kiszolgálóazonosító", "lsp-server-label": "Kiszolgálócímke", "lsp-server-not-found": "Nem található a kiszolgáló", "lsp-server-uninstalled": "Kiszolgáló eltávolítva", "lsp-startup-timeout": "Indítási időtúllépés", "lsp-startup-timeout-ms": "Indítási időtúllépés (ezredmásodperc)", "lsp-startup-timeout-set": "Indítási időtúllépés beállítva: {timeout} ms", "lsp-state-disabled": "letiltva", "lsp-state-enabled": "engedélyezve", "lsp-status-check-failed": "Nem sikerült az ellenőrzés", "lsp-status-installed": "Telepítve", "lsp-status-installed-version": "Telepítve ({version})", "lsp-status-line": "Állapot: {status}", "lsp-status-not-installed": "Nincs telepítve", "lsp-status-unknown": "Ismeretlen", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Nem érhető el az eltávolítási parancs", "lsp-uninstall-server": "Kiszolgáló eltávolítása", "lsp-update-command-optional": "Frissítési parancs (nem kötelező)", "lsp-update-command-unavailable": "Nem érhető el a frissítési parancs", "lsp-update-server": "Kiszolgáló frissítése", "lsp-version-line": "Verzió: {version}", "lsp-view-initialization-options": "Előkészítési beállítások megtekintése", "settings-category-about-acode": "Az Acode névjegye", "settings-category-advanced": "Speciális", "settings-category-assistance": "Segítségnyújtás", "settings-category-core": "Alapvető beállítások", "settings-category-cursor": "Kurzor", "settings-category-cursor-selection": "Kurzor és kijelölés", "settings-category-custom-servers": "Egyéni kiszolgálók", "settings-category-customization-tools": "Testreszabás és eszközök", "settings-category-display": "Megjelenítés", "settings-category-editing": "Szerkesztés", "settings-category-features": "Funkciók", "settings-category-files-sessions": "Fájlok és munkamenetek", "settings-category-fonts": "Betűtípusok", "settings-category-general": "Általános", "settings-category-guides-indicators": "Segédvonalak és jelzők", "settings-category-installation": "Telepítés", "settings-category-interface": "Felület", "settings-category-maintenance": "Karbantartás", "settings-category-permissions": "Engedélyek", "settings-category-preview": "Előnézet", "settings-category-scrolling": "Görgetés", "settings-category-server": "Kiszolgáló", "settings-category-servers": "Kiszolgálók", "settings-category-session": "Munkamenet", "settings-category-support-acode": "Az Acode támogatása", "settings-category-text-layout": "Szöveg és elrendezés", "settings-info-app-animation": "Alkalmazáson belüli átmeneti animációk vezérlése.", "settings-info-app-check-files": "Szerkesztők frissítése, ha a fájlok az Acode-on kívül módosulnak.", "settings-info-app-clean-install-state": "Az első lépések és a beállítási folyamatok által használt tárolt telepítési állapot törlése.", "settings-info-app-confirm-on-exit": "Megerősítés kérése az alkalmazás bezárása előtt.", "settings-info-app-console": "Válassza ki, melyik hibakereső konzol-integrációt használja az Acode.", "settings-info-app-default-file-encoding": "Alapértelmezett kódolás fájlok megnyitásakor vagy létrehozásakor.", "settings-info-app-exclude-folders": "Mappák és minták kihagyása kereséskor vagy beolvasáskor.", "settings-info-app-floating-button": "A lebegő gyorsművelet-gomb megjelenítése.", "settings-info-app-font-manager": "Alkalmazás-betűtípusok telepítése, kezelése vagy eltávolítása.", "settings-info-app-fullscreen": "Rendszerszintű állapotsor elrejtése az Acode használatakor.", "settings-info-app-keybindings": "A billentyűparancs-fájl szerkesztése vagy a gyorsbillentyűk alaphelyzetbe állítása.", "settings-info-app-keyboard-mode": "Válassza ki, hogyan viselkedjen a szoftveres billentyűzet szerkesztéskor.", "settings-info-app-language": "Válassza ki az alkalmazás nyelvét és a lefordított feliratokat.", "settings-info-app-open-file-list-position": "Válassza ki, hol jelenjen meg az aktív fájlok listája.", "settings-info-app-quick-tools-settings": "Gyorseszköz-parancsikonok átrendezése és testreszabása.", "settings-info-app-quick-tools-trigger-mode": "Válassza ki, hogyan nyíljanak meg a gyorseszközök érintésre.", "settings-info-app-remember-files": "Legutóbb megnyitott fájlok újbóli megnyitása.", "settings-info-app-remember-folders": "Az előző munkamenet mappáinak újbóli megnyitása.", "settings-info-app-retry-remote-fs": "Távoli fájlműveletek megismétlése sikertelen átvitel után.", "settings-info-app-side-buttons": "További műveletgombok megjelenítése a szerkesztő mellett.", "settings-info-app-sponsor-sidebar": "Támogatói bejegyzés megjelenítése az oldalsávban.", "settings-info-app-touch-move-threshold": "Legkisebb elmozdulás az érintéses húzás érzékeléséhez.", "settings-info-app-vibrate-on-tap": "Haptikus visszajelzés engedélyezése érintésekhez és vezérlőkhöz.", "settings-info-editor-autosave": "Módosítások automatikus mentése egy bizonyos késleltetés után.", "settings-info-editor-color-preview": "Színértékek beágyazott előnézete a szerkesztőben.", "settings-info-editor-fade-fold-widgets": "Kódblokk-összecsukó jelölők elhalványítása, amíg nincs rájuk szükség.", "settings-info-editor-font-family": "Válassza ki a szerkesztőben használt betűtípust.", "settings-info-editor-font-size": "Szerkesztő szövegméretének beállítása.", "settings-info-editor-format-on-save": "Formázó futtatása minden alkalommal, amikor egy fájlt mentenek.", "settings-info-editor-hard-wrap": "Valódi soremelések beszúrása a tisztán vizuális tördelés helyett.", "settings-info-editor-indent-guides": "Behúzási segédvonalak megjelenítése.", "settings-info-editor-line-height": "Sorok közötti függőleges távolság beállítása.", "settings-info-editor-line-numbers": "Sorok számának megjelenítése a margón.", "settings-info-editor-lint-gutter": "Diagnosztikai és szintaxisellenőrző jelölők megjelenítése a margón.", "settings-info-editor-live-autocomplete": "Javaslatok megjelenítése gépelés közben.", "settings-info-editor-rainbow-brackets": "Összetartozó zárójelek színezése a beágyazási mélység alapján.", "settings-info-editor-relative-line-numbers": "Távolság megjelenítése a jelenlegi sortól.", "settings-info-editor-rtl-text": "Jobbról balra haladó viselkedés váltása soronként.", "settings-info-editor-scroll-settings": "Görgetősáv méretének, sebességének és az ujjmozdulatok viselkedésének beállítása.", "settings-info-editor-shift-click-selection": "Kijelölés kiterjesztése a Shift + érintés vagy kattintás használatával.", "settings-info-editor-show-spaces": "Látható szóközjelölők megjelenítése.", "settings-info-editor-soft-tab": "Szóközök beszúrása tabulátorkarakterek helyett.", "settings-info-editor-tab-size": "Állítsa be, hány szóközt használjon egy tabulátorlépés.", "settings-info-editor-teardrop-size": "Kurzorfogantyú méretének beállítása az érintéses szerkesztéshez.", "settings-info-editor-text-wrap": "Hosszú sorok tördelése a szerkesztőben.", "settings-info-lsp-add-custom-server": "Egyéni nyelvi kiszolgáló regisztrálása telepítési, frissítési és indítási parancsokkal.", "settings-info-lsp-edit-init-options": "Előkészítési beállítások szerkesztése JSON-formátumban.", "settings-info-lsp-install-server": "Ezen nyelvi kiszolgáló telepítése vagy javítása.", "settings-info-lsp-server-enabled": "Ezen nyelvi kiszolgáló engedélyezése vagy letiltása.", "settings-info-lsp-startup-timeout": "Állítsa be, hogy az Acode mennyi ideig várjon a kiszolgáló elindulására.", "settings-info-lsp-uninstall-server": "A kiszolgálóhoz tartozó telepített csomagok vagy binárisok eltávolítása.", "settings-info-lsp-update-server": "Ezen nyelvi kiszolgáló frissítése, ha elérhető frissítési folyamat.", "settings-info-lsp-view-init-options": "Érvényes előkészítési beállítások megtekintése JSON-formátumban.", "settings-info-main-ad-rewards": "Reklámok megtekintése az ideiglenes reklámmentes hozzáférés feloldásához.", "settings-info-main-app-settings": "Nyelv, alkalmazás viselkedése és gyorselérési eszközök.", "settings-info-main-backup-restore": "Beállítások exportálása biztonsági mentésbe vagy azok későbbi visszaállítása.", "settings-info-main-changelog": "Legutóbbi frissítések és kiadási megjegyzések megtekintése.", "settings-info-main-edit-settings": "Nyers settings.json fájl közvetlen szerkesztése.", "settings-info-main-editor-settings": "Betűtípusok, tabulátorok, javaslatok és a szerkesztő megjelenítése.", "settings-info-main-formatter": "Válasszon formázót minden támogatott nyelvhez.", "settings-info-main-lsp-settings": "Nyelvi kiszolgálók és szerkesztő-intelligencia konfigurálása.", "settings-info-main-plugins": "Telepített bővítmények és elérhető műveleteik kezelése.", "settings-info-main-preview-settings": "Előnézeti mód, kiszolgálóportok és a böngésző viselkedése.", "settings-info-main-rateapp": "Az Acode értékelése a Google Play áruházban.", "settings-info-main-remove-ads": "Teljesen reklámmentes hozzáférés feloldása.", "settings-info-main-reset": "Az Acode visszaállítása az alapértelmezett konfigurációra.", "settings-info-main-sponsors": "Az Acode folyamatos fejlesztésének támogatása.", "settings-info-main-terminal-settings": "Terminál-téma, betűtípus, kurzor és munkamenet viselkedése.", "settings-info-main-theme": "Alkalmazás-téma, kontraszt és egyéni színek.", "settings-info-preview-disable-cache": "A tartalom mindig töltődjön be újra az alkalmazáson belüli böngészőben.", "settings-info-preview-host": "Az előnézeti webcím megnyitásakor használt gépnév.", "settings-info-preview-mode": "Válassza ki, hol nyíljon meg az előnézet az indításkor.", "settings-info-preview-preview-port": "Az élő előnézeti kiszolgáló által használt port.", "settings-info-preview-server-port": "Az alkalmazás belső kiszolgálója által használt port.", "settings-info-preview-show-console-toggler": "A konzol gombjának megjelenítése az előnézetben.", "settings-info-preview-use-current-file": "A jelenlegi fájl előnyben részesítése az előnézet indításakor.", "settings-info-terminal-convert-eol": "Sorvégek átalakítása a terminálkimenet beillesztésekor vagy megjelenítésekor.", "settings-note-formatter-settings": "Rendeljen formázót minden nyelvhez. További lehetőségek feloldásához telepítsen formázó bővítményeket.", "settings-note-lsp-settings": "A nyelvi kiszolgálók automatikus kiegészítést, diagnosztikát, felugró részleteket és egyebeket nyújtanak. Itt telepíthet, frissíthet vagy definiálhat egyéni kiszolgálókat. A felügyelt telepítők a terminál/proot környezetben futnak.", "search result label singular": "találat", "search result label plural": "találat", "pin tab": "Lap rögzítése", "unpin tab": "Lap rögzítésének megszüntetése", "pinned tab": "Rögzített lap", "unpin tab before closing": "Lap rögzítésének megszüntetése bezárás előtt.", "app font": "Alkalmazás betűtípusa", "settings-info-app-font-family": "Válassza ki az alkalmazás egész felületén használandó betűtípust.", "lsp-transport-method-stdio": "STDIO (bináris parancs futtatása)", "lsp-transport-method-websocket": "WebSocket (kapcsolódás ws/wss webcímhez)", "lsp-websocket-url": "WebSocket webcím", "lsp-websocket-server-managed-externally": "Ezt a kiszolgálót külsőleg kezelik WebSocketen keresztül.", "lsp-error-websocket-url-invalid": "A WebSocket webcímnek ws:// vagy wss:// előtaggal kell kezdődnie", "lsp-error-websocket-url-required": "A WebSocket webcím megadása kötelező", "lsp-remove-custom-server": "Egyéni kiszolgáló eltávolítása", "lsp-remove-custom-server-confirm": "Eltávolítja a(z) {server} egyéni nyelvi kiszolgálót?", "lsp-custom-server-removed": "Egyéni kiszolgáló eltávolítva", "settings-info-lsp-remove-custom-server": "Eltávolítja ezt az egyéni nyelvi kiszolgálót az Acode alkalmazásból." } ================================================ FILE: src/lang/id-id.json ================================================ { "lang": "Bahasa Indonesia", "about": "Tentang", "active files": "Berkas Aktif", "alert": "Peringatan", "app theme": "Tema Aplikasi", "autocorrect": "Aktifkan koreksi otomatis?", "autosave": "Simpan Otomatis", "cancel": "Batal", "change language": "Ubah Bahasa", "choose color": "Pilih warna", "clear": "Bersihkan", "close app": "Tutup aplikasi?", "commit message": "Pesan komit", "console": "Konsol", "conflict error": "Konflik! Mohon tunggu sebelum komit lainnya.", "copy": "Salin", "create folder error": "Maaf, tidak dapat membuat folder baru", "cut": "Potong", "delete": "Hapus", "dependencies": "Dependensi", "delay": "Waktu dalam milidetik", "editor settings": "Pengaturan Editor", "editor theme": "Tema Editor", "enter file name": "Masukkan nama berkas", "enter folder name": "Masukkan nama folder", "empty folder message": "Folder Kosong", "enter line number": "Masukkan nomor baris", "error": "Kesalahan", "failed": "Gagal", "file already exists": "Berkas sudah ada", "file already exists force": "Berkas sudah ada. Timpa?", "file changed": " telah diubah, muat ulang berkas?", "file deleted": "Berkas dihapus", "file is not supported": "Berkas tidak mendukung", "file not supported": "Jenis berkas ini tidak didukung.", "file too large": "Berkas terlalu besar untuk ditangani. Ukuran maksimal berkas yang diizinkan adalah {size}", "file renamed": "Berkas berganti nama", "file saved": "Berkas disimpan", "folder added": "Folder ditambahkan", "folder already added": "Folder sudab ditambahkan", "font size": "Ukuran Font", "goto": "Pergi ke baris", "icons definition": "Definisi ikon", "info": "Info", "invalid value": "Nilai tidak valid", "language changed": "Bahasa telah diubah dengan sukses", "linting": "Cek kesalahan sintaks", "logout": "Keluar", "loading": "Memuat", "my profile": "Profil saya", "new file": "Berkas baru", "new folder": "Folder baru", "no": "Tidak", "no editor message": "Buka atau buat berkas dan folder baru dari menu", "not set": "Tidak ada set", "unsaved files close app": "Ada berkas yang belum disimpan. Tutup Aplikasi?", "notice": "Pemberitahuan", "open file": "Buka berkas", "open files and folders": "Buka berkas dan folder", "open folder": "Buka folder", "open recent": "Buka baru-baru ini", "ok": "Oke", "overwrite": "Timpa", "paste": "Tempel", "preview mode": "Mode Pratinjau", "read only file": "Tidak dapat menyimpan berkas hanya baca. Silakan coba simpan sebagai", "reload": "Muat Ulang", "rename": "Ubah Nama", "replace": "Ganti", "required": "Bidang ini diperlukan", "run your web app": "Jalankan aplikasi web anda", "save": "Simpan", "saving": "Menyimpan", "save as": "Simpan sebagai", "save file to run": "Silakan simpan berkas ini untuk berjalan di peramban", "search": "Cari", "see logs and errors": "Lihat log dan kesalahan", "select folder": "Pilih folder", "settings": "Pengaturan", "settings saved": "Pengaturan disimpan", "show line numbers": "Tampilkan nomor baris", "show hidden files": "Tampilkan berkas tersembunyi", "show spaces": "Tampilkan spasi", "soft tab": "Tab lunak", "sort by name": "Urutkan berdasarkan nama", "success": "Sukses", "tab size": "Ukuran Tab", "text wrap": "Bungkus Teks", "theme": "Tema", "unable to delete file": "Tidak dapat menghapus berkas", "unable to open file": "Maaf, tidak dapat membuka berkas", "unable to open folder": "Maaf, tidak dapat membuka folder", "unable to save file": "Maaf, tidak dapat menyimpan berkas", "unable to rename": "Maaf, tidak dapat mengganti nama", "unsaved file": "Berkas ini tidak disimpan, tutup?", "warning": "Peringatan", "use emmet": "Gunakan emmet", "use quick tools": "Gunakan alat cepat", "yes": "Iya", "encoding": "Enkoding Teks", "syntax highlighting": "Penyorotan Sintaks", "read only": "Hanya baca", "select all": "Pilih semua", "select branch": "Pilih cabang", "create new branch": "Buat cabang baru", "use branch": "Gunakan cabang", "new branch": "Cabang baru", "branch": "Cabang", "key bindings": "Binding kunci", "edit": "Edit", "reset": "Atur ulang", "color": "Warna", "select word": "Pilih kata", "quick tools": "Alat cepat", "select": "Pilih", "editor font": "Font Editor", "new project": "Proyek baru", "format": "Format", "project name": "Nama proyek", "unsupported device": "Perangkat Anda tidak mendukung tema.", "vibrate on tap": "Bergetar pada ketuk", "copy command is not supported by ftp.": "Perintah Salin tidak didukung oleh FTP.", "support title": "Dukung Acode", "fullscreen": "Layar penuh", "animation": "Animasi", "backup": "Cadangkan", "restore": "Mengembalikan", "backup successful": "Berhasil membuat cadangan", "invalid backup file": "Berkas cadangan tidak valid", "add path": "Tambah jalur", "live autocompletion": "Penyelesaian otomatis langsung", "file properties": "Properti berkas", "path": "Jalur", "type": "Jenis", "word count": "Jumlah kata", "line count": "Jumlah baris", "last modified": "Modifikasi terakhir", "size": "Ukuran", "share": "Bagikan", "show print margin": "Tampilkan margin cetak", "login": "Masuk", "scrollbar size": "Ukuran bar gulir", "cursor controller size": "Ukuran pengontrol kursor", "none": "Tidak ada", "small": "Kecil", "large": "Besar", "floating button": "Tombol mengambang", "confirm on exit": "Konfirmasi saat keluar", "show console": "Tampilkan konsol", "image": "Gambar", "insert file": "Menyisipkan berkas", "insert color": "Menyisipkan warna", "powersave mode warning": "Matikan mode hemat daya untuk pratinjau di peramban eksternal.", "exit": "Keluar", "custom": "Kostum", "reset warning": "Apakah Anda yakin ingin mengatur ulang tema?", "theme type": "Jenis tema", "light": "Terang", "dark": "Gelap", "file browser": "Penjelajah Berkas", "operation not permitted": "Operasi tidak diizinkan", "no such file or directory": "Tidak ada berkas atau direktori seperti itu", "input/output error": "Kesalahan masukan/keluaran", "permission denied": "Izin ditolak", "bad address": "Alamat yang buruk", "file exists": "Berkas ada", "not a directory": "Bukan direktori", "is a directory": "Adalah direktori", "invalid argument": "Argumen tidak valid", "too many open files in system": "Terlalu banyak berkas terbuka dalam sistem", "too many open files": "Terlalu banyak berkas terbuka", "text file busy": "Berkas teks sibuk", "no space left on device": "Tidak ada ruang yang tersisa di perangkat", "read-only file system": "Sistem berkas hanya-baca", "file name too long": "Nama berkas terlalu panjang", "too many users": "Terlalu banyak pengguna", "connection timed out": "Waktu koneksi habis", "connection refused": "Koneksi ditolak", "owner died": "Pemilik mati", "an error occurred": "Terjadi kesalahan", "add ftp": "Tambahkan FTP", "add sftp": "Tambahkan SFTP", "save file": "Simpan berkas", "save file as": "Simpan berkas sebagai", "files": "Berkas", "help": "Bantuan", "file has been deleted": "{file} telah dihapus!", "feature not available": "Fitur ini hanya tersedia dalam versi berbayar dari aplikasi.", "deleted file": "Berkas yang dihapus", "line height": "Tinggi baris", "preview info": "Jika Anda ingin menjalankan berkas aktif, ketuk dan tahan ikon Putar.", "manage all files": "Izinkan Acode Editor untuk mengelola semua berkas dalam pengaturan untuk mengedit berkas di perangkat Anda dengan mudah.", "close file": "Tutup berkas", "reset connections": "Atur ulang koneksi", "check file changes": "Periksa perubahan berkas", "open in browser": "Buka di peramban", "desktop mode": "Mode Desktop", "toggle console": "Alihkan Konsol", "new line mode": "Mode baris baru", "add a storage": "Tambahkan penyimpanan", "rate acode": "Nilai Acode", "support": "Dukung", "downloading file": "Mengunduh {file}", "downloading...": "Mengunduh...", "folder name": "Nama Folder", "keyboard mode": "Mode Papan Ketik", "normal": "Normal", "app settings": "Pengaturan Aplikasi", "disable in-app-browser caching": "Nonaktifkan caching peramban-dalam-aplikasi", "Should use Current File For preview instead of default (index.html)": "Harus menggunakan berkas saat ini untuk pratinjau alih-alih default (index.html)", "copied to clipboard": "Disalin ke clipboard", "remember opened files": "Ingat berkas yang dibuka", "remember opened folders": "Ingat folder yang dibuka", "no suggestions": "Tidak ada saran", "no suggestions aggressive": "Tidak ada saran yang agresif", "install": "Pasang", "installing": "Memasang...", "plugins": "Plugin", "recently used": "Baru - baru ini digunakan", "update": "Perbarui", "uninstall": "Copot pemasangan", "download acode pro": "Unduh Acode pro", "loading plugins": "Memuat plugin", "faqs": "FAQ", "feedback": "Umpan balik", "header": "Header", "sidebar": "Bilah sisi", "inapp": "Dalam aplikasi", "browser": "Peramban", "diagonal scrolling": "Pengguliran diagonal", "reverse scrolling": "Pengguliran terbalik", "formatter": "Pemformat", "format on save": "Format pada simpan", "remove ads": "Hapus iklan", "fast": "Cepat", "slow": "Lambat", "scroll settings": "Pengaturan gulir", "scroll speed": "Kecepatan gulir", "loading...": "Memuat...", "no plugins found": "Tidak ditemukan plugin", "name": "Nama", "username": "Nama pengguna", "optional": "Opsional", "hostname": "Nama host", "password": "Kata sandi", "security type": "Jenis keamanan", "connection mode": "Mode koneksi", "port": "Port", "key file": "Berkas kunci", "select key file": "Pilih berkas kunci", "passphrase": "Frasa sandi", "connecting...": "Menghubungkan...", "type filename": "Jenis nama berkas", "unable to load files": "Tidak dapat memuat berkas", "preview port": "Port pratinjau", "find file": "Temukan berkas", "system": "Sistem", "please select a formatter": "Mohon pilih pemformat", "case sensitive": "Sensitif huruf besar", "regular expression": "Ekspresi Regular", "whole word": "Seluruh kata", "edit with": "Edit dengan", "open with": "Buka dengan", "no app found to handle this file": "Tidak ditemukan aplikasi untuk menangani berkas ini", "restore default settings": "Kembalikan pengaturan default", "server port": "Port server", "preview settings": "Pengaturan pratinjau", "preview settings note": "Jika port pratinjau dan port server berbeda, aplikasi tidak akan memulai server dan sebaliknya akan membuka https://: di peramban atau peramban-dalam-aplikasi. Ini berguna ketika Anda menjalankan server di tempat lain.", "backup/restore note": "Ini hanya akan membuat cadangan pengaturan Anda, tema khusus, plugin yang dipasang, dan binding kunci. Ini tidak akan membuat cadangan FTP/SFTP atau status aplikasi Anda.", "host": "Host", "retry ftp/sftp when fail": "Ulangi FTP/SFTP ketika gagal", "more": "Lebih banyak", "thank you :)": "Terima kasih :)", "purchase pending": "Pembelian Tertunda", "cancelled": "Dibatalkan", "local": "Lokal", "remote": "Jarak Jauh", "show console toggler": "Tampilkan pengalih konsol", "binary file": "Berkas ini berisi data biner, apakah Anda ingin membukanya?", "relative line numbers": "Nomor garis relatif", "elastic tabstops": "Tabstop elastis", "line based rtl switching": "Beralih RTL berbasis garis", "hard wrap": "Bungkus keras", "spellcheck": "Periksa ejaan", "wrap method": "Metode Bungkus", "use textarea for ime": "Gunakan textarea untuk IME", "invalid plugin": "Plugin Tidak Valid", "type command": "Ketik perintah", "plugin": "Plugin", "quicktools trigger mode": "Mode Pemicu Alat Cepat", "print margin": "Cetak margin", "touch move threshold": "Ambang pergerakan sentuh", "info-retryremotefsafterfail": "Ulangi koneksi FTP/SFTP ketika gagal.", "info-fullscreen": "Sembunyikan Bilah Judul di Layar Beranda.", "info-checkfiles": "Periksa perubahan berkas ketika aplikasi di latar belakang.", "info-console": "Pilih konsol JavaScript. Legacy adalah konsol default, Eruda adalah konsol pihak ketiga.", "info-keyboardmode": "Mode keyboard untuk input teks, tidak ada saran akan menyembunyikan saran dan koreksi otomatis. Jika tidak ada saran tidak berfungsi, cobalah untuk mengubah nilai ke tanpa saran yang agresif.", "info-rememberfiles": "Ingat berkas yang dibuka ketika aplikasi ditutup.", "info-rememberfolders": "Ingat folder yang dibuka ketika aplikasi ditutup.", "info-floatingbutton": "Tampilkan atau sembunyikan tombol apung Alat Cepat.", "info-openfilelistpos": "Di mana untuk menampilkan daftar berkas aktif.", "info-touchmovethreshold": "Jika sensitivitas sentuhan perangkat Anda terlalu tinggi, Anda dapat meningkatkan nilai ini untuk mencegah gerakan sentuh yang tidak disengaja.", "info-scroll-settings": "Pengaturan ini berisi pengaturan gulir termasuk bungkus teks.", "info-animation": "Jika aplikasi terasa lag, nonaktifkan animasi.", "info-quicktoolstriggermode": "Jika tombol dalam alat cepat tidak berfungsi, cobalah untuk mengubah nilai ini.", "info-checkForAppUpdates": "Periksa pembaruan aplikasi secara otomatis.", "info-quickTools": "Tampilkan atau sembunyikan alat cepat.", "info-showHiddenFiles": "Tampilkan berkas dan folder tersembunyi. (Berawal dengan .)", "info-all_file_access": "Aktifkan akses ke /sdcard dan /storage di terminal.", "info-fontSize": "Ukuran font yang digunakan untuk menampilkan teks.", "info-fontFamily": "Jenis huruf yang digunakan untuk menampilkan teks.", "info-theme": "Tema warna terminal.", "info-cursorStyle": "Gaya kursor saat terminal sedang aktif.", "info-cursorInactiveStyle": "Gaya kursor saat terminal tidak terfokus.", "info-fontWeight": "Ketebalan font yang digunakan untuk menampilkan teks tidak tebal.", "info-cursorBlink": "Apakah kursor berkedip.", "info-scrollback": "Jumlah scrollback di terminal. Scrollback adalah jumlah baris yang dipertahankan ketika baris digulirkan melampaui tampilan awal.", "info-tabStopWidth": "Ukuran tab berhenti di terminal.", "info-letterSpacing": "Jarak antar karakter dalam piksel penuh.", "info-imageSupport": "Apakah gambar didukung di terminal.", "info-fontLigatures": "Apakah ligatur font diaktifkan di terminal.", "info-confirmTabClose": "Mintalah konfirmasi sebelum menutup tab terminal.", "info-backup": "Membuat cadangan instalasi terminal.", "info-restore": "Mengembalikan cadangan instalasi terminal.", "info-uninstall": "Menghapus instalasi terminal.", "owned": "Dimiliki", "api_error": "API server turun, silakan coba setelah beberapa waktu.", "installed": "Terpasang", "all": "Semua", "medium": "Medium", "refund": "Pengembalian dana", "product not available": "Produk tidak tersedia", "no-product-info": "Produk ini tidak tersedia di negara Anda saat ini, silakan coba lagi.", "close": "Tutup", "explore": "Jelajahi", "key bindings updated": "Binding kunci diperbarui", "search in files": "Cari dalam berkas", "exclude files": "Kecualikan berkas", "include files": "Sertakan berkas", "search result": "{matches} hasil dalam {files} berkas.", "invalid regex": "Ekspresi regular tidak valid: {message}.", "bottom": "Bawah", "save all": "Simpan semua", "close all": "Tutup semua", "unsaved files warning": "Beberapa berkas tidak disimpan. Klik 'Oke' pilih apa yang harus dilakukan atau tekan 'Batal' untuk kembali.", "save all warning": "Apakah Anda yakin ingin menyimpan semua berkas dan tutup? Tindakan ini tidak dapat dibalik.", "save all changes warning": "Apakah Anda yakin ingin menyimpan semua berkas?", "close all warning": "Apakah Anda yakin ingin menutup semua berkas? Anda akan kehilangan perubahan yang belum disimpan dan tindakan ini tidak dapat dibalik.", "refresh": "Segarkan", "shortcut buttons": "Tombol pintasan", "no result": "Tidak ada hasil", "searching...": "Mencari...", "quicktools:ctrl-key": "Kunci Control/Command", "quicktools:tab-key": "Kunci Tab", "quicktools:shift-key": "Kunci Shift", "quicktools:undo": "Membatalkan", "quicktools:redo": "Mengulangi", "quicktools:search": "Cari di berkas", "quicktools:save": "Simpan berkas", "quicktools:esc-key": "Kunci Escape", "quicktools:curlybracket": "Masukkan kurung kurawal", "quicktools:squarebracket": "Masukkan kurung persegi", "quicktools:parentheses": "Masukkan tanda kurung", "quicktools:anglebracket": "Masukkan kurung sudut", "quicktools:left-arrow-key": "Kunci panah kiri", "quicktools:right-arrow-key": "Kunci panah kanan", "quicktools:up-arrow-key": "Kunci panah atas", "quicktools:down-arrow-key": "Kunci panah bawah", "quicktools:moveline-up": "Pindahkan baris ke atas", "quicktools:moveline-down": "Pindahkan baris ke bawah", "quicktools:copyline-up": "Salin baris ke atas", "quicktools:copyline-down": "Salin baris ke bawah", "quicktools:semicolon": "Masukkan titik koma", "quicktools:quotation": "Masukkan kutipan", "quicktools:and": "Masukkan simbol dan", "quicktools:bar": "Masukkan simbol bar", "quicktools:equal": "Masukkan simbol sama dengan", "quicktools:slash": "Masukkan simbol slash", "quicktools:exclamation": "Masukkan tanda seru", "quicktools:alt-key": "Kunci Alt", "quicktools:meta-key": "Kunci Windows/Meta", "info-quicktoolssettings": "Kustomisasi tombol pintas dan tombol keyboard dalam wadah alat cepat di bawah editor untuk meningkatkan pengalaman pengkodean Anda.", "info-excludefolders": "Gunakan pola **/node_modules/** untuk mengabaikan semua berkas dari folder node_modules. Ini akan mengecualikan berkas dari terdaftar dan juga akan mencegah mereka dimasukkan dalam pencarian berkas.", "missed files": "Memindai {count} berkas setelah pencarian dimulai dan tidak akan dimasukkan dalam pencarian.", "remove": "Hapus", "quicktools:command-palette": "Palet perintah", "default file encoding": "Enkoding berkas default", "remove entry": "Apakah Anda yakin ingin menghapus '{name}' dari jalur yang disimpan? Harap dicatat bahwa menghapusnya tidak akan menghapus jalur itu sendiri.", "delete entry": "Konfirmasikan penghapusan: '{name}'. Tindakan ini tidak dapat diurungkan. Melanjutkan?", "change encoding": "Buka kembali '{file}' dengan enkoding '{encoding}'? Tindakan ini akan mengakibatkan hilangnya perubahan yang belum disimpan yang dilakukan pada berkas. Apakah Anda ingin melanjutkan pembukaan kembali?", "reopen file": "Apakah Anda yakin ingin membuka kembali '{file}'? Setiap perubahan yang belum disimpan akan hilang.", "plugin min version": "{name} hanya tersedia dalam Acode - {v-code} dan di atas. Klik di sini untuk memperbarui.", "color preview": "Pratinjau warna", "confirm": "Konfirmasi", "list files": "Daftar semua berkas dalam {name}? Terlalu banyak berkas dapat menyebabkan aplikasi rusak.", "problems": "Masalah", "show side buttons": "Tampilkan tombol samping", "bug_report": "Kirim Laporan Bug", "verified publisher": "Penerbit terverifikasi", "most_downloaded": "Paling Banyak Diunduh", "newly_added": "Baru Ditambahkan", "top_rated": "Peringkat Teratas", "rename not supported": "Mengganti nama pada direktori Termux tidak didukung", "compress": "Kompres", "copy uri": "Salin Uri", "delete entries": "Apakah Anda yakin ingin menghapus {count} item?", "deleting items": "menghapus {count} item...", "import project zip": "Impor Proyek (zip)", "changelog": "Catatan Perubahan", "notifications": "Notifikasi", "no_unread_notifications": "Tidak ada pemberitahuan yang belum dibaca", "should_use_current_file_for_preview": "Harus menggunakan berkas saat ini untuk pratinjau alih-alih default (index.html)", "fade fold widgets": "Widget Lipat Pudar", "quicktools:home-key": "Kunci Home", "quicktools:end-key": "Kunci End", "quicktools:pageup-key": "Kunci PageUp", "quicktools:pagedown-key": "Kunci PageDown", "quicktools:delete-key": "Kunci Delete", "quicktools:tilde": "Masukkan tanda gelombang", "quicktools:backtick": "Masukkan tanda kutip terbalik", "quicktools:hash": "Masukkan tanda pagar", "quicktools:dollar": "Masukkan simbol dolar", "quicktools:modulo": "Masukkan simbol modulus/persen", "quicktools:caret": "Masukkan tanda sisipan", "plugin_enabled": "Plugin diaktifkan", "plugin_disabled": "Plugin dinonaktifkan", "enable_plugin": "Aktifkan Plugin ini", "disable_plugin": "Nonaktifkan Plugin ini", "open_source": "Sumber Terbuka", "terminal settings": "Pengaturan Terminal", "font ligatures": "Ligatur Huruf", "letter spacing": "Jarak Antar Huruf", "terminal:tab stop width": "Lebar Hentian Tab", "terminal:scrollback": "Garis Gulir Balik", "terminal:cursor blink": "Kursor Berkedip", "terminal:font weight": "Berat Huruf", "terminal:cursor inactive style": "Gaya Kursor Tidak Aktif", "terminal:cursor style": "Gaya Kursor", "terminal:font family": "Keluarga Huruf", "terminal:convert eol": "Konversi Akhir Baris", "terminal:confirm tab close": "Konfirmasi penutupan tab terminal", "terminal:image support": "Dukungan gambar", "terminal": "Terminal", "allFileAccess": "Semua akses berkas", "fonts": "Huruf", "sponsor": "Sponsor", "downloads": "Unduhan", "reviews": "Ulasan", "overview": "Ikhtisar", "contributors": "Kontributor", "quicktools:hyphen": "Masukkan simbol tanda hubung", "check for app updates": "Periksa pembaruan aplikasi", "prompt update check consent message": "Acode dapat memeriksa pembaruan aplikasi baru saat Anda online. Aktifkan pemeriksaan pembaruan?", "keywords": "Kata kunci", "author": "Pembuat", "filtered by": "Disaring oleh", "clean install state": "Bersihkan kondisi instal", "backup created": "Cadangan dibuat", "restore completed": "Pemulihan selesai", "restore will include": "Ini akan memulihkan", "restore warning": "Tindakan ini tidak dapat dibatalkan. Lanjutkan?", "reload to apply": "Muat ulang untuk menerapkan perubahan?", "reload app": "Muat ulang aplikasi", "preparing backup": "Mempersiapkan cadangan", "collecting settings": "Mengumpulkan pengaturan", "collecting key bindings": "Mengumpulkan binding kunci", "collecting plugins": "Mengumpulkan informasi plugin", "creating backup": "Membuat berkas cadangan", "validating backup": "Memvalidasi cadangan", "restoring key bindings": "Mengembalikan binding kunci", "restoring plugins": "Memulihkan plugin", "restoring settings": "Memulihkan pengaturan", "legacy backup warning": "Ini adalah format pencadangan lama. Beberapa fitur mungkin terbatas.", "checksum mismatch": "Ketidakcocokan checksum - berkas cadangan mungkin telah dimodifikasi atau rusak.", "plugin not found": "Plugin tidak ditemukan di registri.", "paid plugin skipped": "Plugin berbayar - pembelian tidak ditemukan", "source not found": "Berkas sumber tidak lagi ada.", "restored": "Dipulihkan", "skipped": "Dilewati", "backup not valid object": "Berkas cadangan bukan objek yang valid", "backup no data": "Berkas cadangan tidak berisi data untuk dipulihkan.", "backup legacy warning": "Ini adalah format pencadangan lama (v1). Beberapa fitur mungkin terbatas.", "backup missing metadata": "Metadata cadangan hilang - beberapa informasi mungkin tidak tersedia.", "backup checksum mismatch": "Ketidakcocokan checksum - berkas cadangan mungkin telah dimodifikasi atau rusak. Lanjutkan dengan hati-hati.", "backup checksum verify failed": "Checksum tidak dapat diverifikasi.", "backup invalid settings": "Format pengaturan tidak valid", "backup invalid keybindings": "Format keyBindings tidak valid", "backup invalid plugins": "Format installedPlugins tidak valid", "issues found": "Masalah ditemukan", "error details": "Detail kesalahan", "active tools": "Alat-alat yang aktif", "available tools": "Alat-alat yang tersedia", "recent": "Berkas terbaru", "command palette": "Buka Palet Perintah", "change theme": "Ganti tema", "documentation": "Dokumentasi", "open in terminal": "Buka di Terminal", "developer mode": "Mode Pengembang", "info-developermode": "Aktifkan alat pengembang (Eruda) untuk men-debug plugin dan memeriksa status aplikasi. Inspektor akan diinisialisasi saat aplikasi dimulai.", "developer mode enabled": "Mode pengembang diaktifkan. Gunakan palet perintah untuk mengaktifkan/menonaktifkan inspektor (Ctrl+Shift+I).", "developer mode disabled": "Mode pengembang dinonaktifkan", "copy relative path": "Salin jalur relatif", "shortcut request sent": "Permintaan pintasan dibuka. Tekan Tambah untuk menyelesaikan.", "add to home screen": "Tambah ke layar beranda", "pin shortcuts not supported": "Pintasan layar beranda tidak didukung di perangkat ini.", "save file before home shortcut": "Simpan berkas sebelum menambahkannya ke layar beranda.", "terminal_required_message_for_lsp": "Terminal tidak terpasang. Mohon pasang Terminal terlebih dahulu untuk menggunakan server LSP.", "shift click selection": "Pemilihan Shift + tekan/klik", "earn ad-free time": "Ambil waktu bebas iklan", "indent guides": "Panduan indentasi", "language servers": "Server bahasa", "lint gutter": "Tampilkan gutter lint", "rainbow brackets": "Kurung pelangi", "lsp-add-custom-server": "Tambah server kostum", "lsp-binary-args": "Argumen binari (senarai JSON)", "lsp-binary-command": "Perintah binari", "lsp-binary-path-optional": "Jalur binari (opsional)", "lsp-check-command-optional": "Perintah periksa (opsional timpa)", "lsp-checking-installation-status": "Memeriksa status pemasangan...", "lsp-configured": "Dikonfigurasi", "lsp-custom-server-added": "Server kostum ditambahkan", "lsp-default": "Bawaan", "lsp-details-line": "Detail: {details}", "lsp-edit-initialization-options": "Edit opsi inisiasi", "lsp-empty": "Kosong", "lsp-enabled": "Diaktifkan", "lsp-error-add-server-failed": "Gagal menambahkan server", "lsp-error-args-must-be-array": "Argumen harus berupa sebuah senarai JSON", "lsp-error-binary-command-required": "Perintah binari diperlukan", "lsp-error-language-id-required": "Setidaknya satu ID bahasa diperlukan", "lsp-error-package-required": "Setidaknya satu paket diperlukan", "lsp-error-server-id-required": "ID server diperlukan", "lsp-feature-completion": "Penyelesaian kode", "lsp-feature-completion-info": "Aktifkan saran penyelesaian otomatis dari server.", "lsp-feature-diagnostics": "Diagnostik", "lsp-feature-diagnostics-info": "Tampilkan kesalahan dan peringatan dari server bahasa.", "lsp-feature-formatting": "Pemformatan", "lsp-feature-formatting-info": "Aktifkan pemformatan kode dari server.", "lsp-feature-hover": "Informasi saat kursor diarahkan", "lsp-feature-hover-info": "Tampilkan informasi tipe dan dokumentasi saat kursor diarahkan ke atasnya.", "lsp-feature-inlay-hints": "Petunjuk sisipan", "lsp-feature-inlay-hints-info": "Tampilkan petunjuk pengetikan sebaris di editor.", "lsp-feature-signature": "Bantuan parameter fungsi", "lsp-feature-signature-info": "Tampilkan petunjuk parameter fungsi saat mengetik.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Opsi inisiasi", "lsp-initialization-options-json": "Opsi inisiasi (JSON)", "lsp-initialization-options-updated": "Opsi inisiasi diperbarui", "lsp-install-command": "Perintah pasang", "lsp-install-command-unavailable": "Perintah pasang tidak tersedia", "lsp-install-info-check-failed": "Acode tidak dapat menverifikasi status pemasangan.", "lsp-install-info-missing": "Server bahasa tidak terpasang di dalam lingkungan terminal.", "lsp-install-info-ready": "Server bahasa terpasang dan siap.", "lsp-install-info-unknown": "Status pemasangan tidak dapat diperiksa secara otomatis.", "lsp-install-info-version-available": "Versi {version} tersedia.", "lsp-install-method-apk": "Paket APK", "lsp-install-method-cargo": "Crate Cargo", "lsp-install-method-manual": "Binari manual", "lsp-install-method-npm": "Paket npm", "lsp-install-method-pip": "Paket pip", "lsp-install-method-shell": "Shell kostum", "lsp-install-method-title": "Metode pasang", "lsp-install-repair": "Pasang / perbaiki", "lsp-installation-status": "Status pemasangan", "lsp-installed": "Terpasang", "lsp-invalid-timeout": "Nilai timeout tidak valid", "lsp-language-ids": "ID bahasa (dipisah koma)", "lsp-packages-prompt": "{method} paket (dipisah koma)", "lsp-remove-installed-files": "Hapus berkas terpasang dari {server}?", "lsp-server-disabled-toast": "Server dinonaktifkan", "lsp-server-enabled-toast": "Server diaktifkan", "lsp-server-id": "ID server", "lsp-server-label": "Label server", "lsp-server-not-found": "Server tidak ditemukan", "lsp-server-uninstalled": "Server dicopot pemasangannya", "lsp-startup-timeout": "Batas waktu mulai", "lsp-startup-timeout-ms": "Batas waktu mulai (milidetik)", "lsp-startup-timeout-set": "Batas waktu mulai diatur ke {timeout} ms", "lsp-state-disabled": "dinonaktifkan", "lsp-state-enabled": "diaktifkan", "lsp-status-check-failed": "Pemeriksaan gagal", "lsp-status-installed": "Terpasang", "lsp-status-installed-version": "Terpasang ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Tidak terpasang", "lsp-status-unknown": "Tidak diketahui", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Perintah copot pemasangan tidak tersedia", "lsp-uninstall-server": "Copot pemasangan server", "lsp-update-command-optional": "Perintah perbarui (opsional)", "lsp-update-command-unavailable": "Perintah perbarui tidak tersedia", "lsp-update-server": "Perbarui server", "lsp-version-line": "Versi: {version}", "lsp-view-initialization-options": "Lihat opsi inisiasi", "settings-category-about-acode": "Tentang Acode", "settings-category-advanced": "Lanjutan", "settings-category-assistance": "Asisten", "settings-category-core": "Pengaturan inti", "settings-category-cursor": "Kursor", "settings-category-cursor-selection": "Kursor & pemilihan", "settings-category-custom-servers": "Kostum server", "settings-category-customization-tools": "Kostumisasi & alat", "settings-category-display": "Tampilan", "settings-category-editing": "Mengedit", "settings-category-features": "Fitur", "settings-category-files-sessions": "Berkas & sesi", "settings-category-fonts": "Huruf", "settings-category-general": "Umum", "settings-category-guides-indicators": "Panduan & indikator", "settings-category-installation": "Pemasangan", "settings-category-interface": "Antarmuka", "settings-category-maintenance": "Pemeliharaan", "settings-category-permissions": "Izin", "settings-category-preview": "Pratinjau", "settings-category-scrolling": "Pengguliran", "settings-category-server": "Server", "settings-category-servers": "Server", "settings-category-session": "Sesi", "settings-category-support-acode": "Dukung Acode", "settings-category-text-layout": "Teks & tampilan", "settings-info-app-animation": "Kendalikan animasi transisi di seluruh aplikasi.", "settings-info-app-check-files": "Segarkan ulang editor ketika berkas berubah di luar Acode.", "settings-info-app-clean-install-state": "Hapus status instalasi yang tersimpan yang digunakan oleh alur orientasi dan pengaturan.", "settings-info-app-confirm-on-exit": "Tanyakan sebelum menutup aplikasi.", "settings-info-app-console": "Pilih mana integrasi konsol debug yang Acode gunakan.", "settings-info-app-default-file-encoding": "Enkoding bawaan ketika membuka atau membuat berkas.", "settings-info-app-exclude-folders": "Lewati folder dan pola ketika mencari atau memindai.", "settings-info-app-floating-button": "Tampilkan tombol aksi cepat yang melayang.", "settings-info-app-font-manager": "Pasang, kelola, atau menghapus huruf aplikasi.", "settings-info-app-fullscreen": "Sembunyikan bar status sistem ketika menggunakan Acode.", "settings-info-app-keybindings": "Edit berkas binding kunci atau atur ulang pintasan.", "settings-info-app-keyboard-mode": "Pilih bagaimana papan ketik perangkat lunak berperilaku ketika mengedit.", "settings-info-app-language": "Pilih bahasa aplikasi dan label yang diterjemahkan.", "settings-info-app-open-file-list-position": "Pilih dimana daftar berkas aktif muncul.", "settings-info-app-quick-tools-settings": "Urutkan ulang dan kostumisaikan pintasan alat cepat.", "settings-info-app-quick-tools-trigger-mode": "Pilih bagaimana alat cepat terbuka saat ditekan atau disentuh.", "settings-info-app-remember-files": "Buka ulang berkas yang dibuka terakhir kali.", "settings-info-app-remember-folders": "Buka ulang folder dari sesi sebelumnya.", "settings-info-app-retry-remote-fs": "Ulangi lagi operasi berkas jarak jauh setelah transfer gagal.", "settings-info-app-side-buttons": "Tampilkan tombol aksi ekstra di samping editor.", "settings-info-app-sponsor-sidebar": "Tampilkan entri sponsor di bar samping.", "settings-info-app-touch-move-threshold": "Pergerakan minimum sebelum penyeretan sentuh terdeteksi.", "settings-info-app-vibrate-on-tap": "Aktifkan umpan balik getar untuk tekan dan kendali.", "settings-info-editor-autosave": "Simpan perubahan secara otomatis setelah jeda.", "settings-info-editor-color-preview": "Pratinjau nilai warna langsung di dalam editor.", "settings-info-editor-fade-fold-widgets": "Redupkan penanda lipatan hingga dibutuhkan.", "settings-info-editor-font-family": "Pilih tipe huruf yang digunakan di dalam editor.", "settings-info-editor-font-size": "Atur ukuran teks di editor.", "settings-info-editor-format-on-save": "Jalankan pemformat ketika sebuah berkas tersimpan.", "settings-info-editor-hard-wrap": "Masukkan pemotong baris asli daripada hanya membungkusnya secara visual.", "settings-info-editor-indent-guides": "Tampilkan baris panduan indentasi.", "settings-info-editor-line-height": "Atur jarak vertikal diantara baris.", "settings-info-editor-line-numbers": "Tampilkan nomor baris di dalam gutter.", "settings-info-editor-lint-gutter": "Tampilkan alat diagnostik dan penanda lint di gutter.", "settings-info-editor-live-autocomplete": "Tampilkan saran ketika anda mengetik.", "settings-info-editor-rainbow-brackets": "Warna menyamakan kurung dari kedalaman bersarang.", "settings-info-editor-relative-line-numbers": "Tampilkan jarak dari baris saat ini.", "settings-info-editor-rtl-text": "Ganti perilaku kanan-ke-kiri setiap baris.", "settings-info-editor-scroll-settings": "Atur ukuran bar gulir, kecepatan, dan perilaku gestur.", "settings-info-editor-shift-click-selection": "Perluas pemilihan dengan Shift + tekan atau klik.", "settings-info-editor-show-spaces": "Tampilkan penanda spasi yang terlihat.", "settings-info-editor-soft-tab": "Masukkan spasi daripada karakter tab.", "settings-info-editor-tab-size": "Atur seberapa banyak spasi setiap langkah tab digunakan.", "settings-info-editor-teardrop-size": "Atur ukuran penangan kursor untuk pengeditan sentuh.", "settings-info-editor-text-wrap": "Bungkus baris panjang di dalam editor.", "settings-info-lsp-add-custom-server": "Daftarkan server bahasa kostum dengan perintah pasang, perbarui, dan luncur.", "settings-info-lsp-edit-init-options": "Edit opsi inisiasi sebagai JSON.", "settings-info-lsp-install-server": "Pasang atau perbaiki server bahasa ini.", "settings-info-lsp-server-enabled": "Aktifkan atau nonaktifkan server bahasa ini.", "settings-info-lsp-startup-timeout": "Atur seberapa lama Acode menunggu untuk server memulai.", "settings-info-lsp-uninstall-server": "Hapus paket terpasang atau binari dari server ini.", "settings-info-lsp-update-server": "Perbarui server bahasa ini jika alur pembaruan tersedia.", "settings-info-lsp-view-init-options": "Lihat opsi inisiasi efektif sebagai JSON.", "settings-info-main-ad-rewards": "Tonton iklan untuk membuka akses bebas iklan sementara.", "settings-info-main-app-settings": "Bahasa, perilaku aplikasi, dan alat akses cepat.", "settings-info-main-backup-restore": "Ekspor pengaturan ke pencadangan atau atur ulang mereka nanti.", "settings-info-main-changelog": "Lihat perbaruan saat ini dan catatan rilis.", "settings-info-main-edit-settings": "Edit berkas mentah settings.json secara langsung.", "settings-info-main-editor-settings": "Huruf, tab, saran, dan tampilan editor.", "settings-info-main-formatter": "Pilih pemformat untuk setiap bahasa yang didukung.", "settings-info-main-lsp-settings": "Konfigurasi server bahasa dan kecerdasan editor.", "settings-info-main-plugins": "Kelola plugin terpasang dan aksi yang tersedia.", "settings-info-main-preview-settings": "Mode pratinjau, port server, dan perilaku peramban.", "settings-info-main-rateapp": "Nilai Acode di Google Play.", "settings-info-main-remove-ads": "Buka akses permanen bebas iklan.", "settings-info-main-reset": "Atur ulang Acode ke konfigurasi bawaannya.", "settings-info-main-sponsors": "Mendukung pengembangan Acode yang berkelanjutan.", "settings-info-main-terminal-settings": "Tema Terminal, huruf, kursor, dan perilaku sesi.", "settings-info-main-theme": "Tema aplikasi, kontras, dan warna kostum.", "settings-info-preview-disable-cache": "Selalu muat ulang konten di dalam peramban-dalam-aplikasi.", "settings-info-preview-host": "Nama host yang digunakan ketika membuka URL pratinjau.", "settings-info-preview-mode": "Pilih dimana pratinjau dibuka ketika anda meluncurkannya.", "settings-info-preview-preview-port": "Port yang digunakan oleh server pratinjau langsung.", "settings-info-preview-server-port": "Port yang digunakan oleh server internal aplikasi.", "settings-info-preview-show-console-toggler": "Tampilkan tombol konsol di dalam pratinjau.", "settings-info-preview-use-current-file": "Utamakan berkas saat ini ketika memulai pratinjau.", "settings-info-terminal-convert-eol": "Konversikan akhiran baris saat menempelkan atau menampilkan output terminal.", "settings-note-formatter-settings": "Tetapkan pemformat untuk setiap bahasa. Pasang plugin pemformat untuk membuka lebih banyak opsi.", "settings-note-lsp-settings": "Server bahasa menambahkan fitur pelengkapan otomatis, diagnostik, detail saat kursor diarahkan ke teks, dan banyak lagi. Anda dapat memasang, memperbarui, atau menentukan server khusus di sini. Penginstal terkelola berjalan di dalam lingkungan terminal/proot.", "search result label singular": "hasil", "search result label plural": "hasil", "pin tab": "Sematkan tab", "unpin tab": "Lepas sematan tab", "pinned tab": "Tab yang tersematkan", "unpin tab before closing": "Lepas sematan tab sebelum menutupnya.", "app font": "Huruf Aplikasi", "settings-info-app-font-family": "Pilih huruf yang digunakan di seluruh antarmuka aplikasi.", "lsp-transport-method-stdio": "STDIO (luncurkan perintah binari)", "lsp-transport-method-websocket": "WebSocket (hubungkan ke sebuah URL ws/wss)", "lsp-websocket-url": "URL WebSocket", "lsp-websocket-server-managed-externally": "Server ini dikelola secara eksternal lewat WebSocket.", "lsp-error-websocket-url-invalid": "URL WebSocket harus berawalan ws:// atau wss://", "lsp-error-websocket-url-required": "URL WebSocket diperlukan", "lsp-remove-custom-server": "Hapus server kostum", "lsp-remove-custom-server-confirm": "Hapus server bahasa kostum {server}?", "lsp-custom-server-removed": "Server kostum dihapus", "settings-info-lsp-remove-custom-server": "Hapus server bahasa kostum ini dari Acode." } ================================================ FILE: src/lang/ir-fa.json ================================================ { "lang": "فارسی - ترجمه صفا صفری", "about": "درباره ما", "active files": "فایلهای فعال", "alert": "هشدار", "app theme": "تم برنامه", "autocorrect": "فعالسازی اصلاح خودکار؟", "autosave": "ذخیره خودکار", "cancel": "انصراف", "change language": "انتخاب زبان", "choose color": "انتخاب رنگ", "clear": "واضح", "close app": "خروج از برنامه؟", "commit message": "تایید پیام", "console": "خط فرمان", "conflict error": "Conflict! Please wait before another commit.", "copy": "کپی", "create folder error": "متأسفم ، قادر به ایجاد پوشه جدید نیستم", "cut": "برش", "delete": "حذف", "dependencies": "Dependencies", "delay": "زمان در واحد میلی ثانیه", "editor settings": "تنظیمات ویرایشگر", "editor theme": "تم ویرایشگر", "enter file name": "نام فایل را وارد کنید", "enter folder name": "نام پوشه را وارد کنید", "empty folder message": "پوشه خالی", "enter line number": "شماره خط را وارد کنید", "error": "خطا", "failed": "ناموفق", "file already exists": "فایل در حال حاضر موجود میباشد", "file already exists force": "فایل در حال حاضر موجود میباشد ، رونویسی کنم؟", "file changed": " تغییر یافت دوباره بارگزاری کنم؟", "file deleted": "فایل پاک شده", "file is not supported": "فایل پشتیبانی نمیشود", "file not supported": "این نوع فایل پشتیبانی نمیشود.", "file too large": "{size} فایل برای نمایش خیلی بزرگ است حداکثر اندازه فایل مجاز", "file renamed": "نام فایل تغییر کرد", "file saved": "فایل ذخیره شد", "folder added": "فولدر ایجاد شد", "folder already added": "پوشه قبلاً اضافه شده است", "font size": "اندازه متن", "goto": "برو به خط", "icons definition": "تعریف آیکن ها", "info": "اطلاعات", "invalid value": "مقدار نامعتبر است", "language changed": "زبان برنامه با موفقیت تغییر یافت", "linting": "خطای علامتی را بررسی کنم", "logout": "خروج", "loading": "بارگذاری", "my profile": "پروفایل من", "new file": "فایل جدید", "new folder": "پوشه جدید", "no": "خیر", "no editor message": "فایل را انتخاب کنید و یا فایل و پوشه جدید را در فهرست ایجاد کنید", "not set": "تنظیم نشده", "unsaved files close app": "در اینجا فایل ذخیره نشده وجود دارد ، از برنامه خارج میشوید؟", "notice": "نکته", "open file": "باز کردن فایل", "open files and folders": "باز کردن فایل و پوشه", "open folder": "بازکردن پوشه", "open recent": "اخیراً را باز کنید", "ok": "تایید", "overwrite": "رونویسی", "paste": "چسباندن", "preview mode": "حالت پیش نمایش", "read only file": "نمیتوانم فایل های فقط خواندنی(read only) را ویرایش کنم. گزینه (ذخیره به عنوان) را امتحان کنید.", "redo defination": "پسرو", "reload": "بارگذاری مجدد", "rename": "تغییرنام", "replace": "جایگذاری", "required": "این فیلد لازم است", "run your web app": "اجرای برنامه وب", "save": "ذخیره", "saving": "در حال ذخیره", "save as": "ذخیره به عنوان", "save file to run": "لطفا این فایل را ذخیره کنید تا در مرورگر اجرا شود", "search": "جست و جو", "see logs and errors": "دیدن لاگ و خطا ها", "select folder": "انتخاب پوشه", "settings": "تنظیمات", "settings saved": "تنظیمات ذخیره شد", "show line numbers": "نمایش شماره خطوط", "show hidden files": "نمایش فایلهای مخفی", "show spaces": "نمایش فضاها", "soft tab": "فاصله با دکمه 'tab'", "sort by name": "مرتب سازی براساس نام", "success": "موفق", "tab size": "اندازه فاصله با دکمه tab", "text wrap": "بسته بندی متن", "theme": "تم", "unable to delete file": "نمیتوانم فایل را حذف کنم", "unable to open file": "متأسفم ، نمیتوانم فایل را باز کنم", "unable to open folder": "متأسفم ، نمیتوانم پوشه را باز کنم", "unable to save file": "متأسفم ، نمیتوانم فایل را ذخیره کنم", "unable to rename": "متأسفم ، نمیتوانم نام فایل را تغیبر بدم", "unsaved file": "این فایل ذخیره نشده ، خروج حتمی؟", "warning": "هشدار", "use emmet": "استفاده از emmet", "use quick tools": "استفاده از ابزار سریع", "yes": "بله", "encoding": "رمزگذاری متن", "syntax highlighting": "برجسته نحو", "read only": "فقط خواندنی", "select all": "انتخاب همه", "select branch": "انتخاب شاخه", "create new branch": "شعبه جدید ایجاد کنید", "use branch": "از شاخه استفاده کنید", "new branch": "شعبه جدید", "branch": "branch", "key bindings": "اتصالات کلیدی", "edit": "ویرایش", "reset": "تنظیم مجدد", "color": "رنگ", "select word": "کلمه را انتخاب کنید", "quick tools": "ابزار سریع", "select": "سره", "editor font": "Editor font", "new project": "پروژه جدید", "format": "قالب", "project name": "نام پروژه", "unsupported device": "دستگاه شما از موضوع پشتیبانی نمی کند.", "vibrate on tap": "Vibrate on tap", "copy command is not supported by ftp.": "Copy command is not supported by FTP.", "support title": "Support Acode", "fullscreen": "fullscreen", "animation": "animation", "backup": "backup", "restore": "restore", "backup successful": "Backup successful", "invalid backup file": "Invalid backup file", "add path": "Add path", "live autocompletion": "Live autocompletion", "file properties": "File properties", "path": "Path", "type": "Type", "word count": "Word count", "line count": "Line count", "last modified": "Last modified", "size": "Size", "share": "Share", "show print margin": "Show print margin", "login": "login", "scrollbar size": "Scrollbar size", "cursor controller size": "Cursor controller size", "none": "none", "small": "small", "large": "large", "floating button": "Floating button", "confirm on exit": "Confirm on exit", "show console": "Show console", "image": "Image", "insert file": "Insert file", "insert color": "Insert color", "powersave mode warning": "Turn off power saving mode to preview in external browser.", "exit": "Exit", "custom": "custom", "reset warning": "Are you sure you want to reset theme?", "theme type": "Theme type", "light": "light", "dark": "dark", "file browser": "File Browser", "operation not permitted": "Operation not permitted", "no such file or directory": "No such file or directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Not a directory", "is a directory": "Is a directory", "invalid argument": "Invalid argument", "too many open files in system": "Too many open files in system", "too many open files": "Too many open files", "text file busy": "Text file busy", "no space left on device": "No space left on device", "read-only file system": "Read-only file system", "file name too long": "File name too long", "too many users": "Too many users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "An error occurred", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "Save file", "save file as": "Save file as", "files": "Files", "help": "Help", "file has been deleted": "{file} has been deleted!", "feature not available": "This feature is only available in paid version of the app.", "deleted file": "Deleted file", "line height": "Line height", "preview info": "If you want run the active file, tap and hold on play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "Close file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Open in browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "Rate Acode", "support": "Support", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme and key bindings. It will not backup your FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "حامی مالی", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/it-it.json ================================================ { "lang": "Italiano", "about": "Chi siamo", "active files": "File aperti", "alert": "Avviso", "app theme": "Tema dell'app", "autocorrect": "Vuoi attivate l'autocorrezione?", "autosave": "Autosalvataggio", "cancel": "indietro", "change language": "Cambia la lingua", "choose color": "Scegli il colore", "clear": "cancella tutto", "close app": "Vuoi chiudere l'applicazione?", "commit message": "Commetti il messaggio", "console": "Console", "conflict error": "Errore di conflitto! Per favore aspetta prima di commettere di nuovo.", "copy": "copia", "create folder error": "Ci spiace, non è stato possibile creare una nuova cartella", "cut": "taglia", "delete": "Elimina", "dependencies": "Dipendenze", "delay": "Tempo in millisecondi", "editor settings": "Impostazioni dell'editor", "editor theme": "Tema dell'editor", "enter file name": "Inserisci il nome del file", "enter folder name": "Inserisci il nome della cartella", "empty folder message": "La cartella è vuota", "enter line number": "Inserisci il numero della linea", "error": "errore", "failed": "fallito", "file already exists": "Il file esiste già", "file already exists force": "Il file esiste già. Vuoi sovrascriverlo?", "file changed": " è stato modificato, vuoi aprirlo?", "file deleted": "file eliminato", "file is not supported": "questo file non è supportato", "file not supported": "Questo tipo di file non è supportato.", "file too large": "Il file è troppo grande per caricarlo. La grandezza massima è {size}", "file renamed": "il file è stato rinominato", "file saved": "il file è stato salvato", "folder added": "la cartella è stata aggiunta", "folder already added": "la cartella è già stata aggiunta", "font size": "Dimensioni dei caratteri", "goto": "Vai alla linea", "icons definition": "Definizione delle icone", "info": "informazioni", "invalid value": "Valore invalido", "language changed": "la lingua è stata cambiata con successo", "linting": "Controlla per degli errore di sintassi", "logout": "Disconnetti", "loading": "Caricamento", "my profile": "Il mio profilp", "new file": "Nuovo file", "new folder": "Nuova cartella", "no": "No", "no editor message": "Apri o crea un nuovo file dal menu", "not set": "Non settato", "unsaved files close app": "Ci sono dei file non salvati. Vuoi chiudere l'applicazione?", "notice": "Notizia", "open file": "Apri file", "open files and folders": "Apri file e cartelle", "open folder": "Apri la cartella", "open recent": "Apri file recenti", "ok": "ok", "overwrite": "sovrascrovi", "paste": "incolla", "preview mode": "modalità anteprima", "read only file": "Impossibile salvare un file solo lettura. Per favore prova salva come", "reload": "ricarica", "rename": "rinomina", "replace": "rimpiazza", "required": "questo campo è richiesto", "run your web app": "Avvia la tua app sul web", "save": "salva", "saving": "salvando", "save as": "Salva come", "save file to run": "Per favore salva questo file prima di eseguirlo sul web", "search": "cerca", "see logs and errors": "Guarda i log e gli errori", "select folder": "Seleziona la cartella", "settings": "impostazioni", "settings saved": "impostazioni salvate", "show line numbers": "Mostra i numeri delle linee", "show hidden files": "Mostra i file nascosti", "show spaces": "Mostra gli spazi", "soft tab": "Linguetta morbida", "sort by name": "Ordina per nome", "success": "successo", "tab size": "Grandezza della linguetta", "text wrap": "A capo automatico", "theme": "tema", "unable to delete file": "non è stato possibile eliminare il file", "unable to open file": "Ci spiace. non è stato possibile aprire il file", "unable to open folder": "Ci spiace. non è stato possibile aprire la cartella", "unable to save file": "Ci spiace. non è stato possibile salvare il file", "unable to rename": "Ci spiace. non è stato possibile rinominare il file", "unsaved file": "Questo file non è salvato. Vuoi chiudere comunque?", "warning": "avviso", "use emmet": "Usa emmet", "use quick tools": "Usa gli attrezzo veloci", "yes": "si", "encoding": "Codifica del testo", "syntax highlighting": "Evidenziazione della sintassi", "read only": "Sola lettura", "select all": "seleziona tutto", "select branch": "Seleziona ramo", "create new branch": "Crea un nuovo ramo", "use branch": "Usa il ramo", "new branch": "Nuovo ramo", "branch": "ramo", "key bindings": "associazione dei tasti", "edit": "modifica", "reset": "resetta", "color": "colore", "select word": "Seleziona parole", "quick tools": "Strumenti veloci", "select": "seleziona", "editor font": "Font dell'editor", "new project": "Nuovo progetto", "format": "formato", "project name": "Nome del progetto", "unsupported device": "Il tuo dispositivo non supporta questo tema.", "vibrate on tap": "Vibra al tocco", "copy command is not supported by ftp.": "Il comando copia non è supportato da FTP.", "support title": "Supporta Acode", "fullscreen": "schermo intero", "animation": "animazioni", "backup": "backup", "restore": "ristabilire", "backup successful": "Backup eseguito con successo", "invalid backup file": "File di backup invalido", "add path": "Aggiungi percorso", "live autocompletion": "Autocompletazione dal vivo", "file properties": "Proprietà del file", "path": "Percorso", "type": "Tipo", "word count": "Numero delle parole", "line count": "Numero dele linee", "last modified": "Modificato l'ultima volta", "size": "Grandezza", "share": "Condividi", "show print margin": "Mostra margini di stampa", "login": "Accedi", "scrollbar size": "Grandezza della barra di scorrimento", "cursor controller size": "Dimensione del controller del cursore", "none": "niente", "small": "piccolo", "large": "grande", "floating button": "Bottone fluttuante", "confirm on exit": "Conferma all'uscita", "show console": "Mostra la console", "image": "Immagine", "insert file": "Inserisci un file", "insert color": "Inserisci un colore", "powersave mode warning": "Disatriva la modalità risparmio energetico per visualizzare l'anteprima in un browser esterno.", "exit": "Esci", "custom": "custom", "reset warning": "Sei sicuro di voler resettare il tema?", "theme type": "Tipo di tema", "light": "chiaro", "dark": "scuro", "file browser": "Browser dei file", "operation not permitted": "Azione nnon consentita", "no such file or directory": "Nessun file o cartella simile trovato", "input/output error": "Errore di input/output", "permission denied": "Permesso negato", "bad address": "Indirizzo invalido", "file exists": "Il file esiste già", "not a directory": "Non è una cartella", "is a directory": "È una cartella", "invalid argument": "Argomento invalido", "too many open files in system": "Troppi file aperti nel sistema", "too many open files": "Troppi file aperti", "text file busy": "File di testo occupato", "no space left on device": "La memoria del dispositivo è piena", "read-only file system": "File di sistema solo lettura", "file name too long": "Il nome del file è troppo lungo", "too many users": "Troppi utenti", "connection timed out": "La connessione è terminata", "connection refused": "Connessione rifiutata", "owner died": "Il proprietario è morto", "an error occurred": "C'è stato un errore", "add ftp": "Aggiungi FTP", "add sftp": "Aggiungi SFTP", "save file": "Salva il file", "save file as": "Salva il file come", "files": "Files", "help": "Aiuto", "file has been deleted": "{file} è stato eliminato!", "feature not available": "Questa funzione è disponibile solo nella versione premium dell'app.", "deleted file": "File eliminato", "line height": "Altezza delle linee", "preview info": "Se vuoi eseguire il file corrente clicca e tieni premuto il tasto play.", "manage all files": "Da il permesso ad Acode di gestire i file nelle impostazioni a Acode editor per modificare facilmente i file sul tuo dispositivo.", "close file": "Close file", "reset connections": "Resetta le connessioni", "check file changes": "Controlla cambiamenti nei file", "open in browser": "Apri nel browser", "desktop mode": "Modalità Desktop", "toggle console": "Apri/chiudi la consolr", "new line mode": "Modalità nuova linea", "add a storage": "Aggiungi una memoria", "rate acode": "Recensisci Acode", "support": "Supporto", "downloading file": "Scaricando {file}", "downloading...": "Scaricando...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme and key bindings. It will not backup your FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Sponsor", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/ja-jp.json ================================================ { "lang": "日本語 (by wappo56 / fj68)", "about": "Acode editor", "active files": "アクティブファイル", "alert": "警告", "app theme": "アプリテーマ", "autocorrect": "オートコレクトを有効にする", "autosave": "オートセーブ", "cancel": "キャンセル", "change language": "言語の変更", "choose color": "色の選択", "clear": "クリア", "close app": "アプリを終了しますか?", "commit message": "コミットメッセージ", "console": "コンソール", "conflict error": "競合しました! 別のコミットまでしばらくお待ちください。", "copy": "コピー", "create folder error": "新規フォルダの作成に失敗しました。", "cut": "切り取り", "delete": "削除", "dependencies": "依存設定", "delay": "ミリ秒単位", "editor settings": "エディタ設定", "editor theme": "エディタテーマ", "enter file name": "ファイル名の入力", "enter folder name": "フォルダ名の入力", "empty folder message": "フォルダ名が空です", "enter line number": "行数の入力", "error": "エラー", "failed": "失敗", "file already exists": "ファイルは既に存在します。", "file already exists force": "ファイルは既に存在します。上書きしますか?", "file changed": " は変更されました。ファイルを再読み込みしますか?", "file deleted": "ファイル削除", "file is not supported": "未サポートファイル", "file not supported": "このファイルタイプはサポートされていません。", "file too large": "ファイルが大き過ぎて処理できません。許可される最大ファイルサイズは {size}", "file renamed": "ファイル名変更", "file saved": "ファイル保存", "folder added": "フォルダ追加", "folder already added": "フォルダ追加済み", "font size": "フォントのサイズ", "goto": "行に移動", "icons definition": "アイコン定義", "info": "情報", "invalid value": "無効な値", "language changed": "言語設定を正常に変更しました", "linting": "構文エラーのチェック", "logout": "ログアウト", "loading": "読み込み中", "my profile": "マイプロフィール", "new file": "新規ファイル", "new folder": "新規フォルダ", "no": "無効", "no editor message": "メニューからファイルとフォルダを開くか新規作成してください", "not set": "設定なし", "unsaved files close app": "未保存のファイルがあります。アプリケーションを閉じますか?", "notice": "お知らせ", "open file": "ファイルを開く", "open files and folders": "ファイルとフォルダを開く", "open folder": "フォルダを開く", "open recent": "最近のファイルを開く", "ok": "OK", "overwrite": "上書き", "paste": "貼り付け", "preview mode": "プレビューモード", "read only file": "読み取り専用ファイルは保存できません。名前を付けて保存してください", "reload": "再読み込み", "rename": "ファイル名の変更", "replace": "置換", "required": "この項目は必須です", "run your web app": "ウェブアプリを実行", "save": "保存", "saving": "保存中", "save as": "名前を付けて保存", "save file to run": "このファイルを保存してブラウザで実行してください", "search": "検索", "see logs and errors": "ログとエラーの確認", "select folder": "フォルダを選択", "settings": "設定", "settings saved": "設定を保存しました", "show line numbers": "行数を表示する", "show hidden files": "隠しファイルを表示する", "show spaces": "スペースを表示する", "soft tab": "ソフトタブ", "sort by name": "名前順", "success": "成功", "tab size": "タブのサイズ", "text wrap": "テキストの折り返し", "theme": "テーマ", "unable to delete file": "ファイルを削除できません", "unable to open file": "ファイルを開けません", "unable to open folder": "フォルダを開けません", "unable to save file": "ファイルを保存できません", "unable to rename": "名前を変更できません", "unsaved file": "保存されていませんが、閉じますか?", "warning": "警告", "use emmet": "Emmet使用", "use quick tools": "クイックツール使用", "yes": "有効", "encoding": "テキストエンコード", "syntax highlighting": "構文ハイライト", "read only": "読み取り専用", "select all": "すべて選択", "select branch": "ブランチの選択", "create new branch": "新規ブランチ作成", "use branch": "ブランチを使用", "new branch": "新規ブランチ", "branch": "ブランチ", "key bindings": "キーバインド", "edit": "編集", "reset": "リセット", "color": "カラー", "select word": "単語選択", "quick tools": "クイックツール", "select": "選択", "editor font": "フォント", "new project": "新規プロジェクト", "format": "整形", "project name": "プロジェクト名", "unsupported device": "お使いのデバイスはテーマをサポートしていません。", "vibrate on tap": "タップ時に振動させる", "copy command is not supported by ftp.": "FTPではCopyコマンドはサポートされていません。", "support title": "Acodeを支援", "fullscreen": "フルスクリーン", "animation": "アニメーション", "backup": "バックアップ", "restore": "復元", "backup successful": "バックアップ成功", "invalid backup file": "バックアップファイルが無効です", "add path": "パスを追加", "live autocompletion": "自動補完を実行", "file properties": "ファイルプロパティ", "path": "パス", "type": "タイプ", "word count": "文字数", "line count": "行数", "last modified": "最終更新", "size": "サイズ", "share": "共有", "show print margin": "印刷時の余白を表示", "login": "ログイン", "scrollbar size": "スクロールバーのサイズ", "cursor controller size": "カーソルコントローラーのサイズ", "none": "なし", "small": "小", "large": "大", "floating button": "フローティングボタン", "confirm on exit": "アプリケーション終了時に確認", "show console": "コンソールを表示", "image": "画像", "insert file": "ファイルを挿入", "insert color": "色を挿入", "powersave mode warning": "外部ブラウザでプレビューするには省電力モードをオフにしてください", "exit": "終了", "custom": "カスタム", "reset warning": "テーマをリセットしてよろしいですか?", "theme type": "テーマのタイプ", "light": "ライト", "dark": "ダーク", "file browser": "ファイルブラウザ", "operation not permitted": "許可されていない操作です", "no such file or directory": "ファイル/ディレクトリが見つかりません", "input/output error": "入出力エラー", "permission denied": "許可がありません", "bad address": "不正なアドレスです", "file exists": "ファイルが既に存在しています", "not a directory": "ディレクトリではありません", "is a directory": "ディレクトリです", "invalid argument": "不正な引数です", "too many open files in system": "システムで開くファイルが多すぎます", "too many open files": "開くファイルが多すぎます", "text file busy": "ファイルがビジー状態です", "no space left on device": "空き容量が不足しています", "read-only file system": "読み取り専用です", "file name too long": "ファイル名が長すぎます", "too many users": "ユーザーが多すぎます", "connection timed out": "接続がタイムアウトしました", "connection refused": "接続が拒否されました", "owner died": "ファイルを所有しているプロセスが死んでいます", "an error occurred": "エラーが発生しました", "add ftp": "FTPを追加", "add sftp": "SFTPを追加", "save file": "保存", "save file as": "名前を付けて保存", "files": "ファイル一覧", "help": "ヘルプ", "file has been deleted": "{file}は既に削除されています", "feature not available": "この機能は有料版でのみ使用できます", "deleted file": "ファイルを削除しました", "line height": "行の高さ", "preview info": "アクティブファイルを実行するには実行アイコンを長押しして下さい", "manage all files": "デバイス内のファイルを簡単に編集できるよう、設定でAcode editorに全てのファイルを管理することを許可してください", "close file": "ファイルを閉じる", "reset connections": "接続をリセット", "check file changes": "変更箇所をチェックする", "open in browser": "ブラウザで開く", "desktop mode": "デスクトップモード", "toggle console": "コンソールを表示/非表示", "new line mode": "改行モード", "add a storage": "ストレージを追加", "rate acode": "Acodeを評価する", "support": "支援する", "downloading file": "{file}をダウンロード中", "downloading...": "ダウンロード中...", "folder name": "フォルダ名", "keyboard mode": "キーボードモード", "normal": "通常", "app settings": "アプリ設定", "disable in-app-browser caching": "アプリ内ブラウザのキャッシュを無効", "copied to clipboard": "クリップボードにコピーしました", "remember opened files": "開いたファイルを記憶する", "remember opened folders": "開いたフォルダを記憶する", "no suggestions": "候補を表示しない", "no suggestions aggressive": "候補を積極的に提案しない", "install": "インストール", "installing": "インストール中...", "plugins": "プラグイン", "recently used": "最近使用", "update": "更新", "uninstall": "アンインストール", "download acode pro": "Acode Proをダウンロード", "loading plugins": "プラグインを読み込み中", "faqs": "よくある質問", "feedback": "フィードバック", "header": "ヘッダー", "sidebar": "スライドバー", "inapp": "アプリ内", "browser": "ブラウザ", "diagonal scrolling": "斜めスクロール", "reverse scrolling": "逆スクロール", "formatter": "整形", "format on save": "保存時に整形", "remove ads": "広告を除去", "fast": "高速", "slow": "低速", "scroll settings": "スクロール設定", "scroll speed": "スクロール速度", "loading...": "読み込み中...", "no plugins found": "プラグインが見つかりません", "name": "名前", "username": "ユーザー名", "optional": "オプション", "hostname": "ホスト名", "password": "パスワード", "security type": "セキュリティタイプ", "connection mode": "接続モード", "port": "ポート", "key file": "キーファイル", "select key file": "キーファイルの選択", "passphrase": "パスフレーズ", "connecting...": "接続中...", "type filename": "ファイル名を入力", "unable to load files": "ファイルを読み込めません", "preview port": "プレビューポート", "find file": "ファイルを検索", "system": "システム", "please select a formatter": "整形方法を選択してください", "case sensitive": "大文字と小文字を区別", "regular expression": "正規表現", "whole word": "単語単位", "edit with": "...で編集", "open with": "...で開く", "no app found to handle this file": "このファイルを扱えるアプリがありません", "restore default settings": "デフォルト設定の復元", "server port": "サーバーポート", "preview settings": "プレビュー設定", "preview settings note": "プレビューポートとサーバーポートが異なる場合、アプリはサーバーを起動せず、代わりにブラウザやアプリ内ブラウザで https://: を開きます。これは別の場所でサーバーを動かしている場合に役立ちます。", "backup/restore note": "設定、カスタムテーマ、キーバインドのみバックアップされます。FTP/SFTPやGitHubプロファイルはバックアップされません。", "host": "ホスト", "retry ftp/sftp when fail": "FTP/SFTP接続失敗時に再試行", "more": "さらに表示", "thank you :)": "ありがとうございます :)", "purchase pending": "購入保留中", "cancelled": "キャンセルしました", "local": "ローカル", "remote": "リモート", "show console toggler": "コンソール切り替えボタンを表示", "binary file": "このファイルにはバイナリデータが含まれていますが、開きますか?", "relative line numbers": "相対行番号", "elastic tabstops": "タブ位置調整 (Elastic tabstops)", "line based rtl switching": "行ベースのRTL切り替え", "hard wrap": "ハードラップ", "spellcheck": "スペルチェック", "wrap method": "折り返しの方法", "use textarea for ime": "IME用のテキストエリアを使用", "invalid plugin": "無効なプラグイン", "type command": "コマンドを入力", "plugin": "プラグイン", "quicktools trigger mode": "クイックツールのトリガーモード", "print margin": "印刷の余白", "touch move threshold": "タッチ移動のしきい値", "info-retryremotefsafterfail": "FTP/SFTP接続失敗時に再試行を行います。", "info-fullscreen": "ホーム画面のタイトルバーを非表示にします。", "info-checkfiles": "アプリがバックグラウンドのときにファイルの変更をチェックします。", "info-console": "JavaScriptのコンソールを選択します。 [LEGACY] はデフォルトのコンソール、 [ERUDA] はサードパーティのコンソールです。", "info-keyboardmode": "テキスト入力用のキーボードモードです。 [候補を表示しない] はサジェストとオートコレクトを非表示にします。 [候補を表示しない] が機能しない場合、[候補を積極的に提案しない] に変更してみてください。", "info-rememberfiles": "アプリを閉じるときに開いているファイルを記憶します。", "info-rememberfolders": "アプリを閉じるときに開いているフォルダを記憶します。", "info-floatingbutton": "クイックツールのフローティングボタンの表示/非表示を切り替えます。", "info-openfilelistpos": "アクティブなファイルのリストを表示する位置です。", "info-touchmovethreshold": "デバイスのタッチ感度が高すぎる場合、この値を増やすと、意図しないタッチ移動を防ぐことができます。", "info-scroll-settings": "この設定では、テキストの折り返しを含むスクロールの設定ができます。", "info-animation": "アプリの動作が遅いと感じる場合、アニメーションを無効にしてください。", "info-quicktoolstriggermode": "クイックツールのボタンが機能しない場合、この値を変更してみてください。", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "所有済み", "api_error": "APIサーバーDown。しばらくしてから実行してください。", "installed": "インストール済み", "all": "すべて", "medium": "中", "refund": "払い戻し", "product not available": "利用不可の製品", "no-product-info": "現在、この製品はお住まいの国でご利用できません。後でもう一度お試しください。", "close": "閉じる", "explore": "エクスプローラー", "key bindings updated": "キーバインディングが更新されました", "search in files": "ファイル内を検索", "exclude files": "ファイルを除外", "include files": "ファイルを含める", "search result": "{matches} 個の結果が {files} 個のファイルでみつかりました。", "invalid regex": "正規表現が無効です: {message}", "bottom": "下", "save all": "すべて保存", "close all": "すべて閉じる", "unsaved files warning": "保存されていないファイルがあります。「OK」をクリックして処理を選択するか「キャンセル」をクリックして戻ります。", "save all warning": "すべてのファイルを保存して閉じてもよろしいですか?この操作は取り消せません。", "save all changes warning": "すべてのファイルを保存してもよろしいですか?", "close all warning": "すべてのファイルを閉じてもよろしいですか? 保存されていない変更は失われ、この操作は取り消せません。", "refresh": "更新", "shortcut buttons": "ショートカットボタン", "no result": "結果なし", "searching...": "検索中...", "quicktools:ctrl-key": "Ctrl/Command キー", "quicktools:tab-key": "Tab キー", "quicktools:shift-key": "Shift キー", "quicktools:undo": "元に戻す", "quicktools:redo": "やり直す", "quicktools:search": "ファイル内を検索", "quicktools:save": "ファイルを保存", "quicktools:esc-key": "Esc キー", "quicktools:curlybracket": "{ } を挿入", "quicktools:squarebracket": "[ ] を挿入", "quicktools:parentheses": "( ) を挿入", "quicktools:anglebracket": "< > を挿入", "quicktools:left-arrow-key": "左矢印キー", "quicktools:right-arrow-key": "右矢印キー", "quicktools:up-arrow-key": "上矢印キー", "quicktools:down-arrow-key": "下矢印キー", "quicktools:moveline-up": "行を上に移動", "quicktools:moveline-down": "行を下に移動", "quicktools:copyline-up": "行を上にコピー", "quicktools:copyline-down": "行を下にコピー", "quicktools:semicolon": "セミコロンを挿入", "quicktools:quotation": "クオーテーションを挿入", "quicktools:and": "アンドを挿入", "quicktools:bar": "バーを挿入", "quicktools:equal": "イコールを挿入", "quicktools:slash": "スラッシュを挿入", "quicktools:exclamation": "エクスクラメーションを挿入", "quicktools:alt-key": "Alt キー", "quicktools:meta-key": "Windows/Meta キー", "info-quicktoolssettings": "エディターの下にあるクイックツールコンテナ内のショートカットボタンとキーボードキーをカスタマイズして、コーディングエクスペリエンスを向上させましょう。", "info-excludefolders": "パターン /node_modules/ を使用して、node_modules フォルダ内のすべてのファイルを無視します。 これによりファイルがリストされるのを防ぎファイル検索に含まれなくなります。", "missed files": "検索開始後に {count} 個のファイルをスキャンしましたが検索には含まれません。", "remove": "削除", "quicktools:command-palette": "コマンドパレット", "default file encoding": "デフォルトのファイルエンコーディング", "remove entry": "'{name}' を保存されたパスから削除してもよろしいですか? 削除してもパス自体は削除されないことに注意してください。", "delete entry": "'{name}' を削除しますか? この操作は取り消せません。続行しますか?", "change encoding": "'{file}' を '{encoding}' エンコーディングで再度開きますか? この操作を実行すると、ファイルに対して行われた保存されていない変更がすべて失われます。続行して再度開きますか?", "reopen file": "'{file}' を再度開いてよろしいですか? 保存されていない変更はすべて失われます。", "plugin min version": "'{name}' は Acode - {v-code} 以降でのみ使用できます。こちらをクリックして更新してください。", "color preview": "カラープレビュー", "confirm": "確認", "list files": "{name} 内のすべてのファイルを一覧表示しますか?ファイル数が多すぎるとアプリがクラッシュする可能性があります。", "problems": "問題", "show side buttons": "サイドボタンを表示", "bug_report": "Submit a Bug Report", "verified publisher": "検証済み発行者", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "スポンサー", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/ko-kr.json ================================================ { "lang": "한국어", "about": "정보", "active files": "활성파일", "alert": "경고", "app theme": "앱 테마", "autocorrect": "자동 완성 활성화", "autosave": "자동 저장", "cancel": "취소", "change language": "언어 변경", "choose color": "생상 선택", "clear": "지우기", "close app": "앱을 종료 하시겠습니까?", "commit message": "커밋 메세지", "console": "콘솔", "conflict error": "충돌! 다른 커밋을 기다리세요", "copy": "복사", "create folder error": "새 폴더를 생성 할 수 없습니다.", "cut": "잘라내기", "delete": "삭제", "dependencies": "Dependencies", "delay": "밀리 초 시간 단위", "editor settings": "편집기 설정", "editor theme": "편집기 테마", "enter file name": "파일 이름 입력", "enter folder name": "폴더 이름 입력", "empty folder message": "빈 폴더", "enter line number": "라인 번호", "error": "오류", "failed": "실패", "file already exists": "파일이 이미 존재 합니다.", "file already exists force": "파일이 이미 존재 합니다. 덮어쓰시겠습니까?", "file changed": "파일이 변경되었습니다. 다시 로드하시겠습니까?", "file deleted": "파일 삭제", "file is not supported": "지원하지 않는 파일", "file not supported": "이 파일 유형은 지원하지 않습니다.", "file too large": "파일이 너무 크고 처리할 수 없습니다. 최대 파일 크기:{size}", "file renamed": "파일 이름 변경", "file saved": "파일 저장 완료", "folder added": "폴더 추가", "folder already added": "이미 추가된 폴더입니다.", "font size": "폰트 크기", "goto": "행 이동", "icons definition": "아이콘 정의", "info": "정보", "invalid value": "Invalid value", "language changed": "성공적으로 언어가 변경되었습니다.", "linting": "Check syntax error", "logout": "로그아웃", "loading": "Loading", "my profile": "내 프로필", "new file": "새로운 파일", "new folder": "새로운 폴더", "no": "아니오", "no editor message": "메뉴에서 파일또는 폴더를 열거나 새로 작성", "not set": "설정 안함", "unsaved files close app": "저장안한 파일이 있습니다. 정말로 앱을 종료 하시겠습니까?", "notice": "공지", "open file": "파일 욜기", "open files and folders": "폴더 또는 파일을 선택해 주세요", "open folder": "폴더를 선택해 주세요", "open recent": "최근 파일", "ok": "확인", "overwrite": "덮어쓰기", "paste": "붙혀넣기", "preview mode": "미리보기", "read only file": "읽기 전용 파일은 보존되지 않습니다. 저장하세요", "reload": "다시 불러오기", "rename": "이름 변경", "replace": "변경", "required": "필수 항목입니다", "run your web app": "웹 앱 실행", "save": "저장", "saving": "저장중", "save as": "저장", "save file to run": "파일을 저장하고 브라우저를 실행하세요", "search": "검색", "see logs and errors": "로그와 오류 확인", "select folder": "폴더 선택", "settings": "설정", "settings saved": "설정이 저장되었습니다", "show line numbers": "라인 번호 ㅂ기", "show hidden files": "숨긴 파일 보기", "show spaces": "띄어쓰기 공간 보기", "soft tab": "Soft tab", "sort by name": "이름 정령", "success": "완료", "tab size": "Tab 크기", "text wrap": "텍스트 반환", "theme": "배경", "unable to delete file": "파일을 삭제할 수 없습니다", "unable to open file": "파일을 열수가 없습니다", "unable to open folder": "폴더를 열수가 없습니다", "unable to save file": "파일을 저장할 수 없습니다.", "unable to rename": "파일 이름변경을 할 수 없습니다", "unsaved file": "이 파일은 저장되있지 않습니다 닫으시겠습니까?", "warning": "위험", "use emmet": "Use emmet", "use quick tools": "퀵 도구툴 사용", "yes": "예", "encoding": "인코딩", "syntax highlighting": "구문 강조 표시", "read only": "읽기 전용", "select all": "모두 선택", "select branch": "branch 선택", "create new branch": "branch생성", "use branch": "branch 사용", "new branch": "새로운 branch", "branch": "branch", "key bindings": "단축키", "edit": "수정", "reset": "초기화", "color": "색", "select word": "단어 선택", "quick tools": "퀵툴", "select": "선택", "editor font": "폰트", "new project": "신규 프로젝트", "format": "포맷", "project name": "프로젝트 이름", "unsupported device": "사용자의 디바이스는 지원하지 않습니다", "vibrate on tap": "tap 할 때 진동", "copy command is not supported by ftp.": "FTP에서 Copy명령은 지원되지 않습니다", "support title": "Acode 후원", "fullscreen": "풀 스크린", "animation": "애니메이션", "backup": "백업", "restore": "복원", "backup successful": "백업 완료", "invalid backup file": "백업파일이 없습니다", "add path": "경로추가", "live autocompletion": "자동 완성실행", "file properties": "파일 속성", "path": "경로", "type": "타입", "word count": "문자수", "line count": "행 수", "last modified": "최종 갱신", "size": "크기", "share": "공유", "show print margin": "여백표시", "login": "로그인", "scrollbar size": "스크롤바 크기", "cursor controller size": "커서 컨트로러 크기", "none": "없음", "small": "작은", "large": "큰", "floating button": "유동적인 버튼", "confirm on exit": "엡 종료시 확인", "show console": "콘솔 보기", "image": "사진", "insert file": "파일 삽입", "insert color": "색 삽입", "powersave mode warning": "외부 저장소에서 미리 확인하려면 절전모드를 종료해 주십쇼", "custom": "custom", "reset warning": "Are you sure you want to reset theme?", "theme type": "Theme type", "light": "light", "dark": "dark", "file browser": "File Browser", "exit": "Exit", "operation not permitted": "Operation not permitted", "no such file or directory": "No such file or directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Not a directory", "is a directory": "Is a directory", "invalid argument": "Invalid argument", "too many open files in system": "Too many open files in system", "too many open files": "Too many open files", "text file busy": "Text file busy", "no space left on device": "No space left on device", "read-only file system": "Read-only file system", "file name too long": "File name too long", "too many users": "Too many users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "An error occurred", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "Save file", "save file as": "Save file as", "files": "Files", "help": "Help", "file has been deleted": "{file} has been deleted!", "feature not available": "This feature is only available in paid version of the app.", "deleted file": "Deleted file", "line height": "Line height", "preview info": "If you want run the active file, tap and hold on play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "Close file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Open in browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "Rate Acode", "support": "Support", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme and key bindings. It will not backup your FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "대부분의 다운로드", "newly_added": "새로 추가되었습니다", "top_rated": "최고 평점", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "스폰서", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/ml-in.json ================================================ { "lang": "മലയാളം", "about": "കുറിച്ച്‌", "active files": "സജീവ ഫയലുകൾ", "alert": "ജാഗത", "app theme": "ആപ്പ് പതിപാദം", "autocorrect": "ഓട്ടോമാറ്റിക് തിരുത്തൽ പ്രവർത്തനക്ഷമമാക്കുക?", "autosave": "ഓട്ടോമാറ്റിക് സൂക്ഷിക്കൽ", "cancel": "റദ്ദാക്കുക", "change language": "ഭാഷ മാറ്റുക", "choose color": "നിറം തിരഞ്ഞെടുക്കുക", "clear": "വൃത്തിയാക്കുക", "close app": "ആപ്പിൽ നിന്ന് പുറത്ത് കടക്കണോ?", "commit message": "സ്ഥിരീകരണ സന്ദേശം", "console": "കൺസോൾ", "conflict error": "സംഘർഷം! മറ്റൊരു കമ്മിറ്റിന് മുമ്പായി കാത്തിരിക്കുക.", "copy": "പകർത്തുക", "create folder error": "ക്ഷമിക്കണം, പുതിയ ഫോൾഡർ സൃഷ്ടിക്കാൻ കഴിയില്ല", "cut": "കട്ട്", "delete": "ഇല്ലാതാക്കുക", "dependencies": "ആശ്രിതത്വം", "delay": "സമയം മില്ലിസെക്കൻഡിൽ", "editor settings": "എഡിറ്റർ ക്രമീകരണങ്ങൾ", "editor theme": "എഡിറ്റർ തീം", "enter file name": "ഫയലിന്റെ പേര് നൽകുക", "enter folder name": "ഫോൾഡറിന്റെ പേര് നൽകുക", "empty folder message": "ശൂന്യമായ ഫോൾഡർ", "enter line number": "ലൈൻ നമ്പർ നൽകുക", "error": "പിശക്", "failed": "പരാജയപ്പെട്ടു", "file already exists": "ഫയൽ ഇതിനകം നിലവിലുണ്ട്", "file already exists force": "ഫയൽ ഇതിനകം നിലവിലുണ്ട്. തിരുത്തിയെഴുതണോ?", "file changed": " മാറ്റി, ഫയൽ വീണ്ടും ലോഡുചെയ്യണോ?", "file deleted": "ഫയൽ ഇല്ലാതാക്കി", "file is not supported": "ഫയൽ പിന്തുണയ്‌ക്കുന്നില്ല", "file not supported": "ഈ ഫയൽ തരം പിന്തുണയ്‌ക്കുന്നില്ല.", "file too large": "ഫയൽ കൈകാര്യം ചെയ്യാൻ കഴിയാത്തത്ര വലുതാണ്.{size} ആണ് പരമാവധി ഫയൽ വലുപ്പം.", "file renamed": "ഫയലിന്റെ പേരുമാറ്റി", "file saved": "ഫയൽ സംരക്ഷിച്ചു", "folder added": "ഫോൾഡർ ചേർത്തു", "folder already added": "ഫോൾഡർ മുന്വേതന്നെ ചേർത്തിരുന്നു", "font size": "അക്ഷര വലിപ്പം", "goto": "വരിയിലേക്ക് പോകുക", "icons definition": "ഐക്കണുകളുടെ നിർവചനം", "info": "വിവരം", "invalid value": "അസാധുവായ മൂല്യം", "language changed": "ഭാഷ വിജയകരമായി മാറ്റി", "linting": "വാക്യഘടന പിശക് പരിശോധിക്കുക", "logout": "ലോഗൗട്ട്", "loading": "ലോഡിംഗ്", "my profile": "എന്റെ പ്രൊഫൈൽ", "new file": "പുതിയ ഫയൽ", "new folder": "പുതിയ ഫോൾഡർ", "no": "ഇല്ല", "no editor message": "മെനുവിൽ നിന്ന് പുതിയ ഫയലും ഫോൾഡറും തുറക്കുക അല്ലെങ്കിൽ സൃഷ്ടിക്കുക", "not set": "സജ്ജമാക്കിയിട്ടില്ല", "unsaved files close app": "സംരക്ഷിക്കാത്ത ഫയലുകളുണ്ട്. അപ്ലിക്കേഷൻ അടയ്‌ക്കണോ?", "notice": "ശ്രദ്ധിക്കുക", "open file": "ഫയൽ തുറക്കുക", "open files and folders": "ഫയലുകളും ഫോൾഡറുകളും തുറക്കുക", "open folder": "ഫോൾഡർ തുറക്കുക", "open recent": "അടുത്തിടെ തുറന്ന ഫയൽ തുറക്കുക", "ok": "ശരി", "overwrite": "പുനരാലേഖനം ചെയ്യുക", "paste": "പേസ്റ്റ്", "preview mode": "പ്രിവ്യൂ മോഡ്", "read only file": "വായന മാത്രം ഫയൽ സംരക്ഷിക്കാൻ കഴിയില്ല!. ഇതായി സംരക്ഷിക്കുക ഉപയോഗിക്കുക", "reload": "വീണ്ടും ലോഡുചെയ്യുക", "rename": "പേരുമാറ്റുക", "replace": "മാറ്റിസ്ഥാപിക്കുക", "required": "ഈ ഫീൽഡ് പൂരിപ്പിക്കേണ്ടതുണ്ട്", "run your web app": "നിങ്ങളുടെ വെബ് അപ്ലിക്കേഷൻ പ്രവർത്തിപ്പിക്കുക", "save": "സംരക്ഷിക്കുക", "saving": "സംരക്ഷിക്കുകയാണ്", "save as": "ഇതായി സംരക്ഷിക്കുക", "save file to run": "ബ്രൗസറിൽ പ്രവർത്തിക്കാൻ ഈ ഫയൽ സംരക്ഷിക്കുക", "search": "തിരയൽ", "see logs and errors": "വിവരണപതികയും പിശകുകളും കാണുക", "select folder": "ഫോൾഡർ തിരഞ്ഞെടുക്കുക", "settings": "ക്രമീകരണങ്ങൾ", "settings saved": "ക്രമീകരണങ്ങൾ സംരക്ഷിച്ചു", "show line numbers": "ലൈൻ നമ്പറുകൾ കാണിക്കുക", "show hidden files": "മറഞ്ഞിരിക്കുന്ന ഫയലുകൾ കാണിക്കുക", "show spaces": "ഇടങ്ങൾ കാണിക്കുക", "soft tab": "സോഫ്റ്റ് ടാബ്", "sort by name": "പേര് പ്രകാരം ഇനം തിരിക്കുക", "success": "വിജയം", "tab size": "ടാബ് വലുപ്പം", "text wrap": "ടെക്സ്റ്റ് റാപ്", "theme": "പതിപാദം", "unable to delete file": "ഫയൽ ഇല്ലാതാക്കാൻ കഴിയില്ല", "unable to open file": "ക്ഷമിക്കണം, ഫയൽ തുറക്കാൻ കഴിഞ്ഞില്ല", "unable to open folder": "ക്ഷമിക്കണം, ഫോൾഡർ തുറക്കാൻ കഴിഞ്ഞില്ല", "unable to save file": "ക്ഷമിക്കണം, ഫയൽ സംരക്ഷിക്കാൻ കഴിഞ്ഞില്ല", "unable to rename": "ക്ഷമിക്കണം, പേരുമാറ്റാൻ കഴിഞ്ഞില്ല", "unsaved file": "ഈ ഫയൽ സംരക്ഷിച്ചിട്ടില്ല, എന്തായാലും അടയ്‌ക്കണോ?", "warning": "മുന്നറിയിപ്പ്", "use emmet": "എമ്മറ്റ് ഉപയോഗിക്കുക", "use quick tools": "ദ്രുത ഉപകരണങ്ങൾ ഉപയോഗിക്കുക", "yes": "അതെ", "encoding": "ടെക്സ്റ്റ് എൻ‌കോഡിംഗ്", "syntax highlighting": "സിന്റാക്സ് ഹൈലൈറ്റിംഗ്", "read only": "വായിക്കാൻ മാത്രം", "select all": "എല്ലാം തിരഞ്ഞെടുക്കുക", "select branch": "ശാഖ തിരഞ്ഞെടുക്കുക", "create new branch": "പുതിയ ശാഖ സൃഷ്ടിക്കുക", "use branch": "ശാഖ ഉപയോഗിക്കുക", "new branch": "പുതിയ ശാഖ", "branch": "ശാഖ", "key bindings": "കീ ബൈൻഡിംഗുകൾ", "edit": "തിരുത്തുക", "reset": "പുന: സജ്ജമാക്കുക", "color": "നിറം", "select word": "പദം തിരഞ്ഞെടുക്കുക", "quick tools": "ദ്രുത ഉപകരണങ്ങൾ", "select": "തിരഞ്ഞെടുക്കുക", "editor font": "എഡിറ്റർ ഫോണ്ട്", "new project": "പുതിയ പദ്ധതി", "format": "രൂപകല്പന", "project name": "പദ്ധതിയുടെ പേര്", "unsupported device": "നിങ്ങളുടെ ഉപകരണം തീമിനെ പിന്തുണയ്‌ക്കുന്നില്ല.", "vibrate on tap": "ടാപ്പിൽ വൈബ്രേറ്റ് ചെയ്യുക", "copy command is not supported by ftp.": "കോപ്പി കമാൻഡ് FTP പിന്തുണയ്ക്കുന്നില്ല.", "support title": "Support Acode", "fullscreen": "പൂർണ്ണ സ്ക്രീൻ", "animation": "പൂർണ്ണ സ്ക്രീൻ", "backup": "ബാക്കപ്പ്", "restore": "പുനഃസ്ഥാപിക്കുക", "backup successful": "ബാക്കപ്പ് വിജയിച്ചു", "invalid backup file": "അസാധുവായ ബാക്കപ്പ് ഫയൽ", "add path": "പാത ചേർക്കുക", "live autocompletion": "തത്സമയ യാന്ത്രിക പൂർത്തീകരണം", "file properties": "ഫയൽ പ്രോപ്പർട്ടികൾ", "path": "പാത", "type": "ടൈപ്പ്", "word count": "", "line count": "വാക്കുകളുടെ എണ്ണം", "last modified": "അവസാനം പരിഷ്കരിച്ചത്", "size": "വലിപ്പം", "share": "ഷെയർ", "show print margin": "പ്രിന്റ് മാർജിൻ ഷോ ചെയുക", "login": "ലോഗിൻ", "scrollbar size": "സ്‌ക്രോൾബാർ സൈസ് ", "cursor controller size": "കഴ്സർ കൺട്രോളർ സൈസ് ", "none": "ഒന്നുമില്ല", "small": "ചെറുത്", "large": "വലുത്", "floating button": "ഫ്ലോട്ടിങ് ബട്ടൺ", "confirm on exit": "കൺഫേം ഓൺ എക്സിറ് ", "show console": "കൺസോൾ കാണിക്കുക", "image": "ചിത്രം", "insert file": "ഫയൽ ഇന്സേര്ട് ചെയുക", "insert color": "കളർ ഇന്സേര്ട് ചെയ്ക", "powersave mode warning": "external ബ്രൗസറിൽ പ്രിവ്യു ചെയ്യാൻ പവർ സേവിങ് മോഡ് ഓഫ് ആകേണ്ടതാണ് ", "exit": "പുറത്തേക് പോകുക", "custom": "കസ്റ്റമ് ", "reset warning": "തീം പുനഃസജ്ജമാക്കണമെന്ന് തീർച്ചയാണോ?", "theme type": "തീം തരം", "light": "ലൈറ്റ്", "dark": "ഡാർക്ക്", "file browser": "ഫയൽ ബ്രൌസർ", "operation not permitted": "ഓപ്പറേഷൻ പെർമിറ് ആയിട്ടില്ല", "no such file or directory": "അങ്ങനെ ഒരു ഫയൽ ഓ ഫോൾഡറോ ഇല്ല", "input/output error": "ഇന്പുട് ഔട്ട്പുട്ട് എറർ", "permission denied": "പെര്മിസ്സഷൻ ഡിനൈദ്‌", "bad address": "ബാഡ് അഡ്രസ് ", "file exists": "ഫയൽ ഉണ്ട്", "not a directory": "ഇത് ഒരു ഡയറക്ടറി അല്ല", "is a directory": "ഡിറ്റക്ടറി ആണ്", "invalid argument": "ഇൻവാലിദ് അർജുമെൻറ്", "too many open files in system": "സിസ്റ്റമിൽ നിറയെ ഓപ്പൺഡ് ഫയൽ", "too many open files": "നിറയെ ഓപ്പൺഡ് ഫയൽ", "text file busy": "ടെക്സ്റ്റ് ഫയൽ ബിസി", "no space left on device": "സിസ്റ്റമിൽ സ്പേസ് ഇല്ല", "read-only file system": "റീഡ് ഒൺലി ഫയൽ സിസ്റ്റം", "file name too long": "ഫയൽ നെയിം വലുതാണ് ", "too many users": "നിറയെ അധികം യൂസേഴ്സ്", "connection timed out": "കണക്ഷൻ കട്ട് ആയി", "connection refused": "കണക്ഷൻ പോയി ", "owner died": "ഓണർ പോയി ", "an error occurred": "ഒരു എറർ വന്നു ", "add ftp": "ആഡ് FTP ", "add sftp": "ആഡ് SFTP ", "save file": "ഫയൽ സേവ് ചെയുക ", "save file as": "ഫയൽ സേവ് ആസ് ", "files": "ഫയൽസ് ", "help": "ഹെല്പ് ", "file has been deleted": "{file} ഡിലീറ്റ് ആയി !", "feature not available": "ഈ ഫീചർ പൈഡ് ആപ്പിൽ മാത്രമേ ലഫ്യമാകു .", "deleted file": "ഡെലീറ്റഡ് ഫയൽ ", "line height": "ലൈൻ ഹൈറ് ", "preview info": "ആക്റ്റീവ് ഫയൽ റൺ ചെയ്യാൻ റൺ ബട്ടനിൽ ടാപ് ആൻഡ് ഹോൾഡ് ചെയുക .", "manage all files": "നിങ്ങളുടെ ഉപകരണത്തിലെ ഫയലുകൾ എളുപ്പത്തിൽ എഡിറ്റുചെയ്യുന്നതിന് ക്രമീകരണങ്ങളിലെ എല്ലാ ഫയലുകളും നിയന്ത്രിക്കാൻ Acode എഡിറ്ററെ അനുവദിക്കുക.", "close file": "ക്ലോസ് ഫയൽ ", "reset connections": "കണക്ഷൻ റിസറ്റ് ചെയുക ", "check file changes": "ചെക്ക് ഫയൽ ചേഞ്ച്‌ ", "open in browser": "ബ്രോസ്വേറിൽ ഓപ്പൺ ചെയുക ", "desktop mode": "ഡെസ്ക്‌റ്റോപ് മോഡ് ", "toggle console": "ടോകൾ console", "new line mode": "ന്യൂ ലൈൻ മോഡ് ", "add a storage": "സ്റ്റോറേജ് ആഡ് ചെയുക ", "rate acode": "റേറ്റ് ആക്കോട് ", "support": "സപ്പോർട്ട് ", "downloading file": "ഡൗൺലോഡിങ് {file}", "downloading...": "ഡൗൺലോഡിങ്...", "folder name": "ഫോൾഡർ നെയിം", "keyboard mode": "കീബോർഡ് മോഡ്", "normal": "നോർമൽ", "app settings": "ആപ്പ് സെറ്റിംഗ്സ്", "disable in-app-browser caching": "ഇൻ-ആപ്പ്-ബ്രൗസർ കാഷിംഗ് പ്രവർത്തനരഹിതമാക്കുക", "copied to clipboard": "ക്ലിപ്പ് ബോർഡിൽ കോപ്പി ചെയ്തു ", "remember opened files": "ഓപ്പൺഡ് ഫയൽസ് റിമെംബേർ ചെയുക", "remember opened folders": "ഓപ്പൺഡ് ഫോൾഡർ റിമെംബേർ ചെയുക", "no suggestions": "നോ സഗ്ഗെസ്ഷൻ ", "no suggestions aggressive": "നോ സഗ്ഗെസ്ഷൻ ആഗ്ഗ്രെസ്സീവ്", "install": "ഇൻസ്റ്റാൾ ", "installing": "ഇൻസ്റ്റലിങ്...", "plugins": "പ്ലഗിൻസ്", "recently used": "റീസെന്റലി യൂസ്ഡ്", "update": "അപ്ഡേറ്റ് ", "uninstall": "യൂണിൻസ്റ്റാൾ", "download acode pro": "ആക്കോട് പ്രൊ ഡൌൺലോഡ് ", "loading plugins": "പ്ലജിനുകൾ ലോഡ് ആകുന്നു ", "faqs": "FAQs", "feedback": "ഫീഡ്ബാക്ക്സ്", "header": "ഹെയ്ഡർ ", "sidebar": "സൈഡിബർ ", "inapp": "ആപ്പിന്റെ അകത്തു", "browser": "ബ്രൗസേറിൽ", "diagonal scrolling": "ദിയഗ്ണൽ സ്ക്രോളിങ് ", "reverse scrolling": "റിവേഴ്‌സ് സഫ്രിലിങ് ", "formatter": "ഫോർമാറ്റർ", "format on save": "ഫോർമാറ്റ്‌ ഓൺ സേവ് ", "remove ads": "പരസ്യം റിമോവ് ചെയുക", "fast": "പെട്ടന്ന്", "slow": "പതുക്കെ", "scroll settings": "സ്ക്രോൽ സെറ്റിംഗ്സ്", "scroll speed": "സ്ക്രോൽ സ്പീഡ്", "loading...": "ലോഡിങ്...", "no plugins found": "ഒരു പ്ലഗിംനും ഇല", "name": "പേര്", "username": "യൂസർ നെയിം", "optional": "ഓപ്ഷണൽ", "hostname": "ഹോസ്റ്റ് നെയിം", "password": "പാസ്സ്‌വേർഡ്‌", "security type": "സെക്യൂരിറ്റി ടൈപ്പ്", "connection mode": "കണക്ഷൻ മോഡ്", "port": "പോർട്ട്", "key file": "കീ ഫയൽ", "select key file": "കീ ഫയൽ സെലക്റ്റ് ചെയുക", "passphrase": "പാസ്സ്‌ഫെരസ്", "connecting...": "കണക്ടിങ്...", "type filename": "ഫയൽ നെയിം ടൈപ്പ് ചെയ്ക", "unable to load files": "ഫയൽസ് ലോഡ് ചെയ്യാൻ പറ്റുന്നില്ല", "preview port": "പോർട്ട് പ്രേവ്യൂ", "find file": "ഫയൽ കണ്ടതുക", "system": "സിസ്റ്റം", "please select a formatter": "ഒരു ഫോർമാറ്റർ സെലക്ട് ചെയുക", "case sensitive": "കേസ് സെൻസിറ്റീവ്", "regular expression": "റെഗുലർ എക്സ്പ്രഷൻ", "whole word": "മുഴുവൻ വേർഡ്", "edit with": "എഡിറ്റ്‌ വിത്ത്", "open with": "ഓപ്പൺ വിത്ത്", "no app found to handle this file": "ഈ ഫയൽ handle ചെയ്യാൻ പറ്റിയ ആപ്പ് ഒന്നും കണ്ടില്ല", "restore default settings": "പഴയ സെറ്റിംഗ്സ് റെസ്റ്റോർ ചെയുക", "server port": "സെർവർ പോർട്ട്", "preview settings": "പ്രേവ്യൂ സെറ്റിംഗ്സ്", "preview settings note": "പ്രിവ്യൂ പോർട്ടും സെർവർ പോർട്ടും വ്യത്യസ്തമാണെങ്കിൽ, ആപ്പ് സെർവർ ആരംഭിക്കില്ല, പകരം അത് ബ്രൗസറിലോ ആപ്പ് ബ്രൗസറിലോ https://: തുറക്കും. നിങ്ങൾ മറ്റെവിടെയെങ്കിലും ഒരു സെർവർ പ്രവർത്തിപ്പിക്കുമ്പോൾ ഇത് ഉപയോഗപ്രദമാണ്.", "backup/restore note": "ഇത് നിങ്ങളുടെ ക്രമീകരണങ്ങൾ, ഇഷ്‌ടാനുസൃത തീം, കീ ബൈൻഡിംഗുകൾ എന്നിവ മാത്രമേ ബാക്കപ്പ് ചെയ്യുകയുള്ളൂ. ഇത് നിങ്ങളുടെ FPT/SFTP ബാക്കപ്പ് ചെയ്യില്ല.", "host": "ഹോസ്റ്റ്", "retry ftp/sftp when fail": "പരാജയപ്പെടുമ്പോൾ ftp/sftp വീണ്ടും ശ്രമിക്കുക", "more": "മോർ", "thank you :)": "നന്ദി :)", "purchase pending": "Purchase പെന്റിങ്", "cancelled": "ക്യാൻസൽ ചെയ്തു", "local": "ലോക്കൽ", "remote": "റിമോട്ട്", "show console toggler": "Console ടോഗ്ഗ്‌ലെർ കാണിക്കുക", "binary file": "ഈ ഫിയലിൽ binary ഡാറ്റാ ഇണ്ട്, നിങ്ങൾക്ക് ഇത് ഓപ്പൺ ചെയണോ?", "relative line numbers": "റിലേറ്റീവ് ലൈൻ നമ്പേഴ്സ്", "elastic tabstops": "ഏലസ്റ്റിക് ടാബ്‌സ്‌റ്റോപ്സ്", "line based rtl switching": "ലൈൻ അടിസ്ഥാനമാക്കിയുള്ള RTL സ്വിച്ചിംഗ്", "hard wrap": "ഹാർഡ് Wrap", "spellcheck": "സ്പെല്ലചെക്ക്", "wrap method": "Wrap മെത്തേഡ്", "use textarea for ime": "IME-യ്‌ക്ക് ടെക്‌സ്‌റ്റേറിയ ഉപയോഗിക്കുക", "invalid plugin": "പ്ലെഗിൻ തെറ്റ് ആണ്", "type command": "കമാൻഡ് ടൈപ്പ് ചെയുക", "plugin": "പ്ലെഗിൻ", "quicktools trigger mode": "ക്വിക്ട്ടൂൾസ് ട്രിഗ്ഗർ മോഡ്", "print margin": "പ്രിന്റ് മാർജിൻ", "touch move threshold": "ടച്ച് മൂവ് ത്രെഷോൾഡ്", "info-retryremotefsafterfail": "പരാജയപ്പെടുമ്പോൾ FTP/SFTP കണക്ഷൻ വീണ്ടും ശ്രമിക്കുക", "info-fullscreen": "ഹോം സ്ക്രീനിൽ ടൈറ്റിൽ ബാർ മറയ്ക്കുക.", "info-checkfiles": "ആപ്പ് പശ്ചാത്തലത്തിലായിരിക്കുമ്പോൾ ഫയലിലെ മാറ്റങ്ങൾ പരിശോധിക്കുക.", "info-console": "JavaScript കൺസോൾ തിരഞ്ഞെടുക്കുക. ലെഗസി ഡിഫോൾട്ട് കൺസോളാണ്, എരുഡ ഒരു മൂന്നാം കക്ഷി കൺസോളാണ്.", "info-keyboardmode": "ടെക്സ്റ്റ് ഇൻപുട്ടിനുള്ള കീബോർഡ് മോഡ്, നിർദ്ദേശങ്ങളൊന്നും നിർദ്ദേശങ്ങൾ മറയ്ക്കുകയും സ്വയമേവ ശരിയാക്കുകയും ചെയ്യും. നിർദ്ദേശങ്ങളൊന്നും പ്രവർത്തിക്കുന്നില്ലെങ്കിൽ, നിർദ്ദേശങ്ങളൊന്നും ആക്രമണാത്മകമല്ല എന്നതിലേക്ക് മൂല്യം മാറ്റാൻ ശ്രമിക്കുക.", "info-rememberfiles": "ആപ്പ് അടയ്‌ക്കുമ്പോൾ തുറന്ന ഫയലുകൾ ഓർക്കുക.", "info-rememberfolders": "ആപ്പ് അടയ്‌ക്കുമ്പോൾ തുറന്ന ഫോൾഡറുകൾ ഓർക്കുക.", "info-floatingbutton": "ദ്രുത ഉപകരണങ്ങൾ ഫ്ലോട്ടിംഗ് ബട്ടൺ കാണിക്കുക അല്ലെങ്കിൽ മറയ്ക്കുക.", "info-openfilelistpos": "സജീവ ഫയലുകളുടെ ലിസ്റ്റ് എവിടെ കാണിക്കണം.", "info-touchmovethreshold": "നിങ്ങളുടെ ഉപകരണ ടച്ച് സെൻസിറ്റിവിറ്റി വളരെ ഉയർന്നതാണെങ്കിൽ, ആകസ്മികമായ ടച്ച് നീക്കം തടയാൻ നിങ്ങൾക്ക് ഈ മൂല്യം വർദ്ധിപ്പിക്കാം.", "info-scroll-settings": "ഈ ക്രമീകരണങ്ങളിൽ ടെക്സ്റ്റ് റാപ്പ് ഉൾപ്പെടെയുള്ള സ്ക്രോൾ ക്രമീകരണങ്ങൾ അടങ്ങിയിരിക്കുന്നു.", "info-animation": "ആപ്പ് മന്ദഗതിയിലാണെന്ന് തോന്നുന്നുവെങ്കിൽ, ആനിമേഷൻ പ്രവർത്തനരഹിതമാക്കുക.", "info-quicktoolstriggermode": "ദ്രുത ടൂളുകളിലെ ബട്ടൺ പ്രവർത്തിക്കുന്നില്ലെങ്കിൽ, ഈ മൂല്യം മാറ്റാൻ ശ്രമിക്കുക.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "ഉടമസ്ഥതയിലുള്ളത്", "api_error": "API സെർവർ പ്രവർത്തനരഹിതമാണ്, കുറച്ച് സമയത്തിന് ശേഷം ശ്രമിക്കുക.", "installed": "ഇൻസ്റ്റാൾ ചെയ്തു", "all": "എല്ലാം", "medium": "മീഡിയം", "refund": "റീഫണ്ട്", "product not available": "പ്രോഡക്റ്റ് ആവില്ലബിൾ അല്ല", "no-product-info": "ഈ പ്രോഡക്റ്റ് ഇപ്പൊ നിങ്ങളുടെ നാട്ടിൽ അവൈലബിൾ അല്ല, ദയവായി പിന്നെ ട്രൈ ചെയ്തു നോക്കുക.", "close": "ക്ലോസ്", "explore": "Explore", "key bindings updated": "കീ ബൈൻഡിംഗുകൾ അപ്ഡേറ്റ് ചെയ്തു", "search in files": "ഫയലുകളിൽ തിരയുക", "exclude files": "എസ്ക്ലൂടെ ഫയൽസ്", "include files": "ഇൻക്ലൂടെ ഫയൽസ്", "search result": "{matches} results in {files} files.", "invalid regex": "അസാധുവായ റെഗുലർ എക്സ്പ്രഷൻ: {message}.", "bottom": "Bottom", "save all": "സേവ് ആൾ", "close all": "ക്ലോസ് ആൾ", "unsaved files warning": "ചില ഫയലുകൾ സേവ് ചെയ്തിട്ടില്ല. എന്താണ് ചെയ്യേണ്ടതെന്ന് തിരഞ്ഞെടുക്കുക 'ശരി' ക്ലിക്ക് ചെയ്യുക അല്ലെങ്കിൽ തിരികെ പോകാൻ 'റദ്ദാക്കുക' അമർത്തുക.", "save all warning": "എല്ലാ ഫയലുകളും സംരക്ഷിച്ച് അടയ്ക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പഴയപടിയാക്കാനാകില്ല.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "എല്ലാ ഫയലുകളും അടയ്ക്കണമെന്ന് തീർച്ചയാണോ? സംരക്ഷിക്കാത്ത മാറ്റങ്ങൾ നിങ്ങൾക്ക് നഷ്‌ടമാകും, ഈ പ്രവർത്തനം പഴയപടിയാക്കാനാകില്ല.", "refresh": "പുതുക്കുക", "shortcut buttons": "കുറുക്കുവഴി ബട്ടണുകൾ", "no result": "ഫലമില്ല", "searching...": "തിരയുന്നു...", "quicktools:ctrl-key": "കൺട്രോൾ/കമാൻഡ് കീ", "quicktools:tab-key": "ടാബ് കീ", "quicktools:shift-key": "ഷിഫ്റ്റ് കീ", "quicktools:undo": "പഴയപടിയാക്കുക", "quicktools:redo": "വീണ്ടും ചെയ്യുക", "quicktools:search": "ഫയലിൽ തിരയുക", "quicktools:save": "ഫയൽ സംരക്ഷിക്കുക", "quicktools:esc-key": "എസ്കേപ്പ് കീ", "quicktools:curlybracket": "ചുരുണ്ട ബ്രാക്കറ്റ് ചേർക്കുക", "quicktools:squarebracket": "ചതുര ബ്രാക്കറ്റ് ചേർക്കുക", "quicktools:parentheses": "പരാൻതീസിസുകൾ ചേർക്കുക", "quicktools:anglebracket": "ആംഗിൾ ബ്രാക്കറ്റ് ചേർക്കുക", "quicktools:left-arrow-key": "ഇടത് അമ്പടയാള കീ", "quicktools:right-arrow-key": "വലത് അമ്പടയാള കീ", "quicktools:up-arrow-key": "മുകളിലേക്കുള്ള അമ്പടയാള കീ", "quicktools:down-arrow-key": "താഴേക്കുള്ള അമ്പടയാള കീ", "quicktools:moveline-up": "ലൈൻ അപ്പ് നീക്കുക", "quicktools:moveline-down": "ലൈൻ താഴേക്ക് നീക്കുക", "quicktools:copyline-up": "ലൈൻ അപ്പ് പകർത്തുക", "quicktools:copyline-down": "ലൈൻ താഴേക്ക് പകർത്തുക", "quicktools:semicolon": "അർദ്ധവിരാമം ചേർക്കുക", "quicktools:quotation": "ഉദ്ധരണി ചേർക്കുക", "quicktools:and": "തിരുകുക, ചിഹ്നം", "quicktools:bar": "ബാർ ചിഹ്നം ചേർക്കുക", "quicktools:equal": "തുല്യ ചിഹ്നം ചേർക്കുക", "quicktools:slash": "സ്ലാഷ് ചിഹ്നം ചേർക്കുക", "quicktools:exclamation": "ഇൻസർട് എക്സലമഷൻ", "quicktools:alt-key": "Alt കീ", "quicktools:meta-key": "Windows/Meta കീ", "info-quicktoolssettings": "നിങ്ങളുടെ കോഡിംഗ് അനുഭവം മെച്ചപ്പെടുത്താൻ എഡിറ്ററിന് താഴെയുള്ള Quicktools കണ്ടെയ്‌നറിൽ കുറുക്കുവഴി ബട്ടണുകളും കീബോർഡ് കീകളും ഇഷ്ടാനുസൃതമാക്കുക.", "info-excludefolders": "node_modules ഫോൾഡറിൽ നിന്നുള്ള എല്ലാ ഫയലുകളും അവഗണിക്കാൻ **/node_modules/** പാറ്റേൺ ഉപയോഗിക്കുക. ഇത് ഫയലുകളെ ലിസ്റ്റുചെയ്യുന്നതിൽ നിന്ന് ഒഴിവാക്കുകയും ഫയൽ തിരയലിൽ ഉൾപ്പെടുത്തുന്നതിൽ നിന്ന് തടയുകയും ചെയ്യും.", "missed files": "തിരയൽ ആരംഭിച്ചതിന് ശേഷം സ്‌കാൻ ചെയ്‌ത {count} ഫയലുകൾ തിരയലിൽ ഉൾപ്പെടുത്തില്ല.", "remove": "നീക്കം ചെയ്യുക", "quicktools:command-palette": "കമാൻഡ് പല്ലെറ്റ്", "default file encoding": "ഡിഫോൾട്ട് ഫയൽ എൻകോഡിംഗ്", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "ഇല്ലാതാക്കൽ സ്ഥിരീകരിക്കുക: '{name}'. ഈ പ്രവർത്തനം പഴയപടിയാക്കാനാകില്ല. തുടരണോ?", "change encoding": "'{file}' എന്ന ഫയൽ '{encoding}' എൻകോഡിംഗ് ഉപയോഗിച്ച് വീണ്ടും തുറക്കണോ? ഈ പ്രവർത്തനം ഫയലിൽ വരുത്തിയ സേവ് ചെയ്യാത്ത മാറ്റങ്ങളുടെ നഷ്ടത്തിന് കാരണമാകും. നിങ്ങൾക്ക് വീണ്ടും തുറക്കുന്നത് തുടരണോ?", "reopen file": "'{file}' എന്ന ഫയൽ വീണ്ടും തുറക്കണോ? സേവ് ചെയ്യാത്ത എല്ലാ മാറ്റങ്ങളും നഷ്‌ടമാകും.", "plugin min version": "Acode - {v-code} മുകളിൽ മാത്രമേ {name} ലഭ്യമാകുകയുള്ളൂ. അപ്ഡേറ്റ് ചെയ്യാൻ ഇവിടെ ക്ലിക്ക് ചെയ്യുക.", "color preview": "കളർ പ്രിവ്യൂ", "confirm": "സ്ഥിരീകരിക്കുക", "list files": "{name} ലെ എല്ലാ ഫയലുകളും കാണിക്കണോ? വളരെയധികം ഫയലുകൾ ആപ്പിനെ ക്രാഷ് ചെയ്തേക്കാം.", "problems": "പ്രശ്നങ്ങൾ", "show side buttons": "സൈഡ് ബട്ടണുകൾ കാണിക്കുക", "bug_report": "Submit a Bug Report", "verified publisher": "പരിശോധിച്ച പ്രസാധകൻ", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "സ്പോൺസർ", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/mm-unicode.json ================================================ { "lang": "ဗမာစာ (Unicode)", "about": "ကျွန်တော်တို့အကြောင်း", "active files": "လက်ရှိဖိုင်များ", "alert": "သတိပေးချက်", "app theme": "App Theme", "autocorrect": "အလိုအလျောက်အမှားစစ်မည်လား?", "autosave": "အလိုအလျောက်သိမ်းဆည်းမည်။", "cancel": "ပယ်ဖျက်မည်", "change language": "ဘာသာစကားပြောင်းမည်", "choose color": "အရောင်ရွေးပါ", "clear": "ရှင်းလင်းပါ", "close app": "ယခုဆော့ဝဲကိုပိတ်မှာလား?", "commit message": "commit message", "console": "console", "conflict error": "လွဲနေပါသလာ?နောက် commit ကိုစောင့်ပါ။", "copy": "ကူမည်", "create folder error": "စိတ်မကောင်းပါဘူး။Folderတည်ဆောက်လို့မရပါဘူး။", "cut": "ဖြတ်မည်", "delete": "ဖျက်မည်", "dependencies": "Dependencies", "delay": "Time in milliseconds", "editor settings": "Editor settings", "editor theme": "Editor theme", "enter file name": "File နာမည်ထည့်ပါ", "enter folder name": "Folder နာမည်ထည့်ပါ", "empty folder message": "Folder မရှိပါ", "enter line number": "လိုင်းနံပါတ်ထည့်ပါ", "error": "error", "failed": "မအောင်မြင်ပါ။ထပ်ကြိုးစားပါ။", "file already exists": "File ရှိပြီးသားဖြစ်ပါသည်။", "file already exists force": "File ရှိပြီးသားဖြစ်ပါသည်။File ကိုထပ်ရေးမည်လား။", "file changed": " ပြောင်းလဲသွားပြီ။ဖိုင်ကိုပြန်ဖတ်ပါ။", "file deleted": "file ဖျက်ပြီးပါပြီ", "file is not supported": "file ကိုဖွင့်ဖို့ခွင့်မပြပါ", "file not supported": "ယခု file ကိုဖွင့်ဖို့ခွင့်မပြုပါ", "file too large": "File Size ကြီးလွန်းတယ်။အများဆုံး {size} ပဲထောက်ပံ့တယ်", "file renamed": "file နာမည်ပြောင်းပြီးပါပြီ", "file saved": "file သိမ်းပြီးပါပြီ။", "folder added": "folder ကိုထပ်ပေါင်းထည့်ပြီးပါပြီ", "folder already added": "folder ကထည့်ပြီးသားဖြစ်ပါတယ်", "font size": "Font အရွယ်အစား", "goto": "Go to line", "icons definition": "Icons definition", "info": "သတင်းအချက်အလက်", "invalid value": "မမှန်ကန်သော value ဖြစ်သည်", "language changed": "ဘာသာစကားကိုအောင်မြင်စွာပြောင်းလည်းပြီးပါပြီ။", "linting": "syntax error စစ်ဆေးမည်", "logout": "ထွက်မည်", "loading": "ဖတ်နေတုန်း", "my profile": "ကျွန်ုပ် Profile ", "new file": "Fileအသစ်ဆောက်မည်", "new folder": "Folder အသစ်ဆောက်မည်", "no": "မဟုတ်ဘူး", "no editor message": "ရှိပြီးသားဖိုင်ကိုဖွင့်မှာလား?(သို့မဟုတ်) App Menu မှFile (သိုမဟုတ်) Folder အသစ်တည်ဆောက်ပါ", "not set": "Not set", "unsaved files close app": "ဖိုင်မသိမ်းရသေးဘူး။ထွက်တော့မှာလား?", "notice": "အသိပေးစာ", "open file": "File ဖွင့်ပါ", "open files and folders": "File နဲ့ Folder များကိုဖွင့်ပါ", "open folder": "Folder ဖွင့်ပါ", "open recent": "မကြာသေးခင်ကဖွင့်ထားများ", "ok": "ok", "overwrite": "ပြင်ရေးမည်", "paste": "paste", "preview mode": "Preview Mode", "read only file": "ဖတ်ခွင့်ပဲပြုပါတယ်။သိမ်းလို့မရပါ။Save as နဲ့သိမ်းပါ", "reload": "ပြန်ဖတ်မည်", "rename": "နာမည်ပြန်ပေးမည်", "replace": "အစားထိုးမည်", "required": "လိုအပ်ပါတယ်", "run your web app": "Web App ကို Run ပါ", "save": "သိမ်းပါ", "saving": "သိမ်းနေတုန်း", "save as": "Save as", "save file to run": "Browser မှာrun ဖို့ဖိုင်ကိုသိမ်းပါ", "search": "ရှာမည်", "see logs and errors": "logs errors တွေကိုမြင်လား?", "select folder": "Folder ရွေးပါ", "settings": "settings", "settings saved": "Settings သိမ်းပြီးပါပြီ", "show line numbers": "Line နံပါတ်များပြပါ", "show hidden files": "ဝှက်ထားသည့်File များပြပါ", "show spaces": "Space တွေပြပါ", "soft tab": "Soft tab", "sort by name": "နာမည်နဲ့စီပါ", "success": "အောင်မြင်ပါတယ်။", "tab size": "Tab အရွယ်အစား", "text wrap": "Text wrap / Word wrap", "theme": "theme", "unable to delete file": "ဖိုင်ဖျက်လို့မရပါ", "unable to open file": "ဝမ်းနည်းပါတယ်။ဖိုင်ဖွင့်မရပါ။", "unable to open folder": "ဝမ်းနည်းပါတယ်။Folderဖွင့်မရပါ။", "unable to save file": "ဝမ်းနည်းပါတယ်။ဖိုင်သိမ်းမရပါ။", "unable to rename": "နာမည်ပြန်ပြောင်းလို့မရပါ", "unsaved file": "ဖိုင်မသိမ်းရသေးဘူး။ဘာဖြစ်ဖြစ်ပိတ်မှာလား?", "warning": "သတိ", "use emmet": "Use emmet", "use quick tools": "မြန်ဆန်စေမည့်ကိရိယာများသုံးမည်", "yes": "ဟုတ်ပြီ", "encoding": "Text encoding", "syntax highlighting": "Syntax အရောင်", "read only": "ဖတ်လို့ပဲရပါတယ်", "select all": "အကုန်ရွေးပါ", "select branch": "Branch ရွေးပါ", "create new branch": "Branch အသစ်ဖန်တီးပါ", "use branch": "Branch ကိုသုံးပါ", "new branch": "Branch အသစ်", "branch": "branch", "key bindings": "Key bindings", "edit": "ပြင်မည်", "reset": "reset", "color": "အရောင်", "select word": "စကားလုံးရွေးမည်", "quick tools": "Quick tools", "select": "ရွေးမည်", "editor font": "Editor font", "new project": "Project အသစ်", "format": "format", "project name": "Project နာမည်", "unsupported device": "ခင်ဗျား Device မှာ ယခု Theme ကိုမထောက်ပံ့ပါ။", "vibrate on tap": "ထိထားရင်တုန်ပါ", "copy command is not supported by ftp.": "FTPမှာကူယူတာကိုမထောက်ပံ့ပါ။", "support title": "Acode ကိုထောက်ပံ့ပါ ❤️", "fullscreen": "မျက်နှာပြင်အပြည့်", "animation": "အထူးပြုလုပ်ချက်", "backup": "အရံသိမ်းမည်", "restore": "ပြန်လည်သိုလှောင်မည်", "backup successful": "အရံသိမ်းတာအောင်မြင်ပါတယ်", "invalid backup file": "အရံသိမ်းသည့် File ကမမှန်ကန်ပါ။", "add path": "File ထားတဲ့နေရာထည့်ပါ", "live autocompletion": "Live autocompletion", "file properties": "File အချက်အလက်များ", "path": "လမ်းကြောင်း", "type": "အမျိုးအစား", "word count": "စကားလုံးအရေအတွက်စုစုပေါင်း", "line count": "Line အရေအတွက်စုစုပေါင်း", "last modified": "နောက်ဆုံးပြင်ဆင်ခဲ့သည့်အချိန်", "size": "Size", "share": "မျှဝေမည်", "show print margin": "Show print margin", "login": "login", "scrollbar size": "Scrollbar အရွယ်အစား", "cursor controller size": "Cursor အရွယ်အစား", "none": "none", "small": "သေးမည်", "large": "ကြီးမည်", "floating button": "Floating button", "confirm on exit": "App မှထွက်လျှင် Confirm Button နှိပ်ရမည်", "show console": "console ကိုပြမည်", "image": "Image", "insert file": "Insert file", "insert color": "Insert color", "powersave mode warning": "Turn off power saving mode to preview in external browser.", "exit": "Exit", "custom": "custom", "reset warning": "Are you sure you want to reset theme?", "theme type": "Theme type", "light": "light", "dark": "dark", "file browser": "File Browser", "operation not permitted": "Operation not permitted", "no such file or directory": "No such file or directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Not a directory", "is a directory": "Is a directory", "invalid argument": "Invalid argument", "too many open files in system": "Too many open files in system", "too many open files": "Too many open files", "text file busy": "Text file busy", "no space left on device": "No space left on device", "read-only file system": "Read-only file system", "file name too long": "File name too long", "too many users": "Too many users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "An error occurred", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "Save file", "save file as": "Save file as", "files": "Files", "help": "Help", "file has been deleted": "{file} has been deleted!", "feature not available": "This feature is only available in paid version of the app.", "deleted file": "Deleted file", "line height": "Line height", "preview info": "If you want run the active file, tap and hold on play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "Close file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Open in browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "Rate Acode", "support": "Support", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme and key bindings. It will not backup your FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "စပွန်ဆာ", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/mm-zawgyi.json ================================================ { "lang": "ဗမာစာ (Zawgyi)", "about": "ကြၽန္ေတာ္တို႔အေၾကာင္း", "active files": "လက္ရွိဖိုင္မ်ား", "alert": "သတိေပးခ်က္", "app theme": "App Theme", "autocorrect": "အလိုအေလ်ာက္အမွားစစ္မည္လား?", "autosave": "အလိုအေလ်ာက္သိမ္းဆည္းမည္။", "cancel": "ပယ္ဖ်က္မည္", "change language": "ဘာသာစကားေျပာင္းမည္", "choose color": "အေရာင္ေ႐ြးပါ", "clear": "ရွင္းလင္းပါ", "close app": "ယခုေဆာ့ဝဲကိုပိတ္မွာလား?", "commit message": "commit message", "console": "console", "conflict error": "လြဲေနပါသလာ?ေနာက္ commit ကိုေစာင့္ပါ။", "copy": "ကူမည္", "create folder error": "စိတ္မေကာင္းပါဘူး။Folderတည္ေဆာက္လို႔မရပါဘူး။", "cut": "ျဖတ္မည္", "delete": "ဖ်က္မည္", "dependencies": "Dependencies", "delay": "Time in milliseconds", "editor settings": "Editor settings", "editor theme": "Editor theme", "enter file name": "File နာမည္ထည့္ပါ", "enter folder name": "Folder နာမည္ထည့္ပါ", "empty folder message": "Folder မရွိပါ", "enter line number": "လိုင္းနံပါတ္ထည့္ပါ", "error": "error", "failed": "မေအာင္ျမင္ပါ။ထပ္ႀကိဳးစားပါ။", "file already exists": "File ရွိၿပီးသားျဖစ္ပါသည္။", "file already exists force": "File ရွိၿပီးသားျဖစ္ပါသည္။File ကိုထပ္ေရးမည္လား။", "file changed": " ေျပာင္းလဲသြားၿပီ။ဖိုင္ကိုျပန္ဖတ္ပါ။", "file deleted": "file ဖ်က္ၿပီးပါၿပီ", "file is not supported": "file ကိုဖြင့္ဖို႔ခြင့္မျပပါ", "file not supported": "ယခု file ကိုဖြင့္ဖို႔ခြင့္မျပဳပါ", "file too large": "File Size ႀကီးလြန္းတယ္။အမ်ားဆုံး {size} ပဲေထာက္ပံ့တယ္", "file renamed": "file နာမည္ေျပာင္းၿပီးပါၿပီ", "file saved": "file သိမ္းၿပီးပါၿပီ။", "folder added": "folder ကိုထပ္ေပါင္းထည့္ၿပီးပါၿပီ", "folder already added": "folder ကထည့္ၿပီးသားျဖစ္ပါတယ္", "font size": "Font အ႐ြယ္အစား", "goto": "Go to line", "icons definition": "Icons definition", "info": "သတင္းအခ်က္အလက္", "invalid value": "မမွန္ကန္ေသာ value ျဖစ္သည္", "language changed": "ဘာသာစကားကိုေအာင္ျမင္စြာေျပာင္းလည္းၿပီးပါၿပီ။", "linting": "syntax error စစ္ေဆးမည္", "logout": "ထြက္မည္", "loading": "ဖတ္ေနတုန္း", "my profile": "ကြၽန္ုပ္ Profile ", "new file": "Fileအသစ္ေဆာက္မည္", "new folder": "Folder အသစ္ေဆာက္မည္", "no": "မဟုတ္ဘူး", "no editor message": "ရွိၿပီးသားဖိုင္ကိုဖြင့္မွာလား?(သို႔မဟုတ္) App Menu မွFile (သိုမဟုတ္) Folder အသစ္တည္ေဆာက္ပါ", "not set": "Not set", "unsaved files close app": "ဖိုင္မသိမ္းရေသးဘူး။ထြက္ေတာ့မွာလား?", "notice": "အသိေပးစာ", "open file": "File ဖြင့္ပါ", "open files and folders": "File နဲ႕ Folder မ်ားကိုဖြင့္ပါ", "open folder": "Folder ဖြင့္ပါ", "open recent": "မၾကာေသးခင္ကဖြင့္ထားမ်ား", "ok": "ok", "overwrite": "ျပင္ေရးမည္", "paste": "paste", "preview mode": "Preview Mode", "read only file": "ဖတ္ခြင့္ပဲျပဳပါတယ္။သိမ္းလို႔မရပါ။Save as နဲ႕သိမ္းပါ", "reload": "ျပန္ဖတ္မည္", "rename": "နာမည္ျပန္ေပးမည္", "replace": "အစားထိုးမည္", "required": "လိုအပ္ပါတယ္", "run your web app": "Web App ကို Run ပါ", "save": "သိမ္းပါ", "saving": "သိမ္းေနတုန္း", "save as": "Save as", "save file to run": "Browser မွာrun ဖို႔ဖိုင္ကိုသိမ္းပါ", "search": "ရွာမည္", "see logs and errors": "logs errors ေတြကိုျမင္လား?", "select folder": "Folder ေ႐ြးပါ", "settings": "settings", "settings saved": "Settings သိမ္းၿပီးပါၿပီ", "show line numbers": "Line နံပါတ္မ်ားျပပါ", "show hidden files": "ဝွက္ထားသည့္File မ်ားျပပါ", "show spaces": "Space ေတြျပပါ", "soft tab": "Soft tab", "sort by name": "နာမည္နဲ႕စီပါ", "success": "ေအာင္ျမင္ပါတယ္။", "tab size": "Tab အ႐ြယ္အစား", "text wrap": "Text wrap / Word wrap", "theme": "theme", "unable to delete file": "ဖိုင္ဖ်က္လို႔မရပါ", "unable to open file": "ဝမ္းနည္းပါတယ္။ဖိုင္ဖြင့္မရပါ။", "unable to open folder": "ဝမ္းနည္းပါတယ္။Folderဖြင့္မရပါ။", "unable to save file": "ဝမ္းနည္းပါတယ္။ဖိုင္သိမ္းမရပါ။", "unable to rename": "နာမည္ျပန္ေျပာင္းလို႔မရပါ", "unsaved file": "ဖိုင္မသိမ္းရေသးဘူး။ဘာျဖစ္ျဖစ္ပိတ္မွာလား?", "warning": "သတိ", "use emmet": "Use emmet", "use quick tools": "ျမန္ဆန္ေစမည့္ကိရိယာမ်ားသုံးမည္", "yes": "ဟုတ္ၿပီ", "encoding": "Text encoding", "syntax highlighting": "Syntax အေရာင္", "read only": "ဖတ္လို႔ပဲရပါတယ္", "select all": "အကုန္ေ႐ြးပါ", "select branch": "Branch ေ႐ြးပါ", "create new branch": "Branch အသစ္ဖန္တီးပါ", "use branch": "Branch ကိုသုံးပါ", "new branch": "Branch အသစ္", "branch": "branch", "key bindings": "Key bindings", "edit": "ျပင္မည္", "reset": "reset", "color": "အေရာင္", "select word": "စကားလုံးေ႐ြးမည္", "quick tools": "Quick tools", "select": "ေ႐ြးမည္", "editor font": "Editor font", "new project": "Project အသစ္", "format": "format", "project name": "Project နာမည္", "unsupported device": "ခင္ဗ်ား Device မွာ ယခု Theme ကိုမေထာက္ပံ့ပါ။", "vibrate on tap": "ထိထားရင္တုန္ပါ", "copy command is not supported by ftp.": "FTPမွာကူယူတာကိုမေထာက္ပံ့ပါ။", "support title": "Acode ကိုေထာက္ပံ့ပါ ❤️", "fullscreen": "မ်က္ႏွာျပင္အျပည့္", "animation": "အထူးျပဳလုပ္ခ်က္", "backup": "အရံသိမ္းမည္", "restore": "ျပန္လည္သိုေလွာင္မည္", "backup successful": "အရံသိမ္းတာေအာင္ျမင္ပါတယ္", "invalid backup file": "အရံသိမ္းသည့္ File ကမမွန္ကန္ပါ။", "add path": "File ထားတဲ့ေနရာထည့္ပါ", "live autocompletion": "Live autocompletion", "file properties": "File အခ်က္အလက္မ်ား", "path": "လမ္းေၾကာင္း", "type": "အမ်ိဳးအစား", "word count": "စကားလုံးအေရအတြက္စုစုေပါင္း", "line count": "Line အေရအတြက္စုစုေပါင္း", "last modified": "ေနာက္ဆုံးျပင္ဆင္ခဲ့သည့္အခ်ိန္", "size": "Size", "share": "မွ်ေဝမည္", "show print margin": "Show print margin", "login": "login", "scrollbar size": "Scrollbar အ႐ြယ္အစား", "cursor controller size": "Cursor အ႐ြယ္အစား", "none": "none", "small": "ေသးမည္", "large": "ႀကီးမည္", "floating button": "Floating button", "confirm on exit": "App မွထြက္လွ်င္ Confirm Button ႏွိပ္ရမည္", "show console": "console ကိုျပမည္", "image": "Image", "insert file": "Insert file", "insert color": "Insert color", "powersave mode warning": "Turn off power saving mode to preview in external browser.", "exit": "Exit", "custom": "custom", "reset warning": "Are you sure you want to reset theme?", "theme type": "Theme type", "light": "light", "dark": "dark", "file browser": "File Browser", "operation not permitted": "Operation not permitted", "no such file or directory": "No such file or directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Not a directory", "is a directory": "Is a directory", "invalid argument": "Invalid argument", "too many open files in system": "Too many open files in system", "too many open files": "Too many open files", "text file busy": "Text file busy", "no space left on device": "No space left on device", "read-only file system": "Read-only file system", "file name too long": "File name too long", "too many users": "Too many users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "An error occurred", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "Save file", "save file as": "Save file as", "files": "Files", "help": "Help", "file has been deleted": "{file} has been deleted!", "feature not available": "This feature is only available in paid version of the app.", "deleted file": "Deleted file", "line height": "Line height", "preview info": "If you want run the active file, tap and hold on play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "Close file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Open in browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "Rate Acode", "support": "Support", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme and key bindings. It will not backup your FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "စပွန်ဆာ", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/pl-pl.json ================================================ { "lang": "Polski", "about": "O aplikacji", "active files": "Aktywne pliki", "alert": "Alert", "app theme": "Motyw aplikacji", "autocorrect": "Aktywować autokorektę?", "autosave": "Autozapis", "cancel": "Anuluj", "change language": "Zmień język", "choose color": "Wybierz kolor", "clear": "wyczyść", "close app": "Zamknąć aplikację?", "commit message": "Wiadomość zatwierdzenia (commita)", "console": "Konsola", "conflict error": "Konflikt! Proszę zaczekać przed następnym zatwierdzeniem (commitem).", "copy": "Kopiuj", "create folder error": "Nie udało się utworzyć folderu", "cut": "Wytnij", "delete": "Usuń", "dependencies": "Zależności", "delay": "Czas w milisekundach", "editor settings": "Ustawienia edytora", "editor theme": "Motyw edytora", "enter file name": "Wprowadź nazwę pliku", "enter folder name": "Wprowadź nazwę folderu", "empty folder message": "Pusty folder", "enter line number": "Wprowadź numer wiersza", "error": "Błąd", "failed": "Niepowodzenie", "file already exists": "Plik już istnieje", "file already exists force": "Plik istnieje. Nadpisać go?", "file changed": " został zmodyfikowany, przeładować plik?", "file deleted": "Plik usunięty", "file is not supported": "Plik nie jest wspierany", "file not supported": "Ten typ pliku nie jest wspierany.", "file too large": "Plik jest zbyt duży. Maksymalny dozwolony rozmiar pliku to {size}", "file renamed": "zmieniono nazwę pliku", "file saved": "zapisano plik", "folder added": "dodano folder", "folder already added": "folder został już dodany", "font size": "Rozmiar czcionki", "goto": "Przejdź do wiersza", "icons definition": "Definicja ikon", "info": "Informacja", "invalid value": "Nieprawidłowa wartość", "language changed": "język został zmieniony pomyślnie", "linting": "Sprawdź błąd składni", "logout": "Wyloguj", "loading": "Ładowanie", "my profile": "Mój profil", "new file": "Nowy plik", "new folder": "Nowy folder", "no": "Nie", "no editor message": "Otwórz lub utwórz nowy plik i folder z poziomu menu", "not set": "Nie ustawiony", "unsaved files close app": "Niektóre pliki nie zostały jeszcze zapisane. Zamknąć aplikację?", "notice": "Komunikat", "open file": "Otwórz plik", "open files and folders": "Otwórz pliki i foldery", "open folder": "Otwórz folder", "open recent": "Ostatnio otwarte", "ok": "ok", "overwrite": "Nadpisz", "paste": "Wklej", "preview mode": "Tryb podglądu", "read only file": "Nie można zapisać pliku tylko do odczytu. Spróbuj zapisać go opcją Zapisz jako", "reload": "Przeładuj", "rename": "Zmień nazwę", "replace": "Zastąp", "required": "To pole jest wymagane", "run your web app": "Uruchom swoją aplikację webową", "save": "Zapisz", "saving": "Zapisywanie", "save as": "Zapisz jako", "save file to run": "Zapisz ten plik, aby uruchomić go w przeglądarce", "search": "Wyszukaj", "see logs and errors": "Zobacz błędy i logi", "select folder": "Wybierz folder", "settings": "Ustawienia", "settings saved": "Ustawienia zapisane", "show line numbers": "Pokaż numery wierszy", "show hidden files": "Pokaż ukryte pliki", "show spaces": "Pokaż spacje", "soft tab": "Miękka tabulacja", "sort by name": "Sortuj według nazwy", "success": "Sukces", "tab size": "Wielkość tabulacji", "text wrap": "Zawijanie tekstu", "theme": "Motyw", "unable to delete file": "nie można usunąć pliku", "unable to open file": "Nie udało się otworzyć pliku", "unable to open folder": "Nie udało się otworzyć folderu", "unable to save file": "Nie udało się zapisać pliku", "unable to rename": "Nie udało się zmienić nazwy", "unsaved file": "Ten plik nie został jeszcze zapisany, czy chcesz go mimo to zamknąć?", "warning": "Ostrzeżenie", "use emmet": "Użyj emmet", "use quick tools": "Użyj szybkich narzędzi", "yes": "Tak", "encoding": "Kodowanie tekstu", "syntax highlighting": "Podświetlanie składni", "read only": "Tylko do odczytu", "select all": "Zaznacz wszystko", "select branch": "Wybierz gałąź", "create new branch": "Utwórz nową gałąź", "use branch": "Użyj gałęzi", "new branch": "Nowa gałąź", "branch": "Gałąź", "key bindings": "Skróty klawiszowe", "edit": "Edycja", "reset": "Reset", "color": "Kolor", "select word": "Wybierz słowo", "quick tools": "Szybkie narzędzia", "select": "Wybierz", "editor font": "Czcionka edytora", "new project": "Nowy projekt", "format": "Formatuj", "project name": "Nazwa projektu", "unsupported device": "Twoje urządzenie nie wspiera tego motywu.", "vibrate on tap": "Wibracja przy dotknięciu", "copy command is not supported by ftp.": "Komenda copy nie jest wspierana przez ten serwer FTP.", "support title": "Wesprzyj Acode", "fullscreen": "Pełny ekran", "animation": "Animacja", "backup": "Kopia zapasowa", "restore": "Przywracanie", "backup successful": "Kopia zapasowa wykonana pomyślnie", "invalid backup file": "Nieprawidłowy plik kopii zapasowej", "add path": "Dodaj ścieżkę", "live autocompletion": "Autouzupełnianie kodu", "file properties": "Właściwości pliku", "path": "Ścieżka", "type": "Typ", "word count": "Ilość słów", "line count": "Ilość wierszy", "last modified": "Ostatnia modyfikacja", "size": "Rozmiar", "share": "Udostępnij", "show print margin": "Pokaż margines wydruku", "login": "Logowanie", "scrollbar size": "Rozmiar scrollbaru", "cursor controller size": "Rozmiar znacznika kursora", "none": "Brak", "small": "Mały", "large": "Duży", "floating button": "Pływający przycisk", "confirm on exit": "Potwierdź przy wyjściu", "show console": "Pokaż konsolę", "image": "Zdjęcie", "insert file": "Wprowadź plik", "insert color": "Wprowadź kolor", "powersave mode warning": "Wyłącz tryb oszczędzania, aby wyświetlić w zewnętrznej przeglądarce.", "exit": "Wyjście", "custom": "Niestandardowy", "reset warning": "Czy na pewno chcesz zresetować ten motyw?", "theme type": "Typ motywu", "light": "Jasny", "dark": "Ciemny", "file browser": "Przeglądarka plików", "operation not permitted": "Operacja niedozwolona", "no such file or directory": "Brak takiego pliku lub folderu", "input/output error": "Błąd wejścia/wyjścia", "permission denied": "Dostęp odmówiony", "bad address": "Zły adres", "file exists": "Plik istnieje", "not a directory": "Nie jest folderem", "is a directory": "Jest folderem", "invalid argument": "Nieprawidłowy argument", "too many open files in system": "Zbyt dużo otwartych plików w systemie", "too many open files": "Zbyt dużo otwartych plików", "text file busy": "Plik tekstowy zajęty", "no space left on device": "Brak miejsca na urządzeniu", "read-only file system": "System plików tylko do odczytu", "file name too long": "Zbyt długa nazwa pliku", "too many users": "Zbyt dużo użytkowników", "connection timed out": "Zbyt długi okres oczekiwania na połączenie", "connection refused": "Połączenie odrzucone", "owner died": "Właściciel zmarł", "an error occurred": "Wystąpił błąd", "add ftp": "Dodaj FTP", "add sftp": "Dodaj SFTP", "save file": "Zapisz plik", "save file as": "Zapisz plik jako", "files": "Pliki", "help": "Pomoc", "file has been deleted": "{file} został usunięty!", "feature not available": "Ta funkcja jest dostępna jedynie w płatnej wersji aplikacji.", "deleted file": "Usunięte pliki", "line height": "Wysokość wiersza", "preview info": "Jeśli chcesz uruchomić aktualnie wybrany plik, kliknij i przytrzymaj ikonę odtwarzania.", "manage all files": "Zezwól Acode zarządzać wszystkimi plikami w ustawieniach, aby z łatwością edytować pliki na twoim urządzeniu.", "close file": "Zamknij plik", "reset connections": "Zresetuj połączenia", "check file changes": "Sprawdzaj zmiany w plikach", "open in browser": "Otwórz w przeglądarce", "desktop mode": "Tryb desktopowy", "toggle console": "Przełącz konsolę", "new line mode": "Sekwencja końca wiersza", "add a storage": "Dodaj pamięć", "rate acode": "Oceń Acode", "support": "Wesprzyj", "downloading file": "Pobieranie {file}", "downloading...": "Pobieranie...", "folder name": "Nazwa folderu", "keyboard mode": "Tryb klawiatury", "normal": "Normalny", "app settings": "Ustawienia aplikacji", "disable in-app-browser caching": "Wyłącz cache w wbudowanej przeglądarce", "copied to clipboard": "Skopiowano do schowka", "remember opened files": "Zapamiętaj otwarte pliki", "remember opened folders": "Zapamiętaj otwarte foldery", "no suggestions": "Bez sugestii", "no suggestions aggressive": "Bez sugestii (agresywnie)", "install": "Instaluj", "installing": "Instalowanie...", "plugins": "Wtyczki", "recently used": "Ostatnio używane", "update": "Zaktualizuj", "uninstall": "Odinstaluj", "download acode pro": "Pobierz Acode Pro", "loading plugins": "Ładowanie wtyczek", "faqs": "Najczęściej zadawane pytania", "feedback": "Informacja zwrotna", "header": "Nagłówek", "sidebar": "Pasek boczny", "inapp": "W aplikacji", "browser": "Przeglądarka", "diagonal scrolling": "Przewijanie po przekątnej", "reverse scrolling": "Przewijanie wstecz", "formatter": "Formatter", "format on save": "Formatuj podczas zapisu", "remove ads": "Usuń reklamy", "fast": "Szybko", "slow": "Wolno", "scroll settings": "Ustawienia przewijania", "scroll speed": "Szybkość przewijania", "loading...": "Ładowanie...", "no plugins found": "Nie znaleziono wtyczek", "name": "Nazwa", "username": "Nazwa użytkownika", "optional": "opcjonalnie", "hostname": "Nazwa hosta", "password": "Hasło", "security type": "Typ zabezpieczeń", "connection mode": "Typ połączenia", "port": "Port", "key file": "Plik klucza", "select key file": "Wybierz plik klucza", "passphrase": "Fraza do hasła", "connecting...": "Łączenie...", "type filename": "Wpisz nazwę pliku", "unable to load files": "Nie można załadować plików", "preview port": "Port podglądu", "find file": "Wyszukaj plik", "system": "System", "please select a formatter": "Wybierz formatter", "case sensitive": "Uwzględnianie wielkości liter", "regular expression": "Wyrażenia regularne", "whole word": "Całe słowo", "edit with": "Edytuj za pomocą", "open with": "Otwórz za pomocą", "no app found to handle this file": "Nie znaleziono aplikacji obsługującej ten plik", "restore default settings": "Przywróć ustawienia domyślne", "server port": "Port serwera", "preview settings": "Ustawienia podglądu", "preview settings note": "Jeśli port podglądu i port serwera są różne, aplikacja nie uruchomi serwera i zamiast tego otworzy https://: w przeglądarce lub w przeglądarce w aplikacji. Jest to przydatne, gdy serwer jest uruchomiony w innej lokalizacji", "backup/restore note": "Tworzy kopię zapasową tylko ustawień, niestandardowego motywu i przypisanych klawiszy. Nie tworzy kopii zapasowej FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Ponów ftp/sftp w przypadku niepowodzenia", "more": "Więcej", "thank you :)": "Dziękuję :)", "purchase pending": "zakup w trakcie realizacji", "cancelled": "anulowany", "local": "Lokalne", "remote": "Zdalne", "show console toggler": "Pokaż przełącznik konsoli", "binary file": "Ten plik zawiera dane binarne, czy chcesz go otworzyć?", "relative line numbers": "Relatywne numery linii", "elastic tabstops": "Elastyczne tabulatory", "line based rtl switching": "Przełącznik RTL oparty na linii", "hard wrap": "Twarde zawijanie", "spellcheck": "Sprawdzanie pisowni", "wrap method": "Metoda zawijania", "use textarea for ime": "Użyj textarea dla IME", "invalid plugin": "Nieprawidłowa wtyczka", "type command": "Wpisz polecenie", "plugin": "Wtyczka", "quicktools trigger mode": "Tryb wyzwalania szybkich narzędzi", "print margin": "Margines wydruku", "touch move threshold": "Próg reakcji na dotyk", "info-retryremotefsafterfail": "Ponawianie połączenia FTP/SFTP w przypadku niepowodzenia", "info-fullscreen": "Ukrywanie paska tytułu na ekranie głównym.", "info-checkfiles": "Sprawdza zmiany w plikach, gdy aplikacja działa w tle.", "info-console": "Wybór konsoli JavaScript. Legacy to domyślna konsola, eruda to konsola innej firmy.", "info-keyboardmode": "Tryb klawiatury do wprowadzania tekstu, brak sugestii ukryje sugestie i automatyczną korektę. Jeśli brak sugestii nie działa, spróbuj zmienić wartość na brak sugestii agresywnych.", "info-rememberfiles": "Pamiętaj otwarte pliki po zamknięciu aplikacji.", "info-rememberfolders": "Pamiętaj otwarte foldery po zamknięciu aplikacji.", "info-floatingbutton": "Pokaż lub ukryj pływający przycisk szybkich narzędzi.", "info-openfilelistpos": "Gdzie ma być wyświetlana lista aktywnych plików.", "info-touchmovethreshold": "Jeśli czułość urządzenia na dotyk jest zbyt wysoka, można zwiększyć tę wartość, aby zapobiec przypadkowemu dotknięciu.", "info-scroll-settings": "Ustawienia te zawierają ustawienia przewijania, w tym zawijanie tekstu.", "info-animation": "Jeśli aplikacja działa z opóźnieniem, wyłącz animację.", "info-quicktoolstriggermode": "Jeśli przycisk w szybkich narzędziach nie działa, spróbuj zmienić tę wartość.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Posiadane", "api_error": "Serwer API nie działa, spróbuj za jakiś czas.", "installed": "Zainstalowane", "all": "Wszystko", "medium": "Średni", "refund": "Zwrot", "product not available": "Produkt niedostępny", "no-product-info": "Ten produkt nie jest obecnie dostępny w Twoim kraju, spróbuj ponownie w późniejszym terminie.", "close": "Zamknij", "explore": "Eksploruj", "key bindings updated": "Zaktualizowano powiązania klawiszy", "search in files": "Wyszukaj w plikach", "exclude files": "Wyklucz pliki", "include files": "Uwzględnij pliki", "search result": "{matches} wyniki w {files} plikach.", "invalid regex": "Nieprawidłowe wyrażenie regularne: {message}.", "bottom": "Na dole", "save all": "Zapisz wszystko", "close all": "Zamknij wszystko", "unsaved files warning": "Niektóre pliki nie zostaną zapisane. Kliknij 'ok', aby wybrać, co chcesz zrobić, lub naciśnij 'anuluj', aby wrócić.", "save all warning": "Czy na pewno chcesz zapisać wszystkie pliki i zamknąć? Tego działania nie można cofnąć.", "save all changes warning": "Czy na pewno chcesz zapisać wszystkie pliki?", "close all warning": "Czy na pewno chcesz zamknąć wszystkie pliki? Niezapisane zmiany zostaną utracone, a działania tego nie można cofnąć.", "refresh": "Odśwież", "shortcut buttons": "Przyciski skrótów", "no result": "Brak wyników", "searching...": "Wyszukiwanie...", "quicktools:ctrl-key": "Klawisz Control/Command", "quicktools:tab-key": "Klawisz Tab", "quicktools:shift-key": "Klawisz Shift", "quicktools:undo": "Cofnij", "quicktools:redo": "Ponów", "quicktools:search": "Wyszukaj w plikach", "quicktools:save": "Zapisz plik", "quicktools:esc-key": "Klawisz Escape", "quicktools:curlybracket": "Wstaw nawias klamrowy", "quicktools:squarebracket": "Wstaw nawias kwadratowy", "quicktools:parentheses": "Wstaw nawiasy", "quicktools:anglebracket": "Wstaw nawias kątowy", "quicktools:left-arrow-key": "Klawisz strzałki w lewo", "quicktools:right-arrow-key": "Klawisz strzałki w prawo", "quicktools:up-arrow-key": "Klawisz strzałki w górę", "quicktools:down-arrow-key": "Klawisz strzałki w dół", "quicktools:moveline-up": "Przesuń linię do góry", "quicktools:moveline-down": "Przesuń linię w dół", "quicktools:copyline-up": "Kopiuj linię do góry", "quicktools:copyline-down": "Kopiuj linię w dół", "quicktools:semicolon": "Wstaw średnik", "quicktools:quotation": "Wstaw cudzysłów", "quicktools:and": "Wstaw symbol and", "quicktools:bar": "Wstaw symbol bar", "quicktools:equal": "Wstaw symbol equal", "quicktools:slash": "Wstaw symbol ukośnika", "quicktools:exclamation": "Wstaw wykrzyknik", "quicktools:alt-key": "Klawisz Alt", "quicktools:meta-key": "Klawisz Windows/Meta", "info-quicktoolssettings": "Dostosuj przyciski skrótów i klawisze klawiatury w zasobniku Szybkich narzędzi poniżej edytora, aby zwiększyć komfort kodowania.", "info-excludefolders": "Użyj wzorca **/node_modules/**, aby zignorować wszystkie pliki z folderu node_modules. Spowoduje to wykluczenie plików z listy, a także uniemożliwi ich uwzględnienie w wyszukiwaniu plików.", "missed files": "Po rozpoczęciu wyszukiwania zeskanowano {count} plików, które nie zostaną uwzględnione w wyszukiwaniu.", "remove": "Usuń", "quicktools:command-palette": "Paleta poleceń", "default file encoding": "Domyślne kodowanie plików", "remove entry": "Czy na pewno chcesz usunąć '{name}' z zapisanych ścieżek? Należy pamiętać, że usunięcie go nie spowoduje usunięcia samej ścieżki.", "delete entry": "Potwierdź usunięcie: '{name}'. Tej akcji nie można cofnąć. Kontynuować?", "change encoding": "Czy ponownie otworzyć '{file}' z kodowaniem '{encoding}'? Ta czynność spowoduje utratę wszelkich niezapisanych zmian dokonanych w pliku. Czy chcesz kontynuować ponowne otwieranie?", "reopen file": "Czy na pewno chcesz ponownie otworzyć '{file}'? Wszelkie niezapisane zmiany zostaną utracone.", "plugin min version": "{name} dostępne tylko w Acode - {v-code} i nowszych wersjach. Kliknij tutaj, aby zaktualizować.", "color preview": "Kolor podglądu", "confirm": "Potwierdź", "list files": "Lista wszystkich plików w {name}? Zbyt wiele plików może spowodować awarię aplikacji.", "problems": "Problemy", "show side buttons": "Pokaż przyciski boczne", "bug_report": "Prześlij raport o błędzie", "verified publisher": "Zweryfikowany wydawca", "most_downloaded": "Najczęściej pobierane", "newly_added": "Ostatnio dodane", "top_rated": "Najwyżej oceniane", "rename not supported": "Zmiana nazwy katalogu w termux nie jest obsługiwana", "compress": "Kompresja", "copy uri": "Kopiuj Uri", "delete entries": "Czy na pewno chcesz usunąć {count} elementów?", "deleting items": "Usuwanie {count} elementów...", "import project zip": "Importuj projekt (zip)", "changelog": "Dziennik zmian", "notifications": "Powiadomienia", "no_unread_notifications": "Brak nieodczytanych powiadomień", "should_use_current_file_for_preview": "Należy użyć bieżącego pliku do podglądu zamiast domyślnego (index.html)", "fade fold widgets": "Widżety Fade Fold", "quicktools:home-key": "Klawisz Home", "quicktools:end-key": "Klawisz End", "quicktools:pageup-key": "Klawisz PageUp", "quicktools:pagedown-key": "Klawisz PageDown", "quicktools:delete-key": "Klawisz Delete", "quicktools:tilde": "Wstaw symbol tyldy", "quicktools:backtick": "Wstaw backtick", "quicktools:hash": "Wstaw symbol hash", "quicktools:dollar": "Wstaw symbol dolara", "quicktools:modulo": "Wstaw moduł/symbol procentu", "quicktools:caret": "Wstaw symbol karetki", "plugin_enabled": "Wtyczka włączona", "plugin_disabled": "Wtyczka wyłączona", "enable_plugin": "Włącz tę wtyczkę", "disable_plugin": "Wyłącz tę wtyczkę", "open_source": "Otwarte oprogramowanie", "terminal settings": "Ustawienia terminala", "font ligatures": "Ligatury czcionek", "letter spacing": "Odstępy między literami", "terminal:tab stop width": "Szerokość tabulatora", "terminal:scrollback": "Linie przewijania wstecz", "terminal:cursor blink": "Miganie kursora", "terminal:font weight": "Grubość czcionki", "terminal:cursor inactive style": "Styl nieaktywnego kursora", "terminal:cursor style": "Styl kursora", "terminal:font family": "Rodzina czcionek", "terminal:convert eol": "Konwersja EOL", "terminal:confirm tab close": "Potwierdź zamknięcie karty terminala", "terminal:image support": "Obsługa obrazów", "terminal": "Terminal", "allFileAccess": "Dostęp do wszystkich plików", "fonts": "Czcionki", "sponsor": "Sponsor", "downloads": "pobrania", "reviews": "recenzje", "overview": "Zestawienie", "contributors": "Wspierający", "quicktools:hyphen": "Wstaw symbol myślnika", "check for app updates": "Sprawdź dostępność aktualizacji", "prompt update check consent message": "Acode może sprawdzać dostępność aktualizacji aplikacji, gdy jesteś online. Włączyć sprawdzanie aktualizacji?", "keywords": "Słowa kluczowe", "author": "Autor", "filtered by": "Filtrowane wg", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/pt-br.json ================================================ { "lang": "Português (Brasil)", "about": "Sobre", "active files": "Arquivos ativos", "alert": "Alerta", "app theme": "Tema do app", "autocorrect": "Habilitar autocorreção?", "autosave": "Salvamento automático", "cancel": "Cancelar", "change language": "Mudar idioma", "choose color": "Escolher cor", "clear": "Limpar", "close app": "Fechar a aplicação?", "commit message": "Mensagem de commit", "console": "Console", "conflict error": "Conflito! Por favor aguarde antes de commitar.", "copy": "Copiar", "create folder error": "Desculpe, não foi possível criar a nova pasta", "cut": "Cortar", "delete": "Deletar", "dependencies": "Dependências", "delay": "Tempo em milissegundos", "editor settings": "Configurações do editor", "editor theme": "Tema do editor", "enter file name": "Informar nome do arquivo", "enter folder name": "Informar nome da pasta", "empty folder message": "Pasta vazia", "enter line number": "Informar número da linha", "error": "Erro", "failed": "Falhou", "file already exists": "Arquivo já existente", "file already exists force": "Arquivo já existente. Sobrescrever?", "file changed": " foi alterado, recarregar o arquivo?", "file deleted": "Arquivo deletado", "file is not supported": "Arquivo não suportado", "file not supported": "Este tipo de arquivo não é suportado.", "file too large": "O arquivo é muito grande para manipular. O tamanho máximo permitido por arquivo é {size}", "file renamed": "Arquivo renomeado", "file saved": "Arquivo salvo", "folder added": "Pasta adicionada", "folder already added": "A pasta já foi adicionada", "font size": "Tamanho da fonte", "goto": "Ir para a linha", "icons definition": "Definição de ícones", "info": "Info", "invalid value": "Valor inválido", "language changed": "O idioma foi alterado com sucesso", "linting": "Verificar o erro de sintaxe", "logout": "Sair", "loading": "Carregando", "my profile": "Meu perfil", "new file": "Novo arquivo", "new folder": "Nova pasta", "no": "Não", "no editor message": "Abra ou crie um novo arquivo e pasta no menu", "not set": "Não configurado", "unsaved files close app": "Existem arquivos não salvos. Fechar aplicação?", "notice": "Note", "open file": "Abrir arquivo", "open files and folders": "Abrir arquivos e pastas", "open folder": "Abrir pasta", "open recent": "Abrir recentes", "ok": "Ok", "overwrite": "Sobrescrever", "paste": "Colar", "preview mode": "Modo de pré-vizualização", "read only file": "Não é possível salvar o arquivo, somente leitura. Por favor, tente salvar como", "reload": "Recarregar", "rename": "Renomear", "replace": "Substituir", "required": "Este campo é obrigatório", "run your web app": "Executar seu web app", "save": "Salvar", "saving": "Salvando", "save as": "Salvar como", "save file to run": "Favor salvar este arquivo para executar no navegador", "search": "Pesquisar", "see logs and errors": "Ver logs e erros", "select folder": "Selecionar a pasta", "settings": "Configurações", "settings saved": "Configurações salvas", "show line numbers": "Mostrar números de linha", "show hidden files": "Mostrar arquivos ocultos", "show spaces": "Mostrar espaços", "soft tab": "Usar espaços em vez de tabs?", "sort by name": "Classificar por nome", "success": "Sucesso", "tab size": "Tamanho do tab", "text wrap": "Quebra de texto", "theme": "Tema", "unable to delete file": "não foi possível excluir o arquivo", "unable to open file": "Desculpe, não foi possível abrir o arquivo", "unable to open folder": "Desculpe, não foi possível abrir a pasta", "unable to save file": "Desculpe, não foi possível salvar o arquivo", "unable to rename": "Desculpe, não foi possível renomear", "unsaved file": "Este arquivo não foi salvo, fechar mesmo assim?", "warning": "Aviso", "use emmet": "usar emmet", "use quick tools": "Usar ferramentas rápidas", "yes": "Sim", "encoding": "Codificação de texto", "syntax highlighting": "Realce de sintaxe", "read only": "Somente leitura", "select all": "Selecionar tudo", "select branch": "Selecionar branch", "create new branch": "Criar nova branch", "use branch": "Usar branch", "new branch": "Nova branch", "branch": "Branch", "key bindings": "Combinações de teclas", "edit": "Editar", "reset": "Resetar", "color": "Cor", "select word": "Selecionar palavra", "quick tools": "ferramentas rápidas", "select": "Selecionar", "editor font": "Fonte do editor", "new project": "Novo projeto", "format": "Formatar", "project name": "Nome do Projeto", "unsupported device": "Seu dispositivo não oferece suporte ao tema.", "vibrate on tap": "Vibrar ao tocar", "copy command is not supported by ftp.": "O comando de cópia não é suportado pelo FTP.", "support title": "Acode suporte", "fullscreen": "Tela cheia", "animation": "Animação", "backup": "Backup", "restore": "Restaurar", "backup successful": "Backup bem-sucedido", "invalid backup file": "Arquivo de backup inválido", "add path": "Adicionar caminho", "live autocompletion": "Observar Preenchimento automático", "file properties": "Propriedades do arquivo", "path": "Caminho", "type": "Tipo", "word count": "Contagem de palavras", "line count": "Contagem de linhas", "last modified": "Última modificação", "size": "Tamanho", "share": "Compartilhar", "show print margin": "Mostrar margem de impressão", "login": "Conecte-se", "scrollbar size": "Tamanho da barra de rolagem", "cursor controller size": "Tamanho do controlador do cursor", "none": "Nenhum", "small": "Pequeno", "large": "Grande", "floating button": "Botão flutuante", "confirm on exit": "Confirmar saída", "show console": "Mostrar console", "image": "Imagem", "insert file": "Inserir arquivo", "insert color": "Inserir cor", "powersave mode warning": "Desative o modo de economia de energia para visualizar no navegador externo.", "exit": "Sair", "custom": "Personalizado", "reset warning": "Tem certeza de que deseja redefinir o tema?", "theme type": "Tipo de tema", "light": "Claro", "dark": "Escuro", "file browser": "Navegador de arquivos", "operation not permitted": "Operação não permitida", "no such file or directory": "O arquivo ou diretório não existe", "input/output error": "Entrada/Saída de erros", "permission denied": "Permissão negada", "bad address": "Caminho incorreto", "file exists": "O arquivo existe", "not a directory": "Não é um diretório", "is a directory": "É um diretório", "invalid argument": "Argumento inválido", "too many open files in system": "Muitos arquivos abertos no sistema", "too many open files": "Muitos arquivos abertos", "text file busy": "Arquivo de texto ocupado", "no space left on device": "Não há mais espaço no dispositivo", "read-only file system": "Sistema de arquivos somente leitura", "file name too long": "Nome de arquivo muito longo", "too many users": "Muitos usuários", "connection timed out": "A conexão expirou", "connection refused": "Conexão recusada", "owner died": "Dono morreu", "an error occurred": "Um erro ocorreu", "add ftp": "Adicionar FTP", "add sftp": "Adicionar SFTP", "save file": "Salvar Arquivo", "save file as": "Salvar arquivo como", "files": "Arquivos", "help": "Ajuda", "file has been deleted": "{file} foi deletado!", "feature not available": "Este recurso está disponível apenas na versão paga do aplicativo.", "deleted file": "Arquivo deletado", "line height": "Altura da linha", "preview info": "Se você deseja executar o arquivo ativo, toque e segure no ícone de execução", "manage all files": "Permita que o editor Acode gerencie todos os arquivos nas configurações para editar arquivos em seu dispositivo facilmente.", "close file": "Fechar arquivo", "reset connections": "Redefinir conexões", "check file changes": "Verificar alterações do arquivo", "open in browser": "Abrir no navegador", "desktop mode": "Modo desktop", "toggle console": "Alternar console", "new line mode": "Modo de nova linha", "add a storage": "Adicionar um armazenamento", "rate acode": "Avaliar Acode", "support": "Suporte", "downloading file": "Baixando {file}", "downloading...": "Baixando...", "folder name": "Nome da pasta", "keyboard mode": "Modo de teclado", "normal": "Normal", "app settings": "Configurações do aplicativo", "disable in-app-browser caching": "Desativar o cache do navegador no aplicativo", "copied to clipboard": "Copiado para a área de transferência", "remember opened files": "Manter arquivos abertos", "remember opened folders": "Manter pastas abertas", "no suggestions": "Nenhuma sugestão", "no suggestions aggressive": "Sem sugestões agressivas", "install": "Instalar", "installing": "Instalando...", "plugins": "Plugins", "recently used": "Usado recentemente", "update": "Atualizar", "uninstall": "Desinstalar", "download acode pro": "Baixar Acode pro", "loading plugins": "Carregando plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Cabeçalho", "sidebar": "Barra lateral", "inapp": "No app", "browser": "Navegador", "diagonal scrolling": "Rolagem diagonal", "reverse scrolling": "Rolagem reversa", "formatter": "Formatador", "format on save": "Formatar ao salvar", "remove ads": "Remover propagandas", "fast": "Rápido", "slow": "Lento", "scroll settings": "Configurações de rolagem", "scroll speed": "Velocidade de rolamento", "loading...": "Carregando...", "no plugins found": "Nenhum plugin encontrado", "name": "Nome", "username": "Nome de usuário", "optional": "Opcional", "hostname": "Nome do host", "password": "Senha", "security type": "Tipo de segurança", "connection mode": "Modo de conexão", "port": "Porta", "key file": "Arquivo chave", "select key file": "Selecione o arquivo de chave", "passphrase": "Palavra-chave", "connecting...": "Conectando...", "type filename": "Digite o nome do arquivo", "unable to load files": "Não foi possível carregar os arquivos", "preview port": "Porta de pré-visualização", "find file": "Achar arquivo", "system": "Sistema", "please select a formatter": "Favor selecionar um formatador", "case sensitive": "Case sensitive", "regular expression": "Expressão regular", "whole word": "Palavra inteira", "edit with": "Editar com", "open with": "Abrir com", "no app found to handle this file": "Nenhum aplicativo encontrado para manipular este arquivo", "restore default settings": "Restaurar a configuração original", "server port": "Porta do servidor", "preview settings": "Configurações da pré-vizualização", "preview settings note": "Se a porta de pré-visualização e a porta do servidor forem diferentes, o aplicativo não iniciará o servidor e, em vez disso, abrirá https://: no navegador ou no navegador do aplicativo. Isso é útil quando você está executando um servidor.", "backup/restore note": "Ele fará backup apenas de suas configurações, tema personalizado e atalhos de teclado. Ele não fará backup do seu FTP/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Tentar FTP/SFTP novamente quando falhar", "more": "Mais", "thank you :)": "Obrigado :)", "purchase pending": "compra pendente", "cancelled": "cancelado", "local": "Local", "remote": "Remoto", "show console toggler": "Mostrar alternador de console", "binary file": "Este arquivo contém dados binários, deseja abri-lo?", "relative line numbers": "Números de linha relativos", "elastic tabstops": "Paradas elásticas", "line based rtl switching": "Comutação RTL baseada em linha", "hard wrap": "Quebra rígida", "spellcheck": "Verificação ortográfica", "wrap method": "Método de quebra", "use textarea for ime": "Usar textarea para IME", "invalid plugin": "Plugin inválido", "type command": "Digite o comando", "plugin": "Plugin", "quicktools trigger mode": "Modo de disparo de ferramentas rápidas", "print margin": "Margem de impressão", "touch move threshold": "Limite de movimento de toque", "info-retryremotefsafterfail": "Tentar novamente a conexão FTP/SFTP quando falhar.", "info-fullscreen": "Ocultar barra de título na tela inicial.", "info-checkfiles": "Verificar alterações nos arquivos quando o aplicativo estiver em segundo plano.", "info-console": "Escolha o console JavaScript. Legacy é o console padrão, eruda é um console de terceiros.", "info-keyboardmode": "Modo de teclado para entrada de texto, sem sugestões ocultará sugestões e corrigirá automaticamente. Se nenhuma sugestão não funcionar, tente alterar o valor para nenhuma sugestão agressiva.", "info-rememberfiles": "Lembrar dos arquivos abertos quando o aplicativo for fechado.", "info-rememberfolders": "Lembrar de pastas abertas quando o aplicativo for fechado.", "info-floatingbutton": "Mostrar ou ocultar o botão flutuante de ferramentas rápidas.", "info-openfilelistpos": "Onde mostrar a lista de arquivos ativos.", "info-touchmovethreshold": "Se a sensibilidade ao toque do seu dispositivo for muito alta, você pode aumentar esse valor para evitar movimentos acidentais do toque.", "info-scroll-settings": "Essas configurações contêm configurações de rolagem, incluindo quebra automática de texto.", "info-animation": "Se o aplicativo parecer lento, desative a animação.", "info-quicktoolstriggermode": "Se o botão nas ferramentas rápidas não estiver funcionando, tente alterar este valor.", "info-checkForAppUpdates": "Verificar atualizações do aplicativo automaticamente.", "info-quickTools": "Mostrar ou ocultar as ferramentas rápidas.", "info-showHiddenFiles": "Mostrar arquivos e pastas ocultas. (Eles começam com um .)", "info-all_file_access": "Permitir o acesso de /sdcard e /storage no terminal.", "info-fontSize": "O tamanho da fonte usado para exibir o texto.", "info-fontFamily": "A família de fontes usada para renderizar o texto.", "info-theme": "O tema de cores do terminal.", "info-cursorStyle": "O estilo do cursor quando o terminal está em foco.", "info-cursorInactiveStyle": "O estilo do cursor quando o terminal não está em foco.", "info-fontWeight": "A espessura da fonte usada para renderizar texto sem negrito.", "info-cursorBlink": "Define se o cursor pisca.", "info-scrollback": "A quantidade de rolagem exibida no terminal. A rolagem representa a quantidade de linhas que são mantidas quando as linhas são roladas além da área visível inicial.", "info-tabStopWidth": "O tamanho das tabs (tabulações) no terminal.", "info-letterSpacing": "O espaçamento em pixels inteiros entre os caracteres.", "info-imageSupport": "Define se as imagens são suportadas no terminal.", "info-fontLigatures": "Define se as ligaduras de fontes estão ativadas no terminal.", "info-confirmTabClose": "Solicitar confirmação antes de fechar as abas do terminal.", "info-backup": "Cria um backup da instalação do terminal.", "info-restore": "Restaura um backup da instalação do terminal.", "info-uninstall": "Desinstala a instalação do terminal.", "owned": "Meu", "api_error": "Servidor API desativado, por favor, tente depois de algum tempo.", "installed": "Instalado", "all": "Tudo", "medium": "Médio", "refund": "Reembolso", "product not available": "Produto não disponível", "no-product-info": "Este produto não está disponível em seu país no momento, tente novamente mais tarde.", "close": "Fechar", "explore": "Explorar", "key bindings updated": "Atalhos de teclas atualizados", "search in files": "Pesquisar em arquivos", "exclude files": "Excluir arquivos", "include files": "Incluir arquivos", "search result": "{matches} resultados em {files} arquivos.", "invalid regex": "Expressão regular inválida: {message}.", "bottom": "Em baixo", "save all": "Salvar tudo", "close all": "Fechar tudo", "unsaved files warning": "Alguns arquivos não são estão salvos. Clique em 'ok' e selecione o que fazer ou pressione 'cancelar' para voltar.", "save all warning": "Tem certeza de que deseja salvar todos os arquivos e fechar? Esta ação não pode ser revertida.", "save all changes warning": "Tem certeza de que deseja salvar todos os arquivos?", "close all warning": "Tem certeza de que deseja fechar todos os arquivos? Você perderá as alterações não salvas e esta ação não pode ser revertida.", "refresh": "Atualizar", "shortcut buttons": "Botões de atalho", "no result": "Sem resultado", "searching...": "Procurando...", "quicktools:ctrl-key": "Tecla de CTRL/comando", "quicktools:tab-key": "Tecla de tab", "quicktools:shift-key": "Tacla de shift", "quicktools:undo": "Desfazer", "quicktools:redo": "Refazer", "quicktools:search": "Pesquisar no arquivo", "quicktools:save": "Salvar Arquivo", "quicktools:esc-key": "Tecla de escape", "quicktools:curlybracket": "Inserir chaves", "quicktools:squarebracket": "Inserir colchetes", "quicktools:parentheses": "Inserir parênteses", "quicktools:anglebracket": "Inserir menor que/maior que", "quicktools:left-arrow-key": "Tecla de seta para a esquerda", "quicktools:right-arrow-key": "Tecla de seta para a direita", "quicktools:up-arrow-key": "Tecla de seta para cima", "quicktools:down-arrow-key": "Tecla de seta para baixo", "quicktools:moveline-up": "Mover linha para cima", "quicktools:moveline-down": "Mover linha para baixo", "quicktools:copyline-up": "Copiar alinhamento", "quicktools:copyline-down": "Copiar linha para baixo", "quicktools:semicolon": "Inserir ponto-e-vírgula", "quicktools:quotation": "Inserir citação", "quicktools:and": "Inserir símbolo de e comercial (&)", "quicktools:bar": "Inserir símbolo de barra", "quicktools:equal": "Inserir símbolo de igual", "quicktools:slash": "Inserir símbolo de barra", "quicktools:exclamation": "Inserir exclamação", "quicktools:alt-key": "Tecla Alt", "quicktools:meta-key": "Tecla Windows/Meta", "info-quicktoolssettings": "Personalize os botões de atalho e as teclas do teclado no contêiner ferramentas rápidas abaixo do editor para aprimorar sua experiência de codificação.", "info-excludefolders": "Use o padrão **/node_modules/** para ignorar todos os arquivos da pasta node_modules. Isso excluirá os arquivos da lista e também impedirá que sejam incluídos nas pesquisas de arquivos.", "missed files": "{count} arquivos verificados após o início da pesquisa e não serão incluídos na pesquisa.", "remove": "Remover", "quicktools:command-palette": "Paleta de comandos", "default file encoding": "Codificação de arquivo padrão", "remove entry": "Tem certeza de que deseja remover '{name}' dos caminhos salvos? Observe que removê-lo não excluirá o caminho em si.", "delete entry": "Confirme a exclusão: '{name}'. Essa ação não pode ser desfeita. Continuar?", "change encoding": "Reabrir '{file}' com codificação '{encoding}'? Esta ação resultará na perda de quaisquer alterações não salvas feitas no arquivo. Deseja prosseguir com a reabertura?", "reopen file": "Tem certeza de que deseja reabrir '{file}'? Quaisquer alterações não salvas serão perdidas.", "plugin min version": "{name} disponível apenas no Acode - {v-code} e superior. Clique aqui para atualizar.", "color preview": "Pré-vizualização de cores", "confirm": "Confirmar", "list files": "Listar todos os arquivos em {name}? Muitos arquivos podem travar o aplicativo.", "problems": "Problemas", "show side buttons": "Mostrar botões laterais", "bug_report": "Enviar Relatório de Erro", "verified publisher": "Editor Verificado", "most_downloaded": "Mais Baixados", "newly_added": "Recém-Adicionados", "top_rated": "Mais Bem Avaliados", "rename not supported": "Renomear no diretório do Termux não é suportado", "compress": "Comprimir", "copy uri": "Copiar URI", "delete entries": "Tem certeza de que deseja excluir {count} itens?", "deleting items": "Excluindo {count} itens...", "import project zip": "Importar Projeto (zip)", "changelog": "Registro de Alterações", "notifications": "Notificações", "no_unread_notifications": "Sem notificações não lidas", "should_use_current_file_for_preview": "Deve usar o arquivo atual para pré-visualização em vez do padrão (index.html)", "fade fold widgets": "Widgets Fade Fold", "quicktools:home-key": "Tecla Home", "quicktools:end-key": "Tecla End", "quicktools:pageup-key": "Tecla PageUp", "quicktools:pagedown-key": "Tecla PageDown", "quicktools:delete-key": "Tecla Delete", "quicktools:tilde": "Inserir símbolo de til (~)", "quicktools:backtick": "Inserir crase (`)", "quicktools:hash": "Inserir símbolo de cerquilha (#)", "quicktools:dollar": "Inserir símbolo de dólar ($)", "quicktools:modulo": "Inserir símbolo de módulo/porcentagem (%)", "quicktools:caret": "Inserir símbolo de circunflexo (^)", "plugin_enabled": "Plugin ativado", "plugin_disabled": "Plugin desativado", "enable_plugin": "Ativar este Plugin", "disable_plugin": "Desativar este Plugin", "open_source": "Código Aberto", "terminal settings": "Configurações do Terminal", "font ligatures": "Ligaduras da Fonte", "letter spacing": "Espaçamento entre Letras", "terminal:tab stop width": "Largura do Tab Stop", "terminal:scrollback": "Linhas de Scrollback", "terminal:cursor blink": "Piscar do Cursor", "terminal:font weight": "Peso da Fonte", "terminal:cursor inactive style": "Estilo do Cursor Inativo", "terminal:cursor style": "Estilo do Cursor", "terminal:font family": "Família da Fonte", "terminal:convert eol": "Converter EOL", "terminal:confirm tab close": "Confirme o fechamento da aba do terminal", "terminal:image support": "Suportar imagens", "terminal": "Terminal", "allFileAccess": "Acesso total aos arquivos", "fonts": "Fontes", "sponsor": "Patrocinador", "downloads": "Downloads", "reviews": "Avaliações", "overview": "Visão Geral", "contributors": "Contribuidores", "quicktools:hyphen": "Inserir símbolo de hífen (-)", "check for app updates": "Verifique se há atualizações do aplicativo", "prompt update check consent message": "O Acode pode verificar se há novas atualizações quando você estiver online. Deseja ativar a verificação de atualizações?", "keywords": "Palavras-chave", "author": "Autor", "filtered by": "Filtrado por", "clean install state": "Estado de instalação limpa", "backup created": "Backup criado", "restore completed": "Restauração concluída", "restore will include": "Isso irá restaurar", "restore warning": "Esta ação não pode ser desfeita. Continuar?", "reload to apply": "Recarregar para aplicar as alterações?", "reload app": "Recarregar aplicativo", "preparing backup": "Preparando backup", "collecting settings": "Coletando configurações", "collecting key bindings": "Coletando atalhos de teclado", "collecting plugins": "Coletando informações dos plugins", "creating backup": "Criando arquivo de backup", "validating backup": "Validando backup", "restoring key bindings": "Restaurando atalhos de teclado", "restoring plugins": "Restaurando plugins", "restoring settings": "Restaurando configurações", "legacy backup warning": "Este é um formato de backup antigo. Alguns recursos podem ser limitados.", "checksum mismatch": "Checksum não corresponde — o arquivo de backup pode ter sido modificado ou corrompido.", "plugin not found": "Plugin não encontrado no registro", "paid plugin skipped": "Plugin pago — compra não encontrada", "source not found": "O arquivo de origem não existe mais", "restored": "Restaurado", "skipped": "Ignorado", "backup not valid object": "O arquivo de backup não é um objeto válido", "backup no data": "O arquivo de backup não contém dados para restaurar", "backup legacy warning": "Este é um formato de backup antigo (v1). Alguns recursos podem ser limitados.", "backup missing metadata": "Metadados do backup ausentes — algumas informações podem não estar disponíveis", "backup checksum mismatch": "Checksum não corresponde — o arquivo de backup pode ter sido modificado ou corrompido. Prossiga com cautela.", "backup checksum verify failed": "Não foi possível verificar o checksum", "backup invalid settings": "Formato de configurações inválido", "backup invalid keybindings": "Formato de atalhos de teclado inválido", "backup invalid plugins": "Formato de plugins instalados inválido", "issues found": "Problemas encontrados", "error details": "Detalhes do erro", "active tools": "Ferramentas ativas", "available tools": "Ferramentas disponíveis", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/pu-in.json ================================================ { "lang": "Punjabi (ਪੰਜਾਬੀ) by NiceSapien", "about": "Acode ਬਾਰੇ", "active files": "ਸਰਗਰਮ ਫਾਇਲ", "alert": "ਚੇਤਾਵਨੀ", "app theme": "ਐਪ ਥੀਮ", "autocorrect": "ਕੀ ਸਵੈ-ਸੁਧਾਰ ਚਾਲੂ ਕਰਨਾ ਹੈ?", "autosave": "ਆਟੋ ਸੇਵ", "cancel": "ਰੱਦ", "change language": "ਭਾਸ਼ਾ ਬਦਲੋ", "choose color": "ਰੰਗ ਚੁਣੋ", "clear": "ਸਾਫ਼", "close app": "ਐਪਲੀਕੇਸ਼ਨ ਨੂੰ ਬੰਦ ਕਰਨਾ ਹੈ?", "commit message": "ਸੁਨੇਹਾ ਵਚਨਬੱਧ ਕਰੋ", "console": "ਕੰਸੋਲ", "conflict error": "ਟਕਰਾਅ! ਕਿਰਪਾ ਕਰਕੇ ਇੱਕ ਹੋਰ ਵਚਨਬੱਧਤਾ ਤੋਂ ਪਹਿਲਾਂ ਉਡੀਕ ਕਰੋ।", "copy": "ਕਾਪੀ", "create folder error": "ਮਾਫ਼ ਕਰਨਾ, ਨਵਾਂ ਫੋਲਡਰ ਬਣਾਉਣ ਵਿੱਚ ਅਸਮਰੱਥ", "cut": "ਕੱਟੋ", "delete": "", "dependencies": "ਨਿਰਭਰਤਾਵਾਂ", "delay": "ਮਿਲੀਸਕਿੰਟ ਵਿੱਚ ਸਮਾਂ", "editor settings": "ਸੰਪਾਦਕ ਸੈਟਿੰਗਾਂ", "editor theme": "ਸੰਪਾਦਕ ਥੀਮ", "enter file name": "ਫਾਈਲ ਦਾ ਨਾਮ ਦਰਜ ਕਰੋ", "enter folder name": "ਫੋਲਡਰ ਦਾ ਨਾਮ ਦਰਜ ਕਰੋ", "empty folder message": "ਖਾਲੀ ਫੋਲਡਰ", "enter line number": "ਲਾਈਨ ਨੰਬਰ ਦਰਜ ਕਰੋ", "error": "ਗਲਤੀ", "failed": "ਅਸਫਲ", "file already exists": "ਫ਼ਾਈਲ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ", "file already exists force": "ਫ਼ਾਈਲ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਓਵਰਰਾਈਟ ਕਰਨਾ ਹੈ?", "file changed": " ਬਦਲਿਆ ਗਿਆ ਹੈ, ਫਾਈਲ ਰੀਲੋਡ ਕਰੋ?", "file deleted": "ਫਾਇਲ ਨੂੰ ਹਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ", "file is not supported": "ਫਾਈਲ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ", "file not supported": "ਇਹ ਫਾਈਲ ਕਿਸਮ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।", "file too large": "ਹੈਂਡਲ ਕਰਨ ਲਈ ਫ਼ਾਈਲ ਬਹੁਤ ਵੱਡੀ ਹੈ। ਅਧਿਕਤਮ ਫਾਈਲ ਦਾ ਆਕਾਰ {size} ਹੈ", "file renamed": "ਫਾਈਲ ਦਾ ਨਾਮ ਬਦਲ ਦਿੱਤਾ ਗਿਆ ਹੈ", "file saved": "ਫਾਇਲ ਨੂੰ ਸੰਭਾਲਿਆ ਗਿਆ ਹੈ", "folder added": "ਫੋਲਡਰ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ", "folder already added": "folder already added", "font size": "ਫੌਂਟ ਦਾ ਆਕਾਰ", "goto": "ਲਾਈਨ 'ਤੇ ਜਾਓ", "icons definition": "ਆਈਕਾਨ ਦੀ ਪਰਿਭਾਸ਼ਾ", "info": "ਜਾਣਕਾਰੀ", "invalid value": "ਅਵੈਧ ਮੁੱਲ", "language changed": "ਭਾਸ਼ਾ ਨੂੰ ਸਫਲਤਾਪੂਰਵਕ ਬਦਲ ਦਿੱਤਾ ਗਿਆ ਹੈ", "linting": "ਸੰਟੈਕਸ ਗਲਤੀ ਦੀ ਜਾਂਚ ਕਰੋ", "logout": "ਲਾੱਗ ਆਊਟ", "loading": "ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ", "my profile": "ਮੇਰੀ ਪ੍ਰੋਫਾਈਲ", "new file": "ਨਵੀਂ ਫ਼ਾਈਲ", "new folder": "ਨਵਾਂ ਫੋਲਡਰ", "no": "ਨੰ", "no editor message": "menu ਤੋਂ ਨਵੀਂ ਫਾਈਲ ਅਤੇ ਫੋਲਡਰ ਖੋਲ੍ਹੋ ਜਾਂ ਬਣਾਓ", "not set": "ਸੈੱਟ ਨਹੀਂ ਹੈ", "unsaved files close app": "ਅਣ-ਰੱਖਿਅਤ ਫਾਈਲਾਂ ਹਨ। ਐਪਲੀਕੇਸ਼ਨ ਨੂੰ ਬੰਦ ਕਰਨਾ ਹੈ?", "notice": "ਨੋਟਿਸ", "open file": "ਫਾਇਲ ਖੋਲੋ", "open files and folders": "ਫਾਈਲਾਂ ਅਤੇ ਫੋਲਡਰ ਖੋਲ੍ਹੋ", "open folder": "ਫੋਲਡਰ ਖੋਲ੍ਹੋ", "open recent": "ਹਾਲੀਆ ਖੋਲ੍ਹੋ", "ok": "ਠੀਕ ਹੈ", "overwrite": "ਓਵਰਰਾਈਟ", "paste": "ਚਿਪਕਾਓ", "preview mode": "ਪੂਰਵਦਰਸ਼ਨ ਮੋਡ", "read only file": "ਸਿਰਫ਼ ਪੜ੍ਹਨ ਵਾਲੀ ਫ਼ਾਈਲ ਨੂੰ ਸੁਰੱਖਿਅਤ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ। ਕਿਰਪਾ ਕਰਕੇ ਇਸ ਤੌਰ 'ਤੇ ਸੁਰੱਖਿਅਤ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ", "reload": "ਦੁਬਾਰਾ ਲੋਡ ਕਰੋ", "rename": "ਨਾਮ ਬਦਲੋ", "replace": "ਬਦਲੋ", "required": "ਇਸ ਫੀਲਡ ਦੀ ਲੋੜ ਹੈ", "run your web app": "ਆਪਣੀ ਵੈੱਬ ਐਪ ਚਲਾਓ", "save": "ਸੇਵ", "saving": "saving", "save as": "ਬਤੌਰ ਮਹਿਫ਼ੂਜ਼ ਕਰੋ", "save file to run": "ਕਿਰਪਾ ਕਰਕੇ ਇਸ ਫ਼ਾਈਲ ਨੂੰ ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ ਚਲਾਉਣ ਲਈ ਸੇਵ ਕਰੋ", "search": "ਖੋਜ", "see logs and errors": "ਲੌਗ ਅਤੇ ਤਰੁੱਟੀਆਂ ਦੇਖੋ", "select folder": "ਫੋਲਡਰ ਚੁਣੋ", "settings": "ਸੈਟਿੰਗਾਂ", "settings saved": "ਸੈਟਿੰਗਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀਆਂ ਗਈਆਂ", "show line numbers": "ਲਾਈਨ ਨੰਬਰ ਦਿਖਾਓ", "show hidden files": "ਲੁਕੀਆਂ ਹੋਈਆਂ ਫਾਈਲਾਂ ਦਿਖਾਓ", "show spaces": "ਸਪੇਸ ਦਿਖਾਓ", "soft tab": "ਸਾਫਟ ਟੈਬ", "sort by name": "ਨਾਮ ਦੁਆਰਾ ਛਾਂਟੋ", "success": "ਸਫਲਤਾ", "tab size": "ਟੈਬ ਦਾ ਆਕਾਰ", "text wrap": "ਟੈਕਸਟ ਰੈਪ", "theme": "ਥੀਮ", "unable to delete file": "ਫਾਈਲ ਨੂੰ ਮਿਟਾਉਣ ਵਿੱਚ ਅਸਮਰੱਥ", "unable to open file": "ਮਾਫ਼ ਕਰਨਾ, ਫ਼ਾਈਲ ਖੋਲ੍ਹਣ ਵਿੱਚ ਅਸਮਰੱਥ", "unable to open folder": "ਮਾਫ਼ ਕਰਨਾ, ਫੋਲਡਰ ਖੋਲ੍ਹਣ ਵਿੱਚ ਅਸਮਰੱਥ", "unable to save file": "ਮਾਫ਼ ਕਰਨਾ, ਫ਼ਾਈਲ ਨੂੰ ਸੁਰੱਖਿਅਤ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ", "unable to rename": "ਮਾਫ਼ ਕਰਨਾ, ਨਾਮ ਬਦਲਣ ਵਿੱਚ ਅਸਮਰੱਥ", "unsaved file": "ਇਹ ਫਾਈਲ ਸੁਰੱਖਿਅਤ ਨਹੀਂ ਹੈ, ਫਿਰ ਵੀ ਬੰਦ ਕਰਨਾ ਹੈ?", "warning": "ਚੇਤਾਵਨੀ", "use emmet": "Emmet ਦੀ ਵਰਤੋਂ ਕਰੋ", "use quick tools": "ਤੇਜ਼ ਸਾਧਨਾਂ ਦੀ ਵਰਤੋਂ ਕਰੋ", "yes": "ਹਾਂ", "encoding": "ਟੈਕਸਟ ਇੰਕੋਡਿੰਗ", "syntax highlighting": "ਸਿੰਟੈਕਸ ਹਾਈਲਾਈਟਿੰਗ", "read only": "ਸਿਰਫ ਪੜ੍ਹਨ ਲਈ", "select all": "ਸਾਰਿਆ ਨੂੰ ਚੁਣੋ", "select branch": "ਸ਼ਾਖਾ ਚੁਣੋ", "create new branch": "ਨਵੀਂ ਸ਼ਾਖਾ ਬਣਾਓ", "use branch": "ਸ਼ਾਖਾ ਦੀ ਵਰਤੋਂ ਕਰੋ", "new branch": "ਨਵੀਂ ਸ਼ਾਖਾ", "branch": "branch", "key bindings": "ਕੁੰਜੀ ਬੰਧਨ", "edit": "ਸੰਪਾਦਿਤ ਕਰੋ", "reset": "ਰੀਸੈਟ", "color": "ਰੰਗ", "select word": "ਸ਼ਬਦ ਚੁਣੋ", "quick tools": "ਤੇਜ਼ ਟੂਲ", "select": "ਚੁਣੋ", "editor font": "ਸੰਪਾਦਕ ਫੌਂਟ", "new project": "ਨਵਾਂ ਪ੍ਰੋਜੈਕਟ", "format": "ਫਾਰਮੈਟ", "project name": "ਪ੍ਰੋਜੈਕਟ ਦਾ ਨਾਮ", "unsupported device": "ਤੁਹਾਡੀ ਡਿਵਾਈਸ ਥੀਮ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੀ ਹੈ।", "vibrate on tap": "ਟੈਪ 'ਤੇ ਵਾਈਬ੍ਰੇਟ ਕਰੋ", "copy command is not supported by ftp.": "ਕਾਪੀ ਕਮਾਂਡ FTP ਦੁਆਰਾ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।", "support title": "Acode ਦਾ ਸਮਰਥਨ ਕਰੋ", "fullscreen": "ਪੂਰਾ ਸਕਰੀਨ", "animation": "ਐਨੀਮੇਸ਼ਨ", "backup": "ਬੈਕਅੱਪ", "restore": "ਬਹਾਲ", "backup successful": "ਬੈਕਅੱਪ ਸਫਲ", "invalid backup file": "ਅਵੈਧ ਬੈਕਅੱਪ ਫ਼ਾਈਲ", "add path": "ਫੋਲਡਰ ਸ਼ਾਮਲ ਕਰੋ", "live autocompletion": "ਲਾਈਵ ਸਵੈ-ਸੰਪੂਰਨਤਾ", "file properties": "ਫਾਈਲ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ", "path": "ਮਾਰਗ", "type": "ਟਾਈਪ", "word count": "ਸ਼ਬਦ ਗਿਣਤੀ", "line count": "ਲਾਈਨਾਂ ਦੀ ਗਿਣਤੀ", "last modified": "ਪਿਛਲੀ ਵਾਰ ਸੋਧਿਆ ਗਿਆ", "size": "ਆਕਾਰ", "share": "ਸ਼ੇਅਰ", "show print margin": "ਪ੍ਰਿੰਟ ਮਾਰਜਿਨ ਦਿਖਾਓ", "login": "ਲਾਗਿਨ", "scrollbar size": "ਸਕ੍ਰੋਲਬਾਰ ਦਾ ਆਕਾਰ", "cursor controller size": "ਕਰਸਰ ਕੰਟਰੋਲਰ ਦਾ ਆਕਾਰ", "none": "ਕੋਈ ਨਹੀਂ", "small": "ਛੋਟਾ", "large": "ਵੱਡਾ", "floating button": "ਫਲੋਟਿੰਗ ਬਟਨ", "confirm on exit": "ਬੰਦ ਹੋਣ ਦੀ ਪੁਸ਼ਟੀ ਕਰੋ", "show console": "ਕੰਸੋਲ ਦਿਖਾਓ", "image": "ਚਿੱਤਰ", "insert file": "ਫਾਈਲ ਸ਼ਾਮਲ ਕਰੋ", "insert color": "ਰੰਗ ਪਾਓ", "powersave mode warning": "ਬਾਹਰੀ ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ ਪ੍ਰੀਵਿਊ ਕਰਨ ਲਈ ਪਾਵਰ ਸੇਵਿੰਗ ਮੋਡ ਨੂੰ ਬੰਦ ਕਰੋ।", "exit": "ਨਿਕਾਸ", "custom": "ਪ੍ਰਥਾ", "reset warning": "ਕੀ ਤੁਸੀਂ ਯਕੀਨੀ ਤੌਰ 'ਤੇ ਥੀਮ ਨੂੰ ਰੀਸੈਟ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?", "theme type": "ਥੀਮ ਦੀ ਕਿਸਮ", "light": "ਰੋਸ਼ਨੀ", "dark": "ਹਨੇਰ", "file browser": "ਫਾਈਲ ਬ੍ਰਾਊਜ਼ਰ", "operation not permitted": "ਓਪਰੇਸ਼ਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ", "no such file or directory": "ਅਜਿਹੀ ਕੋਈ ਫਾਇਲ ਜਾਂ ਨਿਰਦੇਸ਼ਿਕਾ ਨਹੀਂ", "input/output error": "ਇਨਪੁਟ/ਆਊਟਪੁੱਟ ਗਲਤੀ", "permission denied": "ਆਗਿਆ ਤੋਂ ਇਨਕਾਰ", "bad address": "ਮਾੜਾ ਪਤਾ", "file exists": "ਫਾਈਲ ਮੌਜੂਦ ਹੈ", "not a directory": "ਡਾਇਰੈਕਟਰੀ ਨਹੀਂ", "is a directory": "ਇੱਕ ਡਾਇਰੈਕਟਰੀ ਹੈ", "invalid argument": "ਅਵੈਧ ਦਲੀਲ", "too many open files in system": "ਸਿਸਟਮ ਵਿੱਚ ਬਹੁਤ ਸਾਰੀਆਂ ਖੁੱਲ੍ਹੀਆਂ ਫਾਈਲਾਂ", "too many open files": "ਬਹੁਤ ਸਾਰੀਆਂ ਖੁੱਲ੍ਹੀਆਂ ਫ਼ਾਈਲਾਂ", "text file busy": "ਟੈਕਸਟ ਫਾਈਲ ਵਿਅਸਤ ਹੈ", "no space left on device": "ਡਿਵਾਈਸ 'ਤੇ ਕੋਈ ਥਾਂ ਨਹੀਂ ਬਚੀ ਹੈ", "read-only file system": "ਸਿਰਫ਼-ਪੜ੍ਹਨ ਲਈ ਫਾਈਲ ਸਿਸਟਮ", "file name too long": "ਫ਼ਾਈਲ ਦਾ ਨਾਮ ਬਹੁਤ ਲੰਮਾ ਹੈ", "too many users": "ਬਹੁਤ ਸਾਰੇ ਉਪਭੋਗਤਾ", "connection timed out": "ਕਨੈਕਸ਼ਨ ਦਾ ਸਮਾਂ ਸਮਾਪਤ ਹੋਇਆ", "connection refused": "ਸੰਬੰਧ ਠੁਕਰਾਉਣਾ", "owner died": "ਮਾਲਕ ਦੀ ਮੌਤ ਹੋ ਗਈ", "an error occurred": "ਇੱਕ ਗਲਤੀ ਆਈ ਹੈ", "add ftp": "FTP ਸ਼ਾਮਲ ਕਰੋ", "add sftp": "SFTP ਸ਼ਾਮਲ ਕਰੋ", "save file": "ਫਾਈਲ ਸੇਵ ਕਰੋ", "save file as": "ਫਾਈਲ ਨੂੰ ਇਸ ਤਰ੍ਹਾਂ ਸੇਵ ਕਰੋ", "files": "ਫਾਈਲਾਂ", "help": "ਮਦਦ", "file has been deleted": "{file} ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ!", "feature not available": "ਇਹ ਵਿਸ਼ੇਸ਼ਤਾ ਐਪ ਦੇ ਭੁਗਤਾਨ ਕੀਤੇ ਸੰਸਕਰਣ ਵਿੱਚ ਹੀ ਉਪਲਬਧ ਹੈ।", "deleted file": "ਮਿਟਾਈ ਗਈ ਫਾਈਲ", "line height": "ਲਾਈਨ ਦੀ ਉਚਾਈ", "preview info": "ਜੇਕਰ ਤੁਸੀਂ ਐਕਟਿਵ ਫਾਈਲ ਨੂੰ ਚਲਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ, ਤਾਂ ਪਲੇ ਆਈਕਨ 'ਤੇ ਟੈਪ ਕਰੋ ਅਤੇ ਹੋਲਡ ਕਰੋ।", "manage all files": "Acode ਸੰਪਾਦਕ ਨੂੰ ਤੁਹਾਡੀ ਡਿਵਾਈਸ 'ਤੇ ਆਸਾਨੀ ਨਾਲ ਫਾਈਲਾਂ ਨੂੰ ਸੰਪਾਦਿਤ ਕਰਨ ਲਈ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਸਾਰੀਆਂ ਫਾਈਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰਨ ਦਿਓ", "close file": "ਫਾਈਲ ਬੰਦ ਕਰੋ", "reset connections": "ਕਨੈਕਸ਼ਨ ਰੀਸੈਟ ਕਰੋ", "check file changes": "ਫਾਈਲ ਤਬਦੀਲੀਆਂ ਦੀ ਜਾਂਚ ਕਰੋ", "open in browser": "ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ ਖੋਲ੍ਹੋ", "desktop mode": "ਡੈਸਕਟਾਪ ਮੋਡ", "toggle console": "ਕੰਸੋਲ ਟੌਗਲ ਕਰੋ", "new line mode": "ਨਵਾਂ ਲਾਈਨ ਮੋਡ", "add a storage": "ਸਟੋਰੇਜ ਸ਼ਾਮਲ ਕਰੋ", "rate acode": "Acode ਨੂੰ ਰੇਟ ਕਰੋ (NiceSapien ਦੁਆਰਾ ਪੰਜਾਬੀ ਅਨੁਵਾਦ)", "support": "ਸਪੋਰਟ", "downloading file": "ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ {file}", "downloading...": "ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...", "folder name": "ਫੋਲਡਰ ਦਾ ਨਾਮ", "keyboard mode": "ਕੀਬੋਰਡ ਮੋਡ", "normal": "ਸਧਾਰਣ", "app settings": "ਐਪ ਸੈਟਿੰਗਾਂ", "disable in-app-browser caching": "ਇਨ-ਐਪ-ਬ੍ਰਾਊਜ਼ਰ ਕੈਚਿੰਗ ਨੂੰ ਅਸਮਰੱਥ ਬਣਾਓ", "copied to clipboard": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ", "remember opened files": "ਖੋਲ੍ਹੀਆਂ ਫਾਈਲਾਂ ਨੂੰ ਯਾਦ ਰੱਖੋ", "remember opened folders": "ਖੋਲ੍ਹੇ ਫੋਲਡਰਾਂ ਨੂੰ ਯਾਦ ਰੱਖੋ", "no suggestions": "ਕੋਈ ਸੁਝਾਅ ਨਹੀਂ", "no suggestions aggressive": "ਕੋਈ ਸੁਝਾਅ ਹਮਲਾਵਰ ਨਹੀਂ ਹਨ", "install": "ਇੰਸਟਾਲ ਕਰੋ", "installing": "ਸਥਾਪਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...", "plugins": "ਪਲੱਗਇਨ", "recently used": "ਹਾਲ ਹੀ ਵਿੱਚ ਵਰਤਿਆ", "update": "ਅੱਪਡੇਟ ਕਰੋ", "uninstall": "ਅਣਇੰਸਟੌਲ ਕਰੋ", "download acode pro": "Acode ਪ੍ਰੋ ਨੂੰ ਡਾਊਨਲੋਡ ਕਰੋ", "loading plugins": "ਪਲੱਗਇਨ ਲੋਡ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ", "faqs": "ਅਕਸਰ ਪੁੱਛੇ ਜਾਂਦੇ ਸਵਾਲ", "feedback": "ਸੁਝਾਅ", "header": "ਸਿਰਲੇਖ", "sidebar": "ਸਾਈਡਬਾਰ", "inapp": "ਇਨਐਪ", "browser": "ਬ੍ਰਾਊਜ਼ਰ", "diagonal scrolling": "ਡਾਇਗਨਲ ਸਕ੍ਰੋਲਿੰਗ", "reverse scrolling": "ਰਿਵਰਸ ਸਕ੍ਰੋਲਿੰਗ", "formatter": "ਫਾਰਮੈਟਰ", "format on save": "ਸੇਵ 'ਤੇ ਫਾਰਮੈਟ", "remove ads": "ਵਿਗਿਆਪਨ ਹਟਾਓ", "fast": "ਤੇਜ਼", "slow": "ਹੌਲੀ", "scroll settings": "ਸਕ੍ਰੋਲ ਸੈਟਿੰਗਾਂ", "scroll speed": "ਸਕ੍ਰੌਲ ਸਪੀਡ", "loading...": "ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...", "no plugins found": "ਕੋਈ ਪਲੱਗਇਨ ਨਹੀਂ ਮਿਲੇ", "name": "ਨਾਮ", "username": "ਯੂਜ਼ਰਨੇਮ", "optional": "ਵਿਕਲਪਿਕ", "hostname": "ਹੋਸਟਨਾਮ", "password": "ਪਾਸਵਰਡ", "security type": "ਸੁਰੱਖਿਆ ਦੀ ਕਿਸਮ", "connection mode": "ਕਨੈਕਸ਼ਨ ਮੋਡ", "port": "ਪੋਰਟ", "key file": "ਕੁੰਜੀ ਫਾਈਲ", "select key file": "ਕੁੰਜੀ ਫਾਈਲ ਚੁਣੋ", "passphrase": "ਪਾਸਫਰੇਜ", "connecting...": "ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ...", "type filename": "ਫਾਈਲ ਨਾਮ ਟਾਈਪ ਕਰੋ", "unable to load files": "ਫਾਈਲਾਂ ਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ", "preview port": "ਝਲਕ ਪੋਰਟ", "find file": "ਫਾਈਲ ਲੱਭੋ", "system": "ਸਿਸਟਮ", "please select a formatter": "ਕਿਰਪਾ ਕਰਕੇ ਇੱਕ ਫਾਰਮੈਟਰ ਚੁਣੋ", "case sensitive": "ਕੇਸ ਸੰਸੇਟਿਵ", "regular expression": "ਨਿਯਮਤ ਸਮੀਕਰਨ", "whole word": "ਪੂਰਾ ਸ਼ਬਦ", "edit with": "ਨਾਲ ਸੰਪਾਦਿਤ ਕਰੋ", "open with": "ਨਾਲ ਖੋਲ੍ਹੋ", "no app found to handle this file": "ਇਸ ਫ਼ਾਈਲ ਨੂੰ ਸੰਭਾਲਣ ਲਈ ਕੋਈ ਐਪ ਨਹੀਂ ਮਿਲੀ", "restore default settings": "ਡਿਫੌਲਟ ਸੈਟਿੰਗਾਂ ਨੂੰ ਰੀਸਟੋਰ ਕਰੋ", "server port": "ਸਰਵਰ ਪੋਰਟ", "preview settings": "ਪੂਰਵਦਰਸ਼ਨ ਸੈਟਿੰਗਾਂ", "preview settings note": "ਜੇਕਰ ਪ੍ਰੀਵਿਊ ਪੋਰਟ ਅਤੇ ਸਰਵਰ ਪੋਰਟ ਵੱਖ-ਵੱਖ ਹਨ, ਤਾਂ ਐਪ ਸਰਵਰ ਨੂੰ ਚਾਲੂ ਨਹੀਂ ਕਰੇਗਾ ਅਤੇ ਇਸ ਦੀ ਬਜਾਏ ਬ੍ਰਾਊਜ਼ਰ ਜਾਂ ਇਨ-ਐਪ ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ https://: ਖੋਲ੍ਹੇਗਾ। ਇਹ ਉਦੋਂ ਲਾਭਦਾਇਕ ਹੁੰਦਾ ਹੈ ਜਦੋਂ ਤੁਸੀਂ ਕਿਤੇ ਹੋਰ ਸਰਵਰ ਚਲਾ ਰਹੇ ਹੋ।", "backup/restore note": "ਇਹ ਸਿਰਫ਼ ਤੁਹਾਡੀਆਂ ਸੈਟਿੰਗਾਂ, ਕਸਟਮ ਥੀਮ ਅਤੇ ਕੁੰਜੀ ਬਾਈਡਿੰਗ ਦਾ ਬੈਕਅੱਪ ਲਵੇਗਾ। ਇਹ ਤੁਹਾਡੇ FTP/SFTP, GitHub ਪ੍ਰੋਫਾਈਲਾਂ ਦਾ ਬੈਕਅੱਪ ਨਹੀਂ ਲਵੇਗਾ।", "host": "ਮੇਜ਼ਬਾਨ", "retry ftp/sftp when fail": "ਅਸਫਲ ਹੋਣ 'ਤੇ ftp/sftp ਦੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ", "more": "ਹੋਰ", "thank you :)": "ਤੁਹਾਡਾ ਧੰਨਵਾਦ :)", "purchase pending": "ਖਰੀਦ ਬਕਾਇਆ", "cancelled": "ਰੱਦ ਕਰ ਦਿੱਤਾ", "local": "ਸਥਾਨਕ", "remote": "ਰਿਮੋਟ", "show console toggler": "ਕੰਸੋਲ ਟੌਗਲਰ ਦਿਖਾਓ", "binary file": "ਇਸ ਫ਼ਾਈਲ ਵਿੱਚ ਬਾਈਨਰੀ ਡਾਟਾ ਹੈ, ਕੀ ਤੁਸੀਂ ਇਸਨੂੰ ਖੋਲ੍ਹਣਾ ਚਾਹੁੰਦੇ ਹੋ?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "ਸਪਾਂਸਰ", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/ru-ru.json ================================================ { "lang": "Русский", "about": "О приложении", "active files": "Открытые файлы", "alert": "Предупреждение", "app theme": "Тема приложения", "autocorrect": "Автозамена", "autosave": "Автосохранение", "cancel": "Отмена", "change language": "Изменить язык", "choose color": "Выбрать цвет", "clear": "Очистить", "close app": "Закрыть приложение?", "commit message": "Сообщение к коммиту", "console": "Консоль", "conflict error": "Конфликт! Пожалуйста подождите, прежде чем оставлять коммит", "copy": "Копировать", "create folder error": "Не удалось создать папку", "cut": "Вырезать", "delete": "Удалить", "dependencies": "Зависимости", "delay": "Задержка в миллисекундах", "editor settings": "Настройки редактора", "editor theme": "Тема", "enter file name": "Введите имя файла", "enter folder name": "Введите имя папки", "empty folder message": "Пустая папка", "enter line number": "Введите номер строки", "error": "Ошибка", "failed": "Провал", "file already exists": "Файл уже существует", "file already exists force": "Файл уже существует. Перезаписать?", "file changed": " был изменён, перезагрузить его?", "file deleted": "Файл удалён", "file is not supported": "Файл не поддерживается", "file not supported": "Данный тип файла не поддерживается.", "file too large": "Файл слишком большой. Допустимый размер: {size}", "file renamed": "Файл переименован", "file saved": "Файл сохранен", "folder added": "Папка добавлена", "folder already added": "Папка уже добавлена", "font size": "Размер шрифта", "goto": "Перейти к строке", "icons definition": "Определение значков", "info": "Информация", "invalid value": "Неверное значение", "language changed": "Язык успешно изменен", "linting": "Проверять синтаксические ошибки", "logout": "Выйти", "loading": "Загрузка", "my profile": "Мой профиль", "new file": "Новый файл", "new folder": "Новая папка", "no": "Нет", "no editor message": "Откройте или создайте новый файл", "not set": "Не задано", "unsaved files close app": "Имеются несохранённые файлы. Всё равно выйти?", "notice": "Уведомление", "open file": "Открыть файл", "open files and folders": "Открытые файлы и папки", "open folder": "Открыть папку", "open recent": "Открыть недавние", "ok": "OК", "overwrite": "Перезаписать", "paste": "Вставить", "preview mode": "Режим предпросмотра", "read only file": "Файл открыт в режиме «Только для чтения»", "reload": "Перезагрузить", "rename": "Переименовать", "replace": "Заменить", "required": "Это поле обязательно к заполнению", "run your web app": "Запустить своё веб-приложение", "save": "Сохранить", "saving": "Сохранение", "save as": "Сохранить как", "save file to run": "Сохраните файл для запуска в браузере", "search": "Поиск", "see logs and errors": "Показать логи и ошибки", "select folder": "Выбрать папку", "settings": "Настройки", "settings saved": "Настройки сохранены", "show line numbers": "Показывать нумерацию строк", "show hidden files": "Показывать скрытые файлы", "show spaces": "Показывать отступы", "soft tab": "Использовать пробелы вместо Tab", "sort by name": "Сортировать по имени", "success": "Успешно", "tab size": "Количество пробелов в табе", "text wrap": "Перенос строк", "theme": "Тема", "unable to delete file": "Невозможно удалить файл", "unable to open file": "Невозможно открыть файл", "unable to open folder": "Невозможно открыть папку", "unable to save file": "Невозможно сохранить файл", "unable to rename": "Невозможно переименовать", "unsaved file": "Файл не сохранён, закрыть?", "warning": "Предупреждение", "use emmet": "Использовать Emmet", "use quick tools": "Использовать быстрые инструменты", "yes": "Да", "encoding": "Кодировка", "syntax highlighting": "Подсветка синтаксиса", "read only": "Только для чтения", "select all": "Выбрать всё", "select branch": "Выберите ветку", "create new branch": "Создать новую ветку", "use branch": "Использовать ветку", "new branch": "Новая ветка", "branch": "Ветка", "key bindings": "Сочетания клавиш", "edit": "Редактировать", "reset": "Сброс", "color": "Цвет", "select word": "Выбрать слово", "quick tools": "Быстрые инструменты", "select": "Выбрать", "editor font": "Шрифт", "new project": "Новый проект", "format": "Форматировать", "project name": "Название проекта", "unsupported device": "На этом устройстве тема не поддерживается", "vibrate on tap": "Вибрация при нажатии", "copy command is not supported by ftp.": "Копирование пока не поддерживается в режиме FTP", "support title": "Поддержи Acode", "fullscreen": "Полноэкранный режим", "animation": "Анимация", "backup": "Резервное копирование", "restore": "Восстановление", "backup successful": "Бэкап создан", "invalid backup file": "Некорректный файл бэкапа", "add path": "Добавить путь", "live autocompletion": "Мгновенное автозавершение", "file properties": "Свойство файла", "path": "Путь", "type": "Тип", "word count": "Количество слов", "line count": "Количество строк", "last modified": "Последнее изменение", "size": "Размер", "share": "Поделиться", "show print margin": "Показать поле печати", "login": "Вход", "scrollbar size": "Размер полосы прокрутки", "cursor controller size": "Размер контроллера курсора", "none": "Выкл.", "small": "Маленький", "large": "Большой", "floating button": "Плавающая кнопка", "confirm on exit": "Подтверждение перед выходом", "show console": "Показать консоль", "image": "Изображение", "insert file": "Вставить файл", "insert color": "Вставить цвет", "powersave mode warning": "Отключите режим энергосбережения для предварительного просмотра во внешнем браузере", "exit": "Выйти", "custom": "Пользовательский", "reset warning": "Вы уверены, что хотите сбросить тему?", "theme type": "Тип темы", "light": "Светлая", "dark": "Тёмная", "file browser": "Файловый менеджер", "operation not permitted": "Операция не разрешена", "no such file or directory": "Данный файл или каталог отсутствует", "input/output error": "Ошибка ввода-вывода", "permission denied": "Доступ запрещён", "bad address": "Неверный адрес", "file exists": "Файл существует", "not a directory": "Это не каталог", "is a directory": "Это каталог", "invalid argument": "Недопустимый аргумент", "too many open files in system": "Слишком много открытых файлов в системе", "too many open files": "Слишком много открытых файлов", "text file busy": "Текстовый файл занят", "no space left on device": "На устройстве не осталось свободного места", "read-only file system": "Файловая система, доступная только для чтения", "file name too long": "Название файла слишком длинное", "too many users": "Слишком много пользователей", "connection timed out": "Время соединения истекло", "connection refused": "Отказано в соединении", "owner died": "Владелец умер", "an error occurred": "Произошла ошибка", "add ftp": "Добавить FTP", "add sftp": "Добавить SFTP", "save file": "Сохранить файл", "save file as": "Сохранить как", "files": "Файлы", "help": "Помощь", "file has been deleted": "{file} удалён!", "feature not available": "Эта функция доступна только в платной версии приложения.", "deleted file": "Удалить файл", "line height": "Высота строки", "preview info": "Если вы хотите запустить активный файл, нажмите и удерживайте значок воспроизведения", "manage all files": "Дайте Acode разрешение на доступ ко всем файлам для их редактирования в настройках", "close file": "Закрыть файл", "reset connections": "Сбросить соединения", "check file changes": "Проверять изменения файлов", "open in browser": "Открыть в браузере", "desktop mode": "Режим ПК", "toggle console": "Переключить консоль", "new line mode": "Перенос строки", "add a storage": "Добавить хранилище", "rate acode": "Оценить Acode", "support": "Поддержка", "downloading file": "Загрузка {file}", "downloading...": "Загрузка...", "folder name": "Имя папки", "keyboard mode": "Режим клавиатуры", "normal": "По умолчанию", "app settings": "Настройки приложения", "disable in-app-browser caching": "Отключить кэширование во встроенном браузере", "copied to clipboard": "Скопировано в буфер обмена", "remember opened files": "Запоминать открытые файлы", "remember opened folders": "Запоминать открытые папки", "no suggestions": "Откл. подсказок", "no suggestions aggressive": "Агрессивное откл. подсказок", "install": "Установить", "installing": "Установка...", "plugins": "Плагины", "recently used": "Недавно использованные", "update": "Обновить", "uninstall": "Удалить", "download acode pro": "Скачать Acode Pro", "loading plugins": "Загрузка плагинов", "faqs": "ЧаВо", "feedback": "Обратная связь", "header": "Сверху", "sidebar": "Сбоку", "inapp": "Встроенный браузер", "browser": "Внешний браузер", "diagonal scrolling": "Диагональный скроллинг", "reverse scrolling": "Обратный скроллинг", "formatter": "Форматтер", "format on save": "Форматирование при сохранении", "remove ads": "Удалить рекламу", "fast": "Быстрая", "slow": "Медленная", "scroll settings": "Настройки скроллинга", "scroll speed": "Скорость скроллинга", "loading...": "Загрузка...", "no plugins found": "Плагины не найдены", "name": "Название", "username": "Имя пользователя", "optional": "необязательно", "hostname": "Имя хоста", "password": "Пароль", "security type": "Тип защиты", "connection mode": "Режим подключения", "port": "Порт", "key file": "Ключ", "select key file": "Выбрать файл ключа", "passphrase": "Пасс-фраза", "connecting...": "Подключение...", "type filename": "Введите имя файла", "unable to load files": "Не удалось загрузить файлы", "preview port": "Порт предпросмотра", "find file": "Поиск", "system": "Как в системе", "please select a formatter": "Выберите форматтер", "case sensitive": "Учитывать регистр", "regular expression": "Регулярное выражение", "whole word": "Целые слова", "edit with": "Редактировать в...", "open with": "Открыть в...", "no app found to handle this file": "Не найдено ни одного приложения для открытия этого типа файлов", "restore default settings": "Восстановить настройки по умолчанию", "server port": "Порт сервера", "preview settings": "Настройки предпросмотра", "preview settings note": "Если порт предпросмотра и порт сервера отличаются, приложение не запустит встроенный сервер, а только откроет https://<хост>:<порт> в браузере. Полезно при использовании сервера на другом устройстве.", "backup/restore note": "Будут сохранены только настройки, темы и сочетания клавиш. FTP/SFTP-сервера и профили GitHub не резервируются.", "host": "Хост", "retry ftp/sftp when fail": "Повторять попытку подключения к FTP/SFTP при ошибке", "more": "Ещё", "thank you :)": "Спасибо :)", "purchase pending": "Ожидание оплаты", "cancelled": "Отменено", "local": "Локальный", "remote": "Удалённый", "show console toggler": "Кнопка переключения консоли", "binary file": "Файл содержит бинарные данные, открыть?", "relative line numbers": "Относительный номер строки", "elastic tabstops": "Эластичная табуляция (автовыравнивание табов)", "line based rtl switching": "Переключение RTL на основе содержимого строки", "hard wrap": "Разрыв строки в месте переноса", "spellcheck": "Проверка правописания", "wrap method": "Способ переноса строк", "use textarea for ime": "Использовать область ввода для IME", "invalid plugin": "Некорректный файл плагина", "type command": "Введите команду", "plugin": "Плагин", "quicktools trigger mode": "Режим обработки нажатий в быстрых инструментах", "print margin": "Размер поля печати", "touch move threshold": "Чувствительность сенсора при перемещении", "info-retryremotefsafterfail": "Повторять попытку подключения к FTP/SFTP при ошибке", "info-fullscreen": "Скрыть заголовок на главном экране", "info-checkfiles": "Проверять изменения файлов в фоновом режиме", "info-console": "Выбор консоли JavaScript: Legacy — стандартная, Eruda — сторонняя с поддержкой пользовательских скриптов", "info-keyboardmode": "Режим клавиатуры для ввода текста: \"Откл. подсказок\" отключит подсказки и автоисправление. Если этот режим не сработает, попробуйте \"Принудительное откл. подсказок\"", "info-rememberfiles": "Запоминать открытые файлы после выхода из приложения", "info-rememberfolders": "Запоминать открытые папки после выхода из приложения", "info-floatingbutton": "Показать или скрыть плавающую кнопку быстрых инструментов", "info-openfilelistpos": "Где показывать список открытых файлов", "info-touchmovethreshold": "Если на Вашем устройстве слишком чувствительный сенсорный экран, увеличьте значение для предотвращения случайных перемещений курсора/полосы прокрутки", "info-scroll-settings": "Этот пункт содержит настройки скролла и переноса строк", "info-animation": "Отключите анимацию при лагах и зависаниях в приложении", "info-quicktoolstriggermode": "Попробуйте сменить значение этой настройки если не работают кнопки быстрых инструментов", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Свои", "api_error": "Сервер API недоступен, повторите попытку позже", "installed": "Установленные", "all": "Все", "medium": "Средний", "refund": "Возврат", "product not available": "Плагин недоступен", "no-product-info": "Плагин недоступен в Вашем регионе на данный момент, попробуйте позже", "close": "Закрыть", "explore": "Поиск плагинов", "key bindings updated": "Сочетания клавиш обновлены", "search in files": "Найти в файлах", "exclude files": "Исключить файлы", "include files": "Включить файлы", "search result": "{matches} совпадений обнаружено в {files} файлах.", "invalid regex": "Некорректное регулярное выражение: {message}.", "bottom": "Снизу", "save all": "Сохранить всё", "close all": "Закрыть всё", "unsaved files warning": "Некоторые файлы не сохранены. Нажмите 'OK' чтобы продолжить или 'Отмена' для возвращения.", "save all warning": "Вы действительно хотите сохранить и закрыть всё?", "save all changes warning": "Вы действительно хотите сохранить все файлы?", "close all warning": "Вы действительно хотите закрыть всё? Все ваши несохранённые файлы или изменения не сохранятся", "refresh": "Обновить", "shortcut buttons": "Иконки быстрого доступа", "no result": "Нет результатов", "searching...": "Поиск...", "quicktools:ctrl-key": "Control/Command клавиша", "quicktools:tab-key": "Клав. Tab", "quicktools:shift-key": "Клав. Shift", "quicktools:undo": "Отмена", "quicktools:redo": "Повтор", "quicktools:search": "Поиск в файле", "quicktools:save": "Сохранить файл", "quicktools:esc-key": "Клав. Escape", "quicktools:curlybracket": "Вставить фигурную скобку", "quicktools:squarebracket": "Вставить квадратную скобку", "quicktools:parentheses": "Вставить круглые скобки", "quicktools:anglebracket": "Вставить угловую скобку", "quicktools:left-arrow-key": "Клав. стрелка влево", "quicktools:right-arrow-key": "Клав. стрелка вправо", "quicktools:up-arrow-key": "Клав. стрелка вверх", "quicktools:down-arrow-key": "Клав. стрелка вниз ", "quicktools:moveline-up": "Перенос на линию выше", "quicktools:moveline-down": "Перенос на линию ниже", "quicktools:copyline-up": "Копировать линию выше", "quicktools:copyline-down": "Копировать линию ниже", "quicktools:semicolon": "Вставить точку с запятой", "quicktools:quotation": "Вставить цитату", "quicktools:and": "Вставка и символ", "quicktools:bar": "Вставка символа полосы", "quicktools:equal": "Вставка эквивалентного символа", "quicktools:slash": "Вставка слэш-символа", "quicktools:exclamation": "Вставить восклицательный знак", "quicktools:alt-key": "Клав. Alt", "quicktools:meta-key": "Клав. Windows/Meta", "info-quicktoolssettings": "Настройка кнопок на панели быстрых инструментов", "info-excludefolders": "Используйте шаблон **/node_modules/**, чтобы игнорировать все файлы из папки node_modules. Это исключит файлы из списка, а также предотвратит их включение в поиске файлов.", "missed files": "Найдено {count} файлов, они не будут учитываться.", "remove": "Удалить", "quicktools:command-palette": "Палитра команд", "default file encoding": "Кодировка по умолчанию", "remove entry": "Вы уверены, что хотите убрать '{name}' из сохранённых путей? Сама папка удалена не будет.", "delete entry": "Удалить '{name}'? Действие отменить невозможно.", "change encoding": "Переоткрыть '{file}' с кодировкой '{encoding}'? Вы потеряете все несохранённые изменения.", "reopen file": "Переоткрыть '{file}'? Вы потеряете все несохранённые изменения.", "plugin min version": "{name} доступен только для Acode версии {v-code} и выше. Нажмите для обновления.", "color preview": "Предпросмотр цвета", "confirm": "Подтверждение", "list files": "В папке {name} содержится очень много файлов. Это может повлиять на работу приложения.", "problems": "Проблемы", "show side buttons": "Показать кнопки на стороне", "bug_report": "Сообщить о багах", "verified publisher": "Проверенный издатель", "most_downloaded": "Популярные", "newly_added": "Новые", "top_rated": "С высокой оценкой", "rename not supported": "Переименование в директории Termux не поддерживается", "compress": "Сжать", "copy uri": "Копировать URI", "delete entries": "Вы уверены, что хотите удалить {count} элементов?", "deleting items": "Удаление {count} элементов...", "import project zip": "Импортировать проект (zip)", "changelog": "Список изменений", "notifications": "Уведомления", "no_unread_notifications": "Нет непрочитанных уведомлений", "should_use_current_file_for_preview": "Использовать текущий файл для предпросмотра вместо файла по умолчанию (index.html)", "fade fold widgets": "Затухание виджетов свёртывания", "quicktools:home-key": "Клав. Home", "quicktools:end-key": "Клав. End", "quicktools:pageup-key": "Клав. PageUp", "quicktools:pagedown-key": "Клав. PageDown", "quicktools:delete-key": "Клав. Delete", "quicktools:tilde": "Вставить тильду", "quicktools:backtick": "Вставить обратный апостроф", "quicktools:hash": "Вставить символ #", "quicktools:dollar": "Вставить символ доллара", "quicktools:modulo": "Вставить символ процента", "quicktools:caret": "Вставить символ каретки", "plugin_enabled": "Плагин включен", "plugin_disabled": "Плагин отключен", "enable_plugin": "Включить этот плагин", "disable_plugin": "Отключить этот плагин", "open_source": "Открытый исходный код", "terminal settings": "Настройки терминала", "font ligatures": "Шрифтовые лигатуры", "letter spacing": "Межбуквенный интервал", "terminal:tab stop width": "Ширина табуляции", "terminal:scrollback": "Строк прокрутки", "terminal:cursor blink": "Мигание курсора", "terminal:font weight": "Насыщенность шрифта", "terminal:cursor inactive style": "Стиль неактивного курсора", "terminal:cursor style": "Стиль курсора", "terminal:font family": "Семейство шрифтов", "terminal:convert eol": "Преобразовывать EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Терминал", "allFileAccess": "Доступ ко всем файлам", "fonts": "Шрифты", "sponsor": "Спонсор", "downloads": "Загрузки", "reviews": "Отзывы", "overview": "Обзор", "contributors": "Авторы", "quicktools:hyphen": "Вставить символ дефиса", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/tl-ph.json ================================================ { "lang": "Tagalog", "about": "Kaalaman", "active files": "Active files", "alert": "Alert", "app theme": "Tema ng App", "autocorrect": "I-enable ang autocorrect", "autosave": "Autosave", "cancel": "Cancel", "change language": "Baguhin ang Wika", "choose color": "Pumili ng Kulay", "clear": "Burahin", "close app": "I-close ang application?", "commit message": "Commit message", "console": "Console", "conflict error": "Conflict! Hintayin bago mag-commit ulit.", "copy": "I-copy", "create folder error": "Pasensya, hindi makalikha ng bagong folder", "cut": "I-cut", "delete": "I-delete", "dependencies": "Mga dependency", "delay": "Oras sa millisecond", "editor settings": "Mga setting ng editor", "editor theme": "Tema ng Editor", "enter file name": "Maglagay ng pangalan ng file", "enter folder name": "Maglagay ng pangalan ng folder", "empty folder message": "Walang laman na folder", "enter line number": "Maglagay ng bilang ng linya", "error": "Error", "failed": "Nabigo", "file already exists": "Mayroon ng file na ganito", "file already exists force": "Mayroon ng file na ganito. I-overwrite?", "file changed": "Nabago ang file, I-reload?", "file deleted": "File deleted", "file is not supported": "Hindi suportado ang file na ito.", "file not supported": "Hindi suportado ang file type na ito.", "file too large": "Masyadong malaki ang file para i-handle. Ang pinakamalaking file size ay {size}", "file renamed": "nai-rename ang file", "file saved": "nai-save ang file", "folder added": "nadagdag ang folder", "folder already added": "nadagdag na ang folder", "font size": "Font size", "goto": "Pumunta sa linya", "icons definition": "Icons definition", "info": "Impormasyon", "invalid value": "Invalid ang value", "language changed": "Matagumpay na nabago ang wika", "linting": "I-check ang syntax error", "logout": "Logout", "loading": "Naglo-load", "my profile": "Aking profile", "new file": "Bagong file", "new folder": "Bagong folder", "no": "Hindi", "no editor message": "Buksan o lumikha ng bagong file at folder mula sa menu", "not set": "Hindi itinakda", "unsaved files close app": "Mayroon pang mga hindi na-isave na files. I-close ang aplikasyon?", "notice": "Paunawa", "open file": "Buksan ang file", "open files and folders": "Buksan ang mga file at folder", "open folder": "Buksan ang folder", "open recent": "Buksan ang kamakailan", "ok": "ok", "overwrite": "I-overwrite", "paste": "I-paste", "preview mode": "I-preview mode", "read only file": "Hindi maaring mag-save sa read-only na file. Subukang i-save bilang", "reload": "I-reload", "rename": "I-rename", "replace": "I-replace", "required": "Kinakailangan ang field na ito", "run your web app": "Patakbuhin ang iyong web app", "save": "I-save", "saving": "Nagse-save", "save as": "I-save bilang", "save file to run": "Mangyaring I-save ang file na ito para magamit sa browser", "search": "Search", "see logs and errors": "Tingnan ang mga log at error", "select folder": "Pumili ng folder", "settings": "Mga setting", "settings saved": "Nai-save ang mga setting", "show line numbers": "Ipakita ang line numbers", "show hidden files": "Ipakita ang hidden files", "show spaces": "Ipakita ang space", "soft tab": "Soft tab", "sort by name": "I-ayos ayon sa pangalan", "success": "Tagumpay", "tab size": "Tab size", "text wrap": "Text wrap / Word wrap", "theme": "Tema", "unable to delete file": "hindi ma-delete ang file", "unable to open file": "Paumanhin, hindi ma-open ang file", "unable to open folder": "Paumanhin, hindi ma-open ang folder", "unable to save file": "Paumanhin, hindi ma-save ang file", "unable to rename": "Paumanhin, hindi maka-rename", "unsaved file": "Hindi nai-save ang file, ipagpatuloy pa rin?", "warning": "Babala", "use emmet": "Gamitin ang emmet", "use quick tools": "Gamitin ang quick tools", "yes": "Oo", "encoding": "Text encoding", "syntax highlighting": "Syntax highlighting", "read only": "Read only", "select all": "I-select lahat", "select branch": "I-select ang branch", "create new branch": "Lumikha ng bagong branch", "use branch": "Gamitin ang branch", "new branch": "Bagong branch", "branch": "Branch", "key bindings": "Key bindings", "edit": "I-edit", "reset": "I-reset", "color": "Color", "select word": "Select word", "quick tools": "Quick tools", "select": "I-select", "editor font": "Editor font", "new project": "Bagong project", "format": "Format", "project name": "Pangalan ng project", "unsupported device": "Hindi suportado ang tema sa inyong device.", "vibrate on tap": "Vibrate on tap", "copy command is not supported by ftp.": "Ang copy command ay hindi suportado ng FTP.", "support title": "Suportahan ang Acode", "fullscreen": "Fullscreen", "animation": "Animation", "backup": "Backup", "restore": "Restore", "backup successful": "Ang backup ay matagumpay", "invalid backup file": "Invalid ang backup file", "add path": "Add path", "live autocompletion": "Live autocompletion", "file properties": "File properties", "path": "Path", "type": "Type", "word count": "Word count", "line count": "Line count", "last modified": "Last modified", "size": "Size", "share": "Share", "show print margin": "Show print margin", "login": "Login", "scrollbar size": "Scrollbar size", "cursor controller size": "Cursor controller size", "none": "None", "small": "Small", "large": "Large", "floating button": "Floating button", "confirm on exit": "I-confirm kapag mag-exit", "show console": "Ipakita ang console", "image": "Image", "insert file": "Insert file", "insert color": "Insert color", "powersave mode warning": "I-turn off ang power saving mode upang ma-preview sa external browser.", "exit": "Exit", "custom": "Custom", "reset warning": "Sigurado ka bang gusto mong i-reset ang tema?", "theme type": "Type ng tema", "light": "Light", "dark": "Dark", "file browser": "File Browser", "operation not permitted": "Hindi pinahihintulutan ang operasyon", "no such file or directory": "Walang ganoong file o directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Hindi directory", "is a directory": "Ito ay isang directory", "invalid argument": "Invalid na argument", "too many open files in system": "Masyadong maraming mga file na nakabukas sa system", "too many open files": "Masyadong maraming mga file na nakabukas", "text file busy": "Text file busy", "no space left on device": "Wala ng natitirang space sa iyong device", "read-only file system": "Read-only file system", "file name too long": "Masyadong mahaba ang file name", "too many users": "Masyadong maraming users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "May error na nangyari", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "I-save ang file", "save file as": "I-save ang file bilang", "files": "Mga file", "help": "Tulong", "file has been deleted": "{file} ay nabura na!", "feature not available": "Ang feature na ito ay available lamang sa paid version ng app.", "deleted file": "Deleted na file", "line height": "Line height", "preview info": "Kung gusto mong i-run ang active file, i-tap at i-hold ang play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "I-close ang file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Buksan sa browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "I-rate ang Acode", "support": "Suportahan", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "Mga settings sa app", "disable in-app-browser caching": "I-disable ang in-app-browser caching", "copied to clipboard": "Kinopya sa clipboard", "remember opened files": "Tandaan ang mga nabuksan na file", "remember opened folders": "Tandaan ang mga nabuksan na folder", "no suggestions": "Walang suggestions", "no suggestions aggressive": "Walang suggestions aggressive", "install": "I-install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Ginamit kamakailan", "update": "I-update", "uninstall": "I-uninstall", "download acode pro": "I-download ang Acode pro", "loading plugins": "Naglo-load ng mga plugin", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Tanggalin ang ads", "fast": "Mabilis", "slow": "Mabgal", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Naglo-load...", "no plugins found": "Walang nakitang mga plugin", "name": "Pangalan", "username": "Username", "optional": "opsyunal", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Hindi ma-load ang mga file", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Mangyaring pumili ng formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "I-edit gamit ang", "open with": "Buksan gamit ang", "no app found to handle this file": "Walang app ang mahanap upang ma-handle ang file na ito.", "restore default settings": "I-restore ang default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "Kung magkaiba ang preview port at server port, hindi magsisimula ang server sa app at sa halip ay magbubukas ito ng https://: sa browser o in-app na browser. Ito ay kapaki-pakinabang kapag nagpapatakbo ka ng isang server sa ibang lugar.", "backup/restore note": "Iba-backup lang nito ang iyong mga setting, custom na tema at key binding. Hindi nito iba-backup ang iyong FTP/SFTP.", "host": "Host", "retry ftp/sftp when fail": "I-retry ang ftp/sftp kapag nabigo", "more": "Higit pa", "thank you :)": "Salamat :)", "purchase pending": "Naka-pending ang pagbili", "cancelled": "kinansela", "local": "Local", "remote": "Remote", "show console toggler": "Ipakita ang console toggler", "binary file": "Ang file na ito ay naglalaman ng binary data, gusto mo ba itong buksan?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Gamitin ang textarea para sa IME", "invalid plugin": "Invalid ang Plugin", "type command": "I-type ang command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "I-retry ang FTP/SFTP connection kapag nabigo.", "info-fullscreen": "Itago ang title bar sa home screen.", "info-checkfiles": "I-check ang mga pagbabago sa file kapag nasa background ang app.", "info-console": "Pumili ng JavaScript console. Ang Legacy ay ang default na console, ang eruda ay isang third party na console.", "info-keyboardmode": "Keyboard mode para sa pag-input ng text, ang 'no suggestions' ay magtatago sa mga suggestions at autocorrect. Kung hindi gumagana ang 'no suggestions', subukang baguhin ang value sa 'no suggestions aggressive'.", "info-rememberfiles": "Tandaan ang mga binuksang file kapag na-close ang app.", "info-rememberfolders": "Tandaan ang mga binuksang folder kapag na-close ang app.", "info-floatingbutton": "Ipakita o itago ang quick tools floating button.", "info-openfilelistpos": "Saan ipapakita ang mga active file list.", "info-touchmovethreshold": "Kung masyadong mataas ang touch sensitivity ng iyong device, maaari mong taasan ang value nito para maiwasan ang touch move.", "info-scroll-settings": "Ang mga setting na ito ay naglalaman ng mga scoll settings kasama ang text wrap.", "info-animation": "Kung ang app ay nakakaranas ng lag, i-disable ang animation.", "info-quicktoolstriggermode": "Kung hindi gumagana ang button sa quick tools, subukang baguhin ang value na ito.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "Ang API server ay down, mangyaring subukan pagkatapos ng ilang oras.", "installed": "Installed", "all": "Lahat", "medium": "Medium", "refund": "Refund", "product not available": "Ang produktong ito ay hindi available", "no-product-info": "Ang produktong ito ay hindi available sa iyong bansa sa ngayon, pakisubukang muli sa ibang pagkakataon.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} resulta sa {files} na files.", "invalid regex": "Invalid ang regular expression: {message}.", "bottom": "Bottom", "save all": "I-save lahat", "close all": "I-close lahat", "unsaved files warning": "Iilan sa mga file ay hindi nai-save. I-click ang 'ok' at piliin kung ano ang gagawin o pindutin ang 'cancel' upang bumalik.", "save all warning": "Sigurado ka bang gusto mong i-save ang lahat ng file at i-close? Ang aksyon na ito ay hindi maaaring baligtarin.", "save all changes warning": "Sigurado ka bang gusto mong i-save ang lahat ng file?", "close all warning": "Sigurado ka bang gusto mong i-close ang lahat ng file? Mawawala sa iyo ang mga hindi nai-save na pagbabago at hindi na maibabalik ang aksyon na ito.", "refresh": "I-refresh", "shortcut buttons": "Shortcut buttons", "no result": "Walang resulta", "searching...": "Naghahanap...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Mag-search sa file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "I-customize ang mga shortcut button at keyboard key sa Quicktools container sa ibaba ng editor upang ma-enhance ang iyong coding experience.", "info-excludefolders": "Gamitin ang pattern **/node_modules/** upang baliwalain ang lahat ng mga file galing sa node_modules folder. Ibubukod nito ang mga file mula sa pagkakalista at pipigilan din ang mga ito na maisama sa file searches.", "missed files": "{count} na file ang nai-scan pagkatapos magsimula ang search at hindi ito isasali sa search.", "remove": "I-remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default na file encoding", "remove entry": "Sigurado ka bang gusto mong burahin ang '{name}' sa mga saved path? Pakitandaan na ang pagtanggal nito ay hindi magbubura sa mismong path.", "delete entry": "Kumpirmahin ang pagbura: '{name}'. Ang aksyon na ito ay hindi maaaring baligtarin. Magpatuloy?", "change encoding": "Muling buksan ang '{file}' gamit ang '{encoding}' encoding? Ang aksyon na ito ay magreresulta sa pagkawala ng anumang hindi nai-save na mga pagbabagong ginawa sa file. Gusto mo bang magpatuloy sa pagbubukas?", "reopen file": "Sigurado ka bang gusto mong muling buksan ang '{file}'? Mawawala ang anumang hindi nai-save na pagbabago.", "plugin min version": "Available lang ang {name} sa Acode - {v-code} at pataas. I-click dito para i-update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "Ilista ang lahat ng file sa {name}? Ang masyadong maraming file ay maaaring magdulot ng pag-crash sa app.", "problems": "Mga Problema", "show side buttons": "Ipakita ang side button", "bug_report": "Mag-submit ng Bug Report", "verified publisher": "Verified na publisher", "most_downloaded": "Pinaka-download", "newly_added": "Bagong idinagdag", "top_rated": "Pinakamataas na rating", "rename not supported": "Ang pag-rename sa termux dir ay hindi suportado", "compress": "I-compress", "copy uri": "I-copy ang Uri", "delete entries": "Sigurado ka bang gusto mong burahin ang {count} na item?", "deleting items": "Binubura ang {count} na item...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Mga notification", "no_unread_notifications": "Walang hindi pa nababasang mga notification", "should_use_current_file_for_preview": "Dapat gamitin ang Current File para sa preview sa halip na default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Sponsor", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/tr-tr.json ================================================ { "lang": "Türkçe (by ibrahim)", "about": "Hakkında", "active files": "Aktif Dosyalar", "alert": "Uyarı", "app theme": "Uygulama Teması", "autocorrect": "Otomatik Düzelt", "autosave": "Otomatik Kaydet", "cancel": "İptal", "change language": "Dili Değiştir", "choose color": "Renk Seç", "clear": "Temizle", "close app": "Uygulama kapatılsın mı?", "commit message": "Commit mesajı", "console": "Konsol", "conflict error": "yanlışlık! Lütfen başka bir committen önce bekleyin", "copy": "Kopyala", "create folder error": "Yeni klasör oluşturulamıyor", "cut": "Kes", "delete": "Sil", "dependencies": "gereklilikler", "delay": "Milisaniye cinsinden süre", "editor settings": "Editör Ayarları", "editor theme": "Editör Teması", "enter file name": "Dosya Adı", "enter folder name": "Klasör Adı", "empty folder message": "Boş Klasör", "enter line number": "Satır numarasını girin", "error": "Hata", "failed": "Başarısız oldu", "file already exists": "Dosya zaten var", "file already exists force": "Dosya zaten var. Üzerine yazılsın mı?", "file changed": " değiştirildi, yeniden yüklensin mi?", "file deleted": "Dosya silindi", "file is not supported": "Dosya desteklenmiyor", "file not supported": "Bu dosya türü desteklenmiyor", "file too large": "Dosya çok büyük. İzin verilen maksimum dosya boyutu {size}", "file renamed": "Dosya yeniden adlandırıldı", "file saved": "Dosya kaydedildi", "folder added": "Klasör eklendi", "folder already added": "Klasör zaten ekli", "font size": "Yazı Boyutu", "goto": "Satıra git", "icons definition": "simgeler tanımı", "info": "Bilgi", "invalid value": "Geçersiz değer", "language changed": "Dil başarıyla değiştirildi", "linting": "Sözdizimi Hatalarını Kontrol Et", "logout": "Çıkış yap", "loading": "Yükleniyor", "my profile": "Profilim", "new file": "Yeni Dosya", "new folder": "Yeni Klasör", "no": "Hayır", "no editor message": "Menüden yeni dosya ve klasör aç veya oluştur", "not set": "Ayarlanmadı", "unsaved files close app": "Kaydedilmemiş dosyalar var. Uygulama kapatılsın mı?", "notice": "Dikkat", "open file": "Dosya Aç", "open files and folders": "Dosya ve Klasör Aç", "open folder": "Klasör Aç", "open recent": "Önceklerden Aç", "ok": "Tamam", "overwrite": "Üzerine yaz", "paste": "Yapıştır", "preview mode": "Ön-izleme Modu", "read only file": "Salt okunur dosya kaydedilemiyor. Lütfen farklı kaydetmeyi deneyin", "reload": "Tekrar Yükle", "rename": "Yeniden Adlandır", "replace": "Değiştir", "required": "Bu alan gerekli", "run your web app": "Web uygulamanızı çalıştırın", "save": "Kaydet", "saving": "kaydediliyor", "save as": "Farklı Kaydet", "save file to run": "Lütfen bu dosyayı tarayıcıda çalışacak şekilde kaydedin", "search": "Ara", "see logs and errors": "Günlük ve hatalara bak", "select folder": "Bu Klasörü Seç", "settings": "Ayarlar", "settings saved": "Ayarlar Kaydedildi", "show line numbers": "Satır Numarasını Göster", "show hidden files": "Gizli Dosyaları Göster", "show spaces": "Boşlukları Göster", "soft tab": "sekmeler Karakteri Yerine Boşluk Kullan", "sort by name": "İsme Göre Sırala", "success": "Başarılı", "tab size": "sekmeler Boyutu", "text wrap": "Sözcük Kaydırma", "theme": "Tema", "unable to delete file": "Dosya silinemedi", "unable to open file": "Dosya açılamadı", "unable to open folder": "Klasör açılamadı", "unable to save file": "Dosya kaydedilemedi", "unable to rename": "Yeniden adlandırılamadı", "unsaved file": "Bu dosya kaydedilmedi, kapatılsın mı?", "warning": "Uyarı", "use emmet": "Emmet kullan", "use quick tools": "Hızlı Araçlar'ı Kullan", "yes": "Evet", "encoding": "Metin Kodlaması", "syntax highlighting": "Sözdizimi Vurgulaması", "read only": "Salt Okunur", "select all": "Hepsini Seç", "select branch": "Branch'ı Seç", "create new branch": "Yeni Branch Oluştur", "use branch": "Branch'ı Kullan", "new branch": "Yeni Branch", "branch": "Branch", "key bindings": "Klavye Kısayolları", "edit": "Düzenle", "reset": "Sıfırla", "color": "Renk", "select word": "Kelime Seç", "quick tools": "Hızlı Araçlar", "select": "Seç", "editor font": "Editörün Yazı Tipi", "new project": "Yeni Proje", "format": "Biçimlendir", "project name": "Proje Adı", "unsupported device": "Cihazınız temayı desteklemiyor", "vibrate on tap": "Tıklamayla vibratör", "copy command is not supported by ftp.": "Kopyalama komutu FTP tarafından desteklenmiyor.", "support title": "Acode'ı destekle", "fullscreen": "tüm-ekran", "animation": "animasyon", "backup": "yedekle", "restore": "restore", "backup successful": "Backup successful", "invalid backup file": "Invalid backup file", "add path": "Yol ekle", "live autocompletion": "canlı Otomatik Düzenleme", "file properties": "dosya bilgileri", "path": "Yol", "type": "tip", "word count": "Kelime sayısı", "line count": "Satır sayısı", "last modified": "En Son değiştirilen", "size": "Boyut", "share": "paylaş", "show print margin": "Show print margin", "login": "login", "scrollbar size": "Scrollbar size", "cursor controller size": "Cursor controller size", "none": "none", "small": "small", "large": "large", "floating button": "Floating button", "confirm on exit": "Confirm on exit", "show console": "Show console", "image": "Image", "insert file": "Insert file", "insert color": "Insert color", "powersave mode warning": "Turn off power saving mode to preview in external browser.", "exit": "Exit", "custom": "custom", "reset warning": "Are you sure you want to reset theme?", "theme type": "Theme type", "light": "light", "dark": "dark", "file browser": "File Browser", "operation not permitted": "Operation not permitted", "no such file or directory": "No such file or directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Not a directory", "is a directory": "Is a directory", "invalid argument": "Invalid argument", "too many open files in system": "Too many open files in system", "too many open files": "Too many open files", "text file busy": "Text file busy", "no space left on device": "No space left on device", "read-only file system": "Read-only file system", "file name too long": "File name too long", "too many users": "Too many users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "An error occurred", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "Save file", "save file as": "Save file as", "files": "Files", "help": "Help", "file has been deleted": "{file} has been deleted!", "feature not available": "This feature is only available in paid version of the app.", "deleted file": "Deleted file", "line height": "Line height", "preview info": "If you want run the active file, tap and hold on play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "Close file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Open in browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "Rate Acode", "support": "Support", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme and key bindings. It will not backup your FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Sponsor", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/uk-ua.json ================================================ { "lang": "Українська", "about": "Про програму", "active files": "Активні файли", "alert": "Сповіщення", "app theme": "Тема", "autocorrect": "Дозволити автовиправлення?", "autosave": "Автозберігання", "cancel": "Скасувати", "change language": "Змінити мову", "choose color": "Обрати колір", "clear": "очистити", "close app": "Закрити програму?", "commit message": "Повідомлення коміту", "console": "Консоль", "conflict error": "Конфлікт! Зачекайте перед іншим комітом.", "copy": "Копіювати", "create folder error": "Вибачте, не вдалося створити нову теку", "cut": "Вирізати", "delete": "Видалити", "dependencies": "Залежності", "delay": "Час у мілісекундах", "editor settings": "Параметри редактора", "editor theme": "Тема редактора", "enter file name": "Уведіть назву файла", "enter folder name": "Уведіть назву теки", "empty folder message": "Порожня тека", "enter line number": "Уведіть номер рядка", "error": "Помилка", "failed": "Не вдалося", "file already exists": "Файл уже існує", "file already exists force": "Файл уже існує. Перезаписати?", "file changed": " змінено, перевантажити файл?", "file deleted": "Файл видалено", "file is not supported": "Файл не підтримується", "file not supported": "Цей тип файлу не підтримується.", "file too large": "Файл завеликий для обробки. Найбільший дозволений розмір файлу {size}", "file renamed": "файл перейменовано", "file saved": "файл збережено", "folder added": "теку додано", "folder already added": "теку вже додано", "font size": "Розмір шрифту", "goto": "Перейти до рядка", "icons definition": "Визначення значків", "info": "Інфо", "invalid value": "Неправильне значення", "language changed": "мову вдало змінено", "linting": "Помилка перевірки синтаксису", "logout": "Вихід", "loading": "Завантаження", "my profile": "Мій профіль", "new file": "Новий файл", "new folder": "Нова тека", "no": "Ні", "no editor message": "Відкрити або створити новий файл і теку з меню", "not set": "Не задано", "unsaved files close app": "Є незбережені файли. Закрити програму?", "notice": "Примітка", "open file": "Відкрити файл", "open files and folders": "Відкрити файли і теки", "open folder": "Відкрити теку", "open recent": "Відкрити останнє", "ok": "гаразд", "overwrite": "Перезаписати", "paste": "Вставити", "preview mode": "Режим перегляду", "read only file": "Не можливо змінити файл лише для читання. Спробуйте зберегти як", "reload": "Перевантажити", "rename": "Перейменувати", "replace": "Замінити", "required": "Це поле обовʼязкове", "run your web app": "Запустити Вашу веб-аплікацію", "save": "Зберегти", "saving": "Зберігання", "save as": "Зберегти як", "save file to run": "Збережіть цей файл для запуску в оглядачі", "search": "Пошук", "see logs and errors": "Дивитися журнал і помилки", "select folder": "Вибрати теку", "settings": "Параметри", "settings saved": "Налаштування збережено", "show line numbers": "Показувати номери рядків", "show hidden files": "Показувати приховані файли", "show spaces": "Показувати пробіли", "soft tab": "Мʼякі таби", "sort by name": "Сортувати за назвою", "success": "Вдало", "tab size": "Розмір табів", "text wrap": "Перенесення тексту", "theme": "Тема", "unable to delete file": "не можливо видалити файл", "unable to open file": "Вибачте, не можливо відкрити файл", "unable to open folder": "Вибачте, не можливо відкрити теку", "unable to save file": "Вибачте, не можливо зберегти файл", "unable to rename": "Вибачте, не можливо перейменувати", "unsaved file": "Цей файл не збережено, закрити попри все?", "warning": "Увага", "use emmet": "Викор. мурашку", "use quick tools": "Викор. швидкі засоби", "yes": "Так", "encoding": "Кодування тексту", "syntax highlighting": "Підсвічування синтаксису", "read only": "Лише для читання", "select all": "Виділити все", "select branch": "Вибрати гілку", "create new branch": "Створити нову гілку", "use branch": "Викор. гілку", "new branch": "Нова гілка", "branch": "Гілка", "key bindings": "Комбінації клавіш", "edit": "Змінити", "reset": "Скинути", "color": "Колір", "select word": "Виділити слово", "quick tools": "Швидкі засоби", "select": "Виділити", "editor font": "Шрифт редактора", "new project": "Новий проєкт", "format": "Формат", "project name": "Назва проєкту", "unsupported device": "Ваш пристрій не підтримує тему.", "vibrate on tap": "Вібрувати під час дотику", "copy command is not supported by ftp.": "Команда копіювання не підтримується FTP.", "support title": "Підтримати Acode", "fullscreen": "На весь екран", "animation": "Анімація", "backup": "Резервна копія", "restore": "Відновити", "backup successful": "Вдало зарезервовано", "invalid backup file": "Не коректний файл резервної копії", "add path": "Додати шлях", "live autocompletion": "Живе автодоповнення", "file properties": "Властивості файлу", "path": "Шлях", "type": "Тип", "word count": "Кількість слів", "line count": "Кількість рядків", "last modified": "Востаннє змінено", "size": "Розмір", "share": "Поділитися", "show print margin": "Показувати поля друку", "login": "Вхід", "scrollbar size": "Розмір смуги прокручування", "cursor controller size": "Розмір контролера курсора", "none": "Нема", "small": "Малий", "large": "Великий", "floating button": "Плаваюча кнопка", "confirm on exit": "Підтверджувати вихід", "show console": "Показати консоль", "image": "Зображення", "insert file": "Вставити файл", "insert color": "Вставити колір", "powersave mode warning": "Вимкнути режим збереження енергії для перегляду в зовнішньому оглядачі.", "exit": "Вихід", "custom": "Власна", "reset warning": "Скинути тему?", "theme type": "Тип теми", "light": "Світла", "dark": "Темна", "file browser": "Огляд файлів", "operation not permitted": "Недозволена операція", "no such file or directory": "Нема такого файла або каталога", "input/output error": "Помилка вводу/виводу", "permission denied": "Дозвіл відхилено", "bad address": "Погана адреса", "file exists": "Файл існує", "not a directory": "Не каталог", "is a directory": "Є каталогом", "invalid argument": "Неправильний арґумент", "too many open files in system": "Забагато відкритих файлів у системі", "too many open files": "Забагато відкритих файлів", "text file busy": "Текстовий файл зайнятий", "no space left on device": "Нема відступів ліворуч пристрою", "read-only file system": "Файлова система лише для читання", "file name too long": "Задовга назва файлу", "too many users": "Забагато користувачів", "connection timed out": "Час зʼєднання вичерпано", "connection refused": "У зʼєднанні відмовлено", "owner died": "Власник помер", "an error occurred": "Відбулася помилка", "add ftp": "Додати FTP", "add sftp": "Додати SFTP", "save file": "Зберегти файл", "save file as": "Зберегти файл як", "files": "Файли", "help": "Довідка", "file has been deleted": "{file} видалено!", "feature not available": "Ця функція доступна лише в платній версії програми.", "deleted file": "Видалений файл", "line height": "Висота рядка", "preview info": "Якщо ви хочете запустити активний файл, натисніть й утримуйте піктограму відтворення.", "manage all files": "Дозвольте редактору Acode керувати всіма файлами в налаштуваннях, щоб легко редагувати файли на вашому пристрої.", "close file": "Закрити файл", "reset connections": "Скинути зʼєднання", "check file changes": "Перевіряти зміни файлу", "open in browser": "Відкрити в оглядачі", "desktop mode": "Режим стільниці", "toggle console": "Увімкнути консоль", "new line mode": "Режим нового рядка", "add a storage": "Додати сховище", "rate acode": "Оцінити Acode", "support": "Підтримка", "downloading file": "Завантажується {file}", "downloading...": "Завантаження...", "folder name": "Назва теки", "keyboard mode": "Режим клавіатури", "normal": "Нормальний", "app settings": "Налаштування програми", "disable in-app-browser caching": "Вимкнути кешування в оглядачі програми", "copied to clipboard": "Скопійовано до буфера обміну", "remember opened files": "Памʼятати відкриті файли", "remember opened folders": "Памʼятати відкриті теки", "no suggestions": "Без пропозицій", "no suggestions aggressive": "Без аґресивних пропозицій", "install": "Встановити", "installing": "Встановлення...", "plugins": "Плагіни", "recently used": "Недавно використане", "update": "Оновити", "uninstall": "Видалити", "download acode pro": "Завантажити Acode pro", "loading plugins": "Завантаження плагінів", "faqs": "FAQs", "feedback": "Зворотний зв'язок", "header": "Заголовок", "sidebar": "Бічна панель", "inapp": "У додатку", "browser": "Браузер", "diagonal scrolling": "Діагональне прокручування", "reverse scrolling": "Зворотне прокручування", "formatter": "Форматувальник", "format on save": "Форматування при збереженні", "remove ads": "Видалити рекламу", "fast": "Швидко", "slow": "Повільно", "scroll settings": "Налаштування прокручування", "scroll speed": "Швидкість прокручування", "loading...": "Завантаження...", "no plugins found": "Плагіни не знайдені", "name": "Ім'я", "username": "Ім'я користувача", "optional": "Опціонально", "hostname": "Ім'я хоста", "password": "Пароль", "security type": "Тип безпеки", "connection mode": "Режим підключення", "port": "Порт", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Підключення...", "type filename": "Type filename", "unable to load files": "Неможливо завантажити файли", "preview port": "Порт попереднього перегляду", "find file": "Знайти файл", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Чутливість до регістру", "regular expression": "Регулярний вираз", "whole word": "Ціле слово", "edit with": "Редагувати за допомогою", "open with": "Відкрити за допомогою", "no app found to handle this file": "Не знайдено програми для роботи з цим файлом", "restore default settings": "Відновити стандартні налаштування", "server port": "Порт сервера", "preview settings": "Налаштування попереднього перегляду", "preview settings note": "Якщо порт попереднього перегляду та порт сервера відрізняються, програма не запустить сервер, а замість цього відкриє https://: у браузері або вбудованому браузері програми. Це корисно, коли ви запускаєте сервер десь інде.", "backup/restore note": "Буде створено резервну копію лише ваших налаштувань, індивідуальної теми та комбінацій клавіш. Резервна копія ваших FTP/SFTP не буде створена.", "host": "Хост", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "Більше", "thank you :)": "Дякую :)", "purchase pending": "purchase pending", "cancelled": "скасовано", "local": "Локальний", "remote": "Віддалений", "show console toggler": "Показати перемикач консолі", "binary file": "Цей файл містить бінарні дані, чи хочете ви його відкрити?", "relative line numbers": "Відносні номери рядків", "elastic tabstops": "Еластичні табулятори", "line based rtl switching": "Комутація RTL на основі ліній", "hard wrap": "Hard wrap", "spellcheck": "Перевірка орфографії", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Недійсний плагін", "type command": "Type command", "plugin": "Плагін", "quicktools trigger mode": "Режим запуску Quicktools", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Приховати рядок заголовка на головному екрані.", "info-checkfiles": "Перевіряти зміни файлів, коли програма працює у фоновому режимі.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Запам'ятовувати відкриті файли після закриття програми.", "info-rememberfolders": "Запам'ятовувати відкриті папки після закриття програми.", "info-floatingbutton": "Показати або приховати плаваючу кнопку швидких інструментів.", "info-openfilelistpos": "Де відображати список активних файлів.", "info-touchmovethreshold": "Якщо чутливість сенсорного екрану вашого пристрою занадто висока, ви можете збільшити це значення, щоб запобігти випадковому переміщенню.", "info-scroll-settings": "Ці налаштування містять налаштування прокрутки, включаючи обтікання тексту.", "info-animation": "Якщо програма працює повільно, вимкніть анімацію.", "info-quicktoolstriggermode": "Якщо кнопка в швидких інструментах не працює, спробуйте змінити це значення.", "info-checkForAppUpdates": "Автоматично перевіряти наявність оновлень для додатка.", "info-quickTools": "Показати або приховати швидкі інструменти.", "info-showHiddenFiles": "Показувати приховані файли та папки. (Починати з .)", "info-all_file_access": "Увімкнути доступ до /sdcard та /storage у терміналі.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "Колірна тема терміналу.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Відновлює резервну копію інсталяції терміналу.", "info-uninstall": "Видаляє інсталяцію терміналу.", "owned": "Власний", "api_error": "Сервер API не працює, спробуйте пізніше.", "installed": "Встановлено", "all": "Усі", "medium": "Середній", "refund": "Повернення коштів", "product not available": "Продукт недоступний", "no-product-info": "Цей продукт наразі недоступний у вашій країні. Спробуйте пізніше.", "close": "Закрити", "explore": "Explore", "key bindings updated": "Оновлено комбінації клавіш", "search in files": "Пошук у файлах", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Зберегти все", "close all": "Закрити все", "unsaved files warning": "Деякі файли не збережені. Натисніть «ОК», виберіть, що робити, або натисніть «Скасувати», щоб повернутися назад.", "save all warning": "Ви впевнені, що хочете зберегти всі файли і закрити? Цю дію неможливо скасувати.", "save all changes warning": "Ви впевнені, що хочете зберегти всі файли?", "close all warning": "Ви впевнені, що хочете закрити всі файли? Ви втратите незбережені зміни, і цю дію неможливо скасувати.", "refresh": "Оновити", "shortcut buttons": "Кнопки швидкого доступу", "no result": "Немає результатів", "searching...": "Пошук...", "quicktools:ctrl-key": "Клавіша Control/Command", "quicktools:tab-key": "Клавіша Tab", "quicktools:shift-key": "Клавіша Shift", "quicktools:undo": "Скасувати", "quicktools:redo": "Повторити", "quicktools:search": "Пошук у файлі", "quicktools:save": "Зберегти файл", "quicktools:esc-key": "Клавіша Escape", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Клавіша зі стрілкою вліво", "quicktools:right-arrow-key": "Клавіша зі стрілкою вправо", "quicktools:up-arrow-key": "Клавіша зі стрілкою вгору", "quicktools:down-arrow-key": "Клавіша зі стрілкою вниз", "quicktools:moveline-up": "Перемістити лінію вгору", "quicktools:moveline-down": "Перемістити лінію вниз", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Вставити лапки", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Вставити знак рівності", "quicktools:slash": "Вставити символ косої риски", "quicktools:exclamation": "Вставити знак оклику", "quicktools:alt-key": "Клавіша Alt", "quicktools:meta-key": "Клавіша Windows/Meta", "info-quicktoolssettings": "Налаштуйте кнопки швидкого доступу та клавіші клавіатури в контейнері Quicktools під редактором, щоб покращити свій досвід кодування.", "info-excludefolders": "Використовуйте шаблон **/node_modules/**, щоб проігнорувати всі файли з папки node_modules. Це виключить файли зі списку та запобіжить їх включенню в пошук файлів.", "missed files": "Після початку пошуку було проскановано {count} файлів, які не будуть включені в пошук.", "remove": "Видалити", "quicktools:command-palette": "Палітра команд", "default file encoding": "Кодування файлу за замовчуванням", "remove entry": "Ви впевнені, що хочете видалити '{name}' зі збережених шляхів? Зверніть увагу, що його видалення не призведе до видалення самого шляху.", "delete entry": "Підтвердити видалення: '{name}'. Цю дію неможливо скасувати. Продовжити?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Ви впевнені, що хочете знову відкрити '{file}'? Усі незбережені зміни будуть втрачені.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Попередній перегляд кольору", "confirm": "Підтвердити", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Проблеми", "show side buttons": "Показати бічні кнопки", "bug_report": "Надіслати звіт про помилку", "verified publisher": "Перевірений видавець", "most_downloaded": "Найбільш завантажувані", "newly_added": "Нещодавно додані", "top_rated": "Найвищий рейтинг", "rename not supported": "Перейменування в каталозі termux не підтримується", "compress": "Стиснути", "copy uri": "Копіювати Uri", "delete entries": "Ви впевнені, що хочете видалити {count} елементів?", "deleting items": "Видалення {count} елементів...", "import project zip": "Імпортувати проект (zip)", "changelog": "Журнал змін", "notifications": "Повідомлення", "no_unread_notifications": "Немає непрочитаних повідомлень", "should_use_current_file_for_preview": "Слід використовувати Поточний файл для попереднього перегляду замість стандартного (index.html)", "fade fold widgets": "Віджети згортання з ефектом зникання", "quicktools:home-key": "Клавіша Home", "quicktools:end-key": "Клавіша End", "quicktools:pageup-key": "Клавіша PageUp", "quicktools:pagedown-key": "Клавіша PageDown", "quicktools:delete-key": "Клавіша Delete", "quicktools:tilde": "Вставити символ тильда", "quicktools:backtick": "Вставити зворотну лапку", "quicktools:hash": "Вставити символ хеш", "quicktools:dollar": "Вставити символ долара", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Вставити символ каретки", "plugin_enabled": "Плагін увімкнено", "plugin_disabled": "Плагін вимкнено", "enable_plugin": "Увімкнути цей плагін", "disable_plugin": "Вимкнути цей плагін", "open_source": "Відкритий код", "terminal settings": "Налаштування терміналу", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Мигання курсору", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Стиль курсора", "terminal:font family": "Сімейство шрифтів", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Підтримка зображень", "terminal": "Термінал", "allFileAccess": "Доступ до всіх файлів", "fonts": "Шрифти", "sponsor": "Спонсор", "downloads": "завантаження", "reviews": "відгуки", "overview": "Огляд", "contributors": "Учасники", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Перевірити наявність оновлень для програми", "prompt update check consent message": "Acode може перевіряти наявність нових оновлень програми, коли ви перебуваєте в мережі. Увімкнути перевірку оновлень?", "keywords": "Ключові слова", "author": "Автор", "filtered by": "Відфільтровано за", "clean install state": "Clean Install State", "backup created": "Резервна копія створена", "restore completed": "Відновлення завершено", "restore will include": "This will restore", "restore warning": "Цю дію неможливо скасувати. Продовжити?", "reload to apply": "Перезавантажити, щоб застосувати зміни?", "reload app": "Перезавантажити додаток", "preparing backup": "Підготовка резервної копії", "collecting settings": "Збір налаштувань", "collecting key bindings": "Збір комбінацій клавіш", "collecting plugins": "Збір інформації про плагіни", "creating backup": "Створення файлу резервної копії", "validating backup": "Перевірка резервної копії", "restoring key bindings": "Відновлення комбінацій клавіш", "restoring plugins": "Відновлення плагінів", "restoring settings": "Відновлення налаштувань", "legacy backup warning": "Це старий формат резервного копіювання. Деякі функції можуть бути обмежені.", "checksum mismatch": "Невідповідність контрольної суми — файл резервної копії, можливо, було змінено або пошкоджено.", "plugin not found": "Плагін не знайдено в реєстрі", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Відновлено", "skipped": "Пропущено", "backup not valid object": "Файл резервної копії не є дійсним об'єктом", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "Це старіший формат резервного копіювання (v1). Деякі функції можуть бути обмежені.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Невідповідність контрольної суми — файл резервної копії, можливо, було змінено або пошкоджено. Дійте обережно.", "backup checksum verify failed": "Не вдалося перевірити контрольну суму", "backup invalid settings": "Недійсний формат налаштувань", "backup invalid keybindings": "Неправильний формат keyBindings", "backup invalid plugins": "Неправильний формат installedPlugins", "issues found": "Знайдені проблеми", "error details": "Деталі помилки", "active tools": "Активні інструменти", "available tools": "Доступні інструменти", "recent": "Нещодавні файли", "command palette": "Відкрити палітру команд", "change theme": "Змінити тему", "documentation": "Документація", "open in terminal": "Відкрити в терміналі", "developer mode": "Режим розробника", "info-developermode": "Увімкнути інструменти розробника (Eruda) для налагодження плагінів та перевірки стану програми. Інспектор буде ініціалізовано під час запуску програми.", "developer mode enabled": "Режим розробника ввімкнено. Використовуйте палітру команд для перемикання інспектора (Ctrl+Shift+I).", "developer mode disabled": "Режим розробника вимкнено", "copy relative path": "Копіювати відносний шлях", "shortcut request sent": "Запит на створення ярлика відкрито. Натисніть «Додати», щоб завершити.", "add to home screen": "Додати на головний екран", "pin shortcuts not supported": "Ярлики на головному екрані не підтримуються на цьому пристрої.", "save file before home shortcut": "Збережіть файл, перш ніж додавати його на головний екран.", "terminal_required_message_for_lsp": "Термінал не встановлено. Будь ласка, спочатку встановіть Термінал, щоб використовувати сервери LSP.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Діагностика", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Команда встановлення", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Сервер не знайдено", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "вимкнено", "lsp-state-enabled": "увімкнено", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Встановлено", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Невідомо", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Версія: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "Про Acode", "settings-category-advanced": "Розширений", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Запитати перед закриттям програми.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Приховати рядок стану системи під час використання Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Виберіть мову програми та перекладені мітки.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Запускати форматувальник щоразу, коли файл зберігається.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Шрифти, вкладки, підказки та відображення редактора.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Налаштуйте мовні сервери та інтелектуальні функції редактора.", "settings-info-main-plugins": "Керуйте встановленими плагінами та доступними для них діями.", "settings-info-main-preview-settings": "Режим попереднього перегляду, порти сервера та поведінка браузера.", "settings-info-main-rateapp": "Оцініть Acode в Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Тема терміналу, шрифт, курсор та поведінка сеансу.", "settings-info-main-theme": "Тема додатка, контрастність та власні кольори.", "settings-info-preview-disable-cache": "Завжди перезавантажуйте вміст у вбудованому браузері програми.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Порт, який використовується сервером попереднього перегляду.", "settings-info-preview-server-port": "Порт, що використовується внутрішнім сервером додатків.", "settings-info-preview-show-console-toggler": "Показувати кнопку консолі в попередньому перегляді.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Призначте форматувальник для кожної мови. Встановіть плагіни форматувальників, щоб розблокувати більше опцій.", "settings-note-lsp-settings": "Мовні сервери додають автозаповнення, діагностику, деталі при наведенні курсора тощо. Тут ви можете встановлювати, оновлювати або визначати власні сервери. Керовані інсталятори працюють у середовищі терміналу/proot.", "search result label singular": "результат", "search result label plural": "результати", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/uz-uz.json ================================================ { "lang": "O'zbekcha (by TILON)", "about": "Ilova haqida", "active files": "Faol fayllar", "alert": "Ogohlantirish", "app theme": "Ilova mavzusi", "autocorrect": "Avtomatik tuzatish yoqilsinmi?", "autosave": "Avtomatik saqlash", "cancel": "bekor qilish", "change language": "Tilni o'zgartirish", "choose color": "Rangni tanlang", "clear": "Tozalash", "close app": "Dasturdan chiqmoqchimisiz?", "commit message": "Xabar berish", "console": "Konsol oynasi", "conflict error": "Qarama-qarshilik! Iltimos, boshqa majburiyatlar olishdan oldin kuting", "copy": "Nusxalash", "create folder error": "Kechirasiz,yangi papka yaratib bo'lmadi", "cut": "Qirqish", "delete": "o'chirish", "dependencies": "Bog'lanishlar", "delay": "Vaqt millisekundlarda", "editor settings": "Tahrirlash sozlamalari", "editor theme": "Tahrirlash mavzulari", "enter file name": "fayl nomini kiriting", "enter folder name": "papka nomini kiriting", "empty folder message": "Ushbu papkada hech narsa yo'q", "enter line number": "Qator raqamini kiriting", "error": "xatolik", "failed": "bajarilmadi", "file already exists": "Ushbu fayl oldindan mavjud", "file already exists force": "Ushbu fayl oldindan mavjud. Baribir yozilsinmi?", "file changed": "o'zgartirildi, faylni qayta yuklaysizmi?", "file deleted": "fayl o'chirildi", "file is not supported": "ushhu fayl qo'llab-quvvatlanmaydi", "file not supported": "fayl turi qo'llab-quvvatlanmaydi", "file too large": "Fayl xajmi juda katta. Maksimal hajmdagi fayl ruxsat etilgan: {size}", "file renamed": "fayl qayta nomlandi", "file saved": "fayl saqlandi", "folder added": "papka qo'shildi", "folder already added": "ushbu papka oldindan qo'shilgan", "font size": "Matn o'lchovi", "goto": "Qatorga borish", "icons definition": "Ikonlarni aniqlash", "info": "Haqida", "invalid value": "Kiritishda xatolik", "language changed": "Til muvoffaqiyatli o'zgartirildi", "linting": "Sintaktik xatolar tekshirilsinmi", "logout": "Tark etish", "loading": "Yuklanmoqda", "my profile": "Mening profilim", "new file": "Yangi fayl", "new folder": "Yangi papka", "no": "Yo'q", "no editor message": "Menyudan yangi fayl va papkani oching yoki yarating", "not set": "O'rnatilmagan", "unsaved files close app": "Saqlanmagan fayllar mavjud. Ilova yopilsinmi?", "notice": "Etibor bering", "open file": "Faylni ochish", "open files and folders": "Fayllarni va papkalarni ochish", "open folder": "Papkani ochish", "open recent": "So'ngi ochilganlar", "ok": "Yaxshi", "overwrite": "Baribir yozilsin", "paste": "Joylash", "preview mode": "Kod natijasini qayerda ko'moqchisiz?", "read only file": "Faqat o'qish uchun bo'lgan faylni saqlab bo'lmadi,Iltimos to'g'irlab qayta saqlab ko'ring", "reload": "qayta yuklash", "rename": "qayta nomlash", "replace": "almashtrish", "required": "Ushbu qator to'ldirilishi shart", "run your web app": "Web-ilovangizni ishga tushiring", "save": "Saqlash", "saving": "Saqlanmoqda", "save as": "Boshqa joyga saqlash", "save file to run": "Iltimos, brauzerda ishga tushirish uchun ushbu faylni saqlang", "search": "qidirish", "see logs and errors": "Loglar va xatoliklarni ko'rish", "select folder": "Papkani tanlash", "settings": "Sozlamalar", "settings saved": "Sozlamalar saqlandi", "show line numbers": "Qator raqamlari ko'rinsinmi", "show hidden files": "Yashirin fayllar ko'rinsinmi", "show spaces": "Bo'sh joylar ko'rinsinmi", "soft tab": "Qulay yorliq", "sort by name": "Ism bo'yicha saralash", "success": "Bajarildi", "tab size": "Yorliq hajmi", "text wrap": "Matnni o'rash", "theme": "Mavzu", "unable to delete file": "faylni o'chirib bo'lmadi", "unable to open file": "Kechirasiz,faylni ochib bo'lmadi", "unable to open folder": "Kechirasiz,papkani ochib bo'lmadi", "unable to save file": "Kechirasiz,faylni saqlab bo'lmadi", "unable to rename": "Kechirasiz,faylni qayta nomlab bo'lmadi", "unsaved file": "Ushbu fayl saqlanmadi, baribir yopilsinmi?", "warning": "Ogohlantirish", "use emmet": "Emmetdan foydalanish", "use quick tools": "Qo'shimcha xususiyatlardan foydalanish", "yes": "Ha", "encoding": "Matnni kodirivkalash", "syntax highlighting": "Sintaktikani ajiratib ko'rsatish", "read only": "Faqat o'qish", "select all": "Barchasini tanlash", "select branch": "Branchni tanlash", "create new branch": "Yangi branch yaratish.", "use branch": "Branchdan foydalanish", "new branch": "Yangi branch", "branch": "branch", "key bindings": "Tezkor kalitlarni o'zgartrish", "edit": "Tahrirlash", "reset": "qayta o'rnatish", "color": "Rang", "select word": "So'zni tanlash", "quick tools": "Tezkor xususiyatlar", "select": "tanlash", "editor font": "Shrift tahrirlash", "new project": "Yangi proyekt", "format": "format", "project name": "Proyekt nomi", "unsupported device": "Sizning qurilmangiz ushbu mavzuni qo'llab quvvatlamaydi", "vibrate on tap": "Vibrate on tap", "copy command is not supported by ftp.": "Copy command is not supported by FTP.", "support title": "Support Acode", "fullscreen": "fullscreen", "animation": "animation", "backup": "backup", "restore": "restore", "backup successful": "Backup successful", "invalid backup file": "Invalid backup file", "add path": "Add path", "live autocompletion": "Live autocompletion", "file properties": "File properties", "path": "Path", "type": "Type", "word count": "Word count", "line count": "Line count", "last modified": "Last modified", "size": "Size", "share": "Share", "show print margin": "Show print margin", "login": "login", "scrollbar size": "Scrollbar size", "cursor controller size": "Cursor controller size", "none": "none", "small": "small", "large": "large", "floating button": "Floating button", "confirm on exit": "Confirm on exit", "show console": "Show console", "image": "Image", "insert file": "Insert file", "insert color": "Insert color", "powersave mode warning": "Turn off power saving mode to preview in external browser.", "exit": "Exit", "custom": "custom", "reset warning": "Are you sure you want to reset theme?", "theme type": "Theme type", "light": "light", "dark": "dark", "file browser": "File Browser", "operation not permitted": "Operation not permitted", "no such file or directory": "No such file or directory", "input/output error": "Input/output error", "permission denied": "Permission denied", "bad address": "Bad address", "file exists": "File exists", "not a directory": "Not a directory", "is a directory": "Is a directory", "invalid argument": "Invalid argument", "too many open files in system": "Too many open files in system", "too many open files": "Too many open files", "text file busy": "Text file busy", "no space left on device": "No space left on device", "read-only file system": "Read-only file system", "file name too long": "File name too long", "too many users": "Too many users", "connection timed out": "Connection timed out", "connection refused": "Connection refused", "owner died": "Owner died", "an error occurred": "An error occurred", "add ftp": "Add FTP", "add sftp": "Add SFTP", "save file": "Save file", "save file as": "Save file as", "files": "Files", "help": "Help", "file has been deleted": "{file} has been deleted!", "feature not available": "This feature is only available in paid version of the app.", "deleted file": "Deleted file", "line height": "Line height", "preview info": "If you want run the active file, tap and hold on play icon.", "manage all files": "Allow Acode editor to manage all files in settings to edit files on your device easily.", "close file": "Close file", "reset connections": "Reset connections", "check file changes": "Check file changes", "open in browser": "Open in browser", "desktop mode": "Desktop mode", "toggle console": "Toggle console", "new line mode": "New line mode", "add a storage": "Add a storage", "rate acode": "Rate Acode", "support": "Support", "downloading file": "Downloading {file}", "downloading...": "Downloading...", "folder name": "Folder name", "keyboard mode": "Keyboard mode", "normal": "Normal", "app settings": "App settings", "disable in-app-browser caching": "Disable in-app-browser caching", "copied to clipboard": "Copied to clipboard", "remember opened files": "Remember opened files", "remember opened folders": "Remember opened folders", "no suggestions": "No suggestions", "no suggestions aggressive": "No suggestions aggressive", "install": "Install", "installing": "Installing...", "plugins": "Plugins", "recently used": "Recently used", "update": "Update", "uninstall": "Uninstall", "download acode pro": "Download Acode pro", "loading plugins": "Loading plugins", "faqs": "FAQs", "feedback": "Feedback", "header": "Header", "sidebar": "Sidebar", "inapp": "Inapp", "browser": "Browser", "diagonal scrolling": "Diagonal scrolling", "reverse scrolling": "Reverse Scrolling", "formatter": "Formatter", "format on save": "Format on save", "remove ads": "Remove ads", "fast": "Fast", "slow": "Slow", "scroll settings": "Scroll settings", "scroll speed": "Scroll speed", "loading...": "Loading...", "no plugins found": "No plugins found", "name": "Name", "username": "Username", "optional": "optional", "hostname": "Hostname", "password": "Password", "security type": "Security Type", "connection mode": "Connection mode", "port": "Port", "key file": "Key file", "select key file": "Select key file", "passphrase": "Passphrase", "connecting...": "Connecting...", "type filename": "Type filename", "unable to load files": "Unable to load files", "preview port": "Preview port", "find file": "Find file", "system": "System", "please select a formatter": "Please select a formatter", "case sensitive": "Case sensitive", "regular expression": "Regular expression", "whole word": "Whole word", "edit with": "Edit with", "open with": "Open with", "no app found to handle this file": "No app found to handle this file", "restore default settings": "Restore default settings", "server port": "Server port", "preview settings": "Preview settings", "preview settings note": "If preview port and server port are different, app will not start server and it will instead open https://: in browser or in-app browser. This is useful when you are running a server somewhere else.", "backup/restore note": "It will only backup your settings, custom theme and key bindings. It will not backup your FPT/SFTP.", "host": "Host", "retry ftp/sftp when fail": "Retry ftp/sftp when fail", "more": "More", "thank you :)": "Thank you :)", "purchase pending": "purchase pending", "cancelled": "cancelled", "local": "Local", "remote": "Remote", "show console toggler": "Show console toggler", "binary file": "This file contains binary data, do you want to open it?", "relative line numbers": "Relative line numbers", "elastic tabstops": "Elastic tabstops", "line based rtl switching": "Line based RTL switching", "hard wrap": "Hard wrap", "spellcheck": "Spellcheck", "wrap method": "Wrap Method", "use textarea for ime": "Use textarea for IME", "invalid plugin": "Invalid Plugin", "type command": "Type command", "plugin": "Plugin", "quicktools trigger mode": "Quicktools trigger mode", "print margin": "Print margin", "touch move threshold": "Touch move threshold", "info-retryremotefsafterfail": "Retry FTP/SFTP connection when fails", "info-fullscreen": "Hide title bar in home screen.", "info-checkfiles": "Check file changes when app is in background.", "info-console": "Choose JavaScript console. Legacy is default console, eruda is a third party console.", "info-keyboardmode": "Keyboard mode for text input, no suggestions will hide suggestions and auto correct. If no suggestions does not work, try to change value to no suggestions aggressive.", "info-rememberfiles": "Remember opened files when app is closed.", "info-rememberfolders": "Remember opened folders when app is closed.", "info-floatingbutton": "Show or hide quick tools floating button.", "info-openfilelistpos": "Where to show active files list.", "info-touchmovethreshold": "If your device touch sensitivity is too high, you can increase this value to prevent accidental touch move.", "info-scroll-settings": "This settings contain scroll settings including text wrap.", "info-animation": "If the app feels laggy, disable animation.", "info-quicktoolstriggermode": "If button in quick tools is not working, try to change this value.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Owned", "api_error": "API server down, please try after some time.", "installed": "Installed", "all": "All", "medium": "Medium", "refund": "Refund", "product not available": "Product not available", "no-product-info": "This product is not available in your country at this moment, please try again later.", "close": "Close", "explore": "Explore", "key bindings updated": "Key bindings updated", "search in files": "Search in files", "exclude files": "Exclude files", "include files": "Include files", "search result": "{matches} results in {files} files.", "invalid regex": "Invalid regular expression: {message}.", "bottom": "Bottom", "save all": "Save all", "close all": "Close all", "unsaved files warning": "Some files are not saved. Click 'ok' select what to do or press 'cancel' to go back.", "save all warning": "Are you sure you want to save all files and close? This action cannot be reversed.", "save all changes warning": "Are you sure you want to save all files?", "close all warning": "Are you sure you want to close all files? You will lose the unsaved changes and this action cannot be reversed.", "refresh": "Refresh", "shortcut buttons": "Shortcut buttons", "no result": "No result", "searching...": "Searching...", "quicktools:ctrl-key": "Control/Command key", "quicktools:tab-key": "Tab key", "quicktools:shift-key": "Shift key", "quicktools:undo": "Undo", "quicktools:redo": "Redo", "quicktools:search": "Search in file", "quicktools:save": "Save file", "quicktools:esc-key": "Escape key", "quicktools:curlybracket": "Insert curly bracket", "quicktools:squarebracket": "Insert square bracket", "quicktools:parentheses": "Insert parentheses", "quicktools:anglebracket": "Insert angle bracket", "quicktools:left-arrow-key": "Left arrow key", "quicktools:right-arrow-key": "Right arrow key", "quicktools:up-arrow-key": "Up arrow key", "quicktools:down-arrow-key": "Down arrow key", "quicktools:moveline-up": "Move line up", "quicktools:moveline-down": "Move line down", "quicktools:copyline-up": "Copy line up", "quicktools:copyline-down": "Copy line down", "quicktools:semicolon": "Insert semicolon", "quicktools:quotation": "Insert quotation", "quicktools:and": "Insert and symbol", "quicktools:bar": "Insert bar symbol", "quicktools:equal": "Insert equal symbol", "quicktools:slash": "Insert slash symbol", "quicktools:exclamation": "Insert exclamation", "quicktools:alt-key": "Alt key", "quicktools:meta-key": "Windows/Meta key", "info-quicktoolssettings": "Customize shortcut buttons and keyboard keys in the Quicktools container below the editor to enhance your coding experience.", "info-excludefolders": "Use the pattern **/node_modules/** to ignore all files from the node_modules folder. This will exclude the files from being listed and will also prevent them from being included in file searches.", "missed files": "Scanned {count} files after search started and will not be included in search.", "remove": "Remove", "quicktools:command-palette": "Command palette", "default file encoding": "Default file encoding", "remove entry": "Are you sure you want to remove '{name}' from the saved paths? Please note that removing it will not delete the path itself.", "delete entry": "Confirm deletion: '{name}'. This action cannot be undone. Proceed?", "change encoding": "Reopen '{file}' with '{encoding}' encoding? This action will result in the loss of any unsaved changes made to the file. Do you want to proceed with reopening?", "reopen file": "Are you sure you want to reopen '{file}'? Any unsaved changes will be lost.", "plugin min version": "{name} only available in Acode - {v-code} and above. Click here to update.", "color preview": "Color preview", "confirm": "Confirm", "list files": "List all files in {name}? Too many files may crash the app.", "problems": "Problems", "show side buttons": "Show side buttons", "bug_report": "Submit a Bug Report", "verified publisher": "Verified publisher", "most_downloaded": "Most Downloaded", "newly_added": "Newly Added", "top_rated": "Top Rated", "rename not supported": "Rename on termux dir isn't supported", "compress": "Compress", "copy uri": "Copy Uri", "delete entries": "Are you sure you want to delete {count} items?", "deleting items": "Deleting {count} items...", "import project zip": "Import Project(zip)", "changelog": "Change Log", "notifications": "Notifications", "no_unread_notifications": "No unread notifications", "should_use_current_file_for_preview": "Should use Current File For preview instead of default (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Homiy", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/vi-vn.json ================================================ { "lang": "Tiếng Việt", "about": "Về phần mềm", "active files": "Các tệp hoạt động ", "alert": "Cảnh báo", "app theme": "Chủ đề ứng dụng", "autocorrect": "Bật tự động sửa lỗi?", "autosave": "Tự động lưu", "cancel": "Hủy bỏ", "change language": "Thay đổi ngôn ngữ", "choose color": "Chọn màu", "clear": "xoá hết", "close app": "Đóng ứng dụng?", "commit message": "Tin nhắn commit", "console": "Bảng điều khiển", "conflict error": "Xung đột! Vui lòng đợi trước khi thực hiện commit kế tiếp.", "copy": "Sao chép", "create folder error": "Xin lỗi, không thể tạo thư mục mới", "cut": "Cắt", "delete": "Xóa", "dependencies": "Phụ thuộc", "delay": "Thời gian tính bằng mili giây", "editor settings": "Cài đặt soạn thảo", "editor theme": "Chủ đề soạn thảo", "enter file name": "Nhập tên tệp", "enter folder name": "Nhập tên thư mục", "empty folder message": "Thư mục trống", "enter line number": "Nhập dòng số", "error": "Lỗi", "failed": "Thất bại", "file already exists": "Tệp đã tồn tại", "file already exists force": "Tệp đã tồn tại. Ghi đè?", "file changed": " đã bị thay đổi, tải lại tệp?", "file deleted": "Tệp đã xóa", "file is not supported": "Tệp không hỗ trợ", "file not supported": "Loại tệp này không được hỗ trợ.", "file too large": "Tệp quá lớn để xử lý. Độ lớn tối đa tệp cho phép là {size}", "file renamed": "tệp đã được đổi tên", "file saved": "tệp đã được lưu", "folder added": "thư mục đã được thêm", "folder already added": "thư mục đã thêm trước đó", "font size": "Kích thước phông chữ", "goto": "Đi đến dòng", "icons definition": "Định nghĩa biểu tượng", "info": "Thông tin", "invalid value": "Giá trị không hợp lệ", "language changed": "ngôn ngữ đã được thay đổi thành công", "linting": "Kiểm tra lỗi cú pháp", "logout": "Đăng xuất", "loading": "Đang tải", "my profile": "Hồ sơ của tôi", "new file": "Tệp mới", "new folder": "Thư mục mới", "no": "Không", "no editor message": "Mở hoặc tạo tệp và thư mục mới từ menu", "not set": "Chưa được đặt", "unsaved files close app": "Có những tệp chưa lưu. Đóng ứng dụng?", "notice": "Chú ý", "open file": "Mở tệp", "open files and folders": "Mở tệp và thư mục", "open folder": "Mở thư mục", "open recent": "Mở mục gần đây", "ok": "ok", "overwrite": "Ghi đè", "paste": "Dán", "preview mode": "Chế độ xem trước", "read only file": "Không thể tệp chỉ đọc. Hãy thử lưu dưới dạng", "reload": "Tải lại", "rename": "Đổi tên", "replace": "Thay thế", "required": "Trường này là bắt buộc", "run your web app": "Chạy ứng dụng web của bạn", "save": "Lưu", "saving": "Đang lưu", "save as": "Lưu dưới dạng", "save file to run": "Hãy lưu tệp này để chạy trong trình duyệt", "search": "Tìm", "see logs and errors": "Xem nhật ký và lỗi", "select folder": "Chọn thư mục", "settings": "Cài đặt", "settings saved": "Đã lưu cài đặt", "show line numbers": "Hiển thị số dòng", "show hidden files": "Hiển thị tệp ẩn", "show spaces": "Hiển thị khoảng trắng", "soft tab": "Tab mềm", "sort by name": "Sắp xếp bằng tên", "success": "Thành công", "tab size": "Kích thước Tab", "text wrap": "Ngắt dòng", "theme": "Chủ đề", "unable to delete file": "không thể xóa tệp", "unable to open file": "Xin lỗi, không thể mở tệp", "unable to open folder": "Xin lỗi, không thể mở thư mục", "unable to save file": "Xin lỗi, không thể lưu tệp", "unable to rename": "Xin lỗi, không thể đổi tên", "unsaved file": "Tệp này chưa được lữu , vẫn đóng lại?", "warning": "Cảnh báo", "use emmet": "Sử dụng Emmet", "use quick tools": "Sử dụng công cụ nhanh", "yes": "Có", "encoding": "Mã hóa văn bản", "syntax highlighting": "Tô sáng cú pháp", "read only": "Chỉ đọc", "select all": "Chọn tất cà", "select branch": "Chọn nhánh", "create new branch": "Tạo nhánh mới", "use branch": "Sử dụng nhánh", "new branch": "Nhánh mới", "branch": "Nhánh", "key bindings": "Phím tắt", "edit": "Chỉnh sửa", "reset": "Đặt lại", "color": "Màu sắc", "select word": "Chọn từ", "quick tools": "Công cụ nhanh", "select": "Chọn", "editor font": "Phông chữ soạn thảo", "new project": "Dự án mới", "format": "Định dạng", "project name": "Tên dự án", "unsupported device": "Thiết bị của bạn không hỗ trợ chủ đề.", "vibrate on tap": "Rung khi chạm", "copy command is not supported by ftp.": "Lệnh sao chép không được FTP hỗ trợ.", "support title": "Hỗ trợ Acode", "fullscreen": "Toàn màn hình", "animation": "Hoạt ảnh", "backup": "Sao lưu", "restore": "Khôi phục", "backup successful": "Sao lưu thành công", "invalid backup file": "Tệp sao lưu không hợp lệ", "add path": "Thêm đường dẫn", "live autocompletion": "Tự động hoàn thành trực tiếp", "file properties": "Thuộc tính tệp", "path": "Đường dẫn", "type": "Loại", "word count": "Số từ", "line count": "Số dòng", "last modified": "Sửa lần cuối", "size": "Kích thước e", "share": "Chia sẻ", "show print margin": "Hiển thị lề in", "login": "Đăng nhập", "scrollbar size": "Kích thước thanh cuộn", "cursor controller size": "Kích thước điều khiển con trỏ", "none": "Không có", "small": "Nhỏ", "large": "Lớn", "floating button": "Nút nổi", "confirm on exit": "Xác nhận khi thoát", "show console": "Hiển thị bảng điều khiển", "image": "Hình ảnh", "insert file": "Chèn tệp", "insert color": "Chèn màu", "powersave mode warning": "Tắt chế độ tiết kiệm điện để xem trước trên trình duyệt bên ngoài.", "exit": "Thoát", "custom": "Tùy chỉnh", "reset warning": "Có chắc muốn đặt lại chủ đề không?", "theme type": "Loại chủ đề", "light": "Sáng", "dark": "Tối", "file browser": "Trình duyệt tệp", "operation not permitted": "Hoạt động không được phép", "no such file or directory": "Không có tệp hoặc thư mục như thế", "input/output error": "Lỗi đầu vào/đầu ra", "permission denied": "Quyền bị từ chối", "bad address": "Địa chỉ không đúng", "file exists": "Tệp đã tồn tại", "not a directory": "Không phải là thư mục", "is a directory": "Là một thư mục", "invalid argument": "Tham số không hợp lệ", "too many open files in system": "Quá nhiều tệp mở trong hệ thống", "too many open files": "Quá nhiều tệp mở", "text file busy": "Tệp văn bản đang bận", "no space left on device": "Không còn chỗ trống trên thiết bị", "read-only file system": "Hệ thống tệp chỉ đọc", "file name too long": "Tên tệp quá dài", "too many users": "Quá nhiều người dùng", "connection timed out": "Kết nối hết thời gian chờ", "connection refused": "Kết nối bị từ chối", "owner died": "Chủ sở hữu đã nằm", "an error occurred": "Đã xảy ra lỗi", "add ftp": "Thêm FTP", "add sftp": "Thêm SFTP", "save file": "Lưu tệp", "save file as": "Lưu tệp dưới dạng", "files": "Tệp", "help": "Trợ giúp", "file has been deleted": "{file} đã bị xoá!", "feature not available": "Tính năng này chỉ có ở phiên bản trả phí của ứng dụng.", "deleted file": "Đã xoá tệp", "line height": "Chiều cao dòng", "preview info": "Nếu muốn chạy tệp đang hoạt động, hãy chạm giữ vào nút phát.", "manage all files": "Cho phép trình soạn thảo Acode quản lý tất cả các tệp trong cài đặt để chỉnh sửa tệp trên thiết bị của bạn một cách dễ dàng.", "close file": "Đóng tệp", "reset connections": "Đặt lại kết nối", "check file changes": "Kiểm tra các thay đổi tệp", "open in browser": "Mở trong trình duyệt", "desktop mode": "Chế độ máy tính", "toggle console": "Chuyển đổi bảng điều khiển", "new line mode": "Chế độ dòng mới", "add a storage": "Thêm một lưu trữ", "rate acode": "Đánh giá Acode", "support": "Hỗ trợ", "downloading file": "Đang tải {file}", "downloading...": "Đang tải...", "folder name": "Tên thư mục", "keyboard mode": "Chế độ bàn phím", "normal": "Bình thường", "app settings": "Cài đặt ứng dụng", "disable in-app-browser caching": "Tắt bộ nhớ đệm trong trình duyệt ứng dụng", "Should use Current File For preview instead of default (index.html)": "Nên sử dụng Tệp hiện tại để xem trước thay vì mặc định (index.html)", "copied to clipboard": "Đã sao chép vào bảng nhớ tạm", "remember opened files": "Ghi nhớ các tệp đã mở", "remember opened folders": "Ghi nhớ các thư mục đã mở", "no suggestions": "Không có gợi ý", "no suggestions aggressive": "Không có gợi ý một cách tích cực", "install": "Cài đặt", "installing": "Đăng cài đặt...", "plugins": "Plugins", "recently used": "Mới sử dụng", "update": "Cập nhật", "uninstall": "Gỡ cài đặt", "download acode pro": "Tải Acode pro", "loading plugins": "Đang tải plugins", "faqs": "Câu hỏi thường gặp", "feedback": "Phản hồi", "header": "Tiêu đề", "sidebar": "Thanh bên", "inapp": "Trong ứng dụng", "browser": "Trình duyệt", "diagonal scrolling": "Cuộn chéo", "reverse scrolling": "Cuộn ngược", "formatter": "Trình định dạng", "format on save": "Định dạng khi lưu", "remove ads": "Xoá quảng cáo", "fast": "Nhanh", "slow": "Chậm", "scroll settings": "Cài đặt cuộn", "scroll speed": "Tốc độ cuộn", "loading...": "Đang tải...", "no plugins found": "Không tìm thấy plugins", "name": "Tên", "username": "Tên người dùng", "optional": "không bắt buộc", "hostname": "Tên máy chủ", "password": "Mật khẩu", "security type": "Loại bảo mật", "connection mode": "Loại kết nối", "port": "Cổng", "key file": "Tệp khoá", "select key file": "Chọn tệp khoá", "passphrase": "Mật khẩu", "connecting...": "Đang kết nối...", "type filename": "Nhập tên tệp", "unable to load files": "Không thể tải tệp", "preview port": "Xem trước cổng", "find file": "Tìm tệp", "system": "Hệ thống", "please select a formatter": "Vui lòng chọn một trình định dạng", "case sensitive": "Phân biệt chữ hoa chữ thường", "regular expression": "Biểu thức chính quy", "whole word": "Toàn bộ từ", "edit with": "Sửa với", "open with": "Mở với", "no app found to handle this file": "Không thấy ứng dụng nào có thể xử lý tệp này", "restore default settings": "Khôi phục cài đặt mặc định", "server port": "Cổng máy chủ", "preview settings": "Cài đặt xem trước", "preview settings note": "Nếu cổng xem trước và cổng máy chủ khác nhau, ứng dụng sẽ không khởi động máy chủ mà thay vào đó sẽ mở https://: trong trình duyệt hoặc trình duyệt trong ứng dụng. Điều này hữu ích khi bạn đang chạy máy chủ ở nơi khác.", "backup/restore note": "Nó sẽ chỉ sao lưu cài đặt, chủ đề tùy chỉnh và phím tắt của bạn. Nó sẽ không sao lưu FTP/SFTP của bạn.", "host": "Máy chủ", "retry ftp/sftp when fail": "Thử lại ftp/sftp khi thất bại", "more": "Thêm", "thank you :)": "Cảm ơn :)", "purchase pending": "đang xử lý giao dịch", "cancelled": "đã hủy", "local": "Nội bộ", "remote": "Từ xa", "show console toggler": "Hiển thị nút chuyển đổi bảng điều khiển", "binary file": "Tệp này chứa dữ liệu nhị phân, bạn có muốn mở nó không?", "relative line numbers": "Số dòng tương đối", "elastic tabstops": "Điểm dùng tab đàn hồi", "line based rtl switching": "Chuyển mạch dòng từ phải --> trái", "hard wrap": "Ngắt dòng cứng", "spellcheck": "Kiểm tra chính tả", "wrap method": "Cách thức ngắt", "use textarea for ime": "Sử dụng textarea cho IME", "invalid plugin": "Plugin không hợp lệ", "type command": "Nhập lệnh", "plugin": "Plugin", "quicktools trigger mode": "Chế độ kích hoạt công cụ nhanh", "print margin": "In lề", "touch move threshold": "Chạm vào ngưỡng di chuyển", "info-retryremotefsafterfail": "Thử kết nối FTP/SFTP lại khi không thành công.", "info-fullscreen": "Ẩn thanh tiêu đề ở màn hình chính.", "info-checkfiles": "Kiểm tra những thay đổi của tệp khi ứng dụng đang chạy nền.", "info-console": "Chọn bảng điều khiển JavaScript. Legacy là bảng điều khiển mặc định, eruda là bảng điều khiển của bên thứ ba.", "info-keyboardmode": "Chế độ bàn phím để nhập văn bản, không có gợi ý sẽ ẩn gợi ý và tự động sửa. Nếu không có gợi ý không hiệu quả, hãy thử thay đổi giá trị thành không có gợi ý một cách tích cực.", "info-rememberfiles": "Ghi nhớ các tệp đã mở khi đóng ứng dụng.", "info-rememberfolders": "Ghi nhớ các thư mục đã mở khi đóng ứng dụng.", "info-floatingbutton": "Hiển thị hoặc ẩn nút nổi của công cụ nhanh.", "info-openfilelistpos": "Nơi hiển thị danh sách các tệp đang hoạt động", "info-touchmovethreshold": "Nếu độ nhạy cảm ứng của thiết bị quá cao, bạn có thể tăng giá trị này để tránh việc di chuyển cảm ứng vô tình.", "info-scroll-settings": "Các thiết lập này bao gồm các thiết lập cuộn và cả cả ngắt dòng", "info-animation": "Hình như hơi dật, tắt hoạt ảnh thử.", "info-quicktoolstriggermode": "Nếu nút trong công cụ nhanh không hoạt động, hãy thử thay đổi giá trị này.", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "Đã sở hữu", "api_error": "Máy chủ API không hoạt động, Hãy thử lại sau", "installed": "Đã cài đặt", "all": "Tất cả", "medium": "Trung bình", "refund": "Hoàn trả", "product not available": "Sản phẩm không có sẵn", "no-product-info": "Sản phẩm này hiện không có sẵn ở quốc gia của bạn, Hãy thử lại sau", "close": "Đóng", "explore": "Khám phá", "key bindings updated": "Đã cập nhật các phím tắt", "search in files": "Tìm kiếm trong các tệp", "exclude files": "Loại trừ các tập tin", "include files": "Bao gồm các tập tin", "search result": "{matches} có trong tệp {files}.", "invalid regex": "Biểu thức chính quy không hợp lệ: {message}.", "bottom": "Phía dưới", "save all": "Lưu tất cả", "close all": "Đóng tất cả", "unsaved files warning": "Một số tệp không được lưu. Nhấp vào 'ok' để chọn việc cần làm hoặc nhấn 'cancel' để quay lại.", "save all warning": "Bạn có chắc muốn lưu tất cả các tệp và đóng không? Việc này không thể hoàn tác.", "save all changes warning": "Bạn có chắc chắn muốn lưu tất cả các tệp không?", "close all warning": "Bạn có chắc muốn đóng tất cả các tệp không? Bạn sẽ mất những thay đổi chưa lưu và việc này không thể hoàn tác.", "refresh": "Làm mới", "shortcut buttons": "Các nút tắt", "no result": "Không có kết quả", "searching...": "Đang tìm...", "quicktools:ctrl-key": "Phím Control/Command", "quicktools:tab-key": "Phím Tab", "quicktools:shift-key": "Phím Shift", "quicktools:undo": "Hoàn tác", "quicktools:redo": "Làm lại", "quicktools:search": "Tìm kiếm trong tệp", "quicktools:save": "Lưu tệp", "quicktools:esc-key": "Phím Escape", "quicktools:curlybracket": "Chèn dấu ngoặc nhọn", "quicktools:squarebracket": "Chèn dấu ngoặc vuông", "quicktools:parentheses": "Chèn dấu ngoặc đơn", "quicktools:anglebracket": "Chèn dấu ngoặc so sánh", "quicktools:left-arrow-key": "Phím mũi tên trái", "quicktools:right-arrow-key": "Phím mũi tên phải", "quicktools:up-arrow-key": "Phím mũi tên lên", "quicktools:down-arrow-key": "Phím mũi tên xuống", "quicktools:moveline-up": "Chuyển dòng đi lên", "quicktools:moveline-down": "Chuyển dòng đi xuống", "quicktools:copyline-up": "Chép dòng lên trên", "quicktools:copyline-down": "Chép dòng xuống dưới", "quicktools:semicolon": "Chèn dấu chấm phẩy", "quicktools:quotation": "Chèn dấu nháy kép", "quicktools:and": "Chèn dấu AND", "quicktools:bar": "Chèn dấu OR", "quicktools:equal": "Chèn dấu Bằng", "quicktools:slash": "Chèn dấu gạch chéo", "quicktools:exclamation": "Chèn dấu chấm than", "quicktools:alt-key": "Phím Alt", "quicktools:meta-key": "Phím Windows/Meta", "info-quicktoolssettings": "Tùy chỉnh các nút tắt và phím bàn phím trong hộp công cụ nhanh bên dưới trình soạn thảo để nâng cao trải nghiệm viết mã.", "info-excludefolders": "Sử dụng mẫu **/node_modules/** để bỏ qua tất cả các tệp từ thư mục node_modules. Thao tác này sẽ loại trừ các tệp khỏi danh sách và cũng sẽ ngăn chúng được đưa vào tìm kiếm tệp.", "missed files": "Đã quét {count} tệp sau khi bắt đầu tìm kiếm và sẽ không được đưa vào tìm kiếm.", "remove": "Loại bỏ", "quicktools:command-palette": "Bảng lệnh", "default file encoding": "Mã hóa tệp mặc định", "remove entry": "Bạn có chắc muốn xóa '{name}' khỏi đường dẫn đã lưu không? Lưu ý rằng việc xóa nó sẽ không xóa chính đường dẫn đó.", "delete entry": "Xác nhận xóa: '{name}'. Không thể hoàn tác việc. Tiếp tục?", "change encoding": "Mở lại '{file}' với mã hóa '{encoding}'? Hành động này sẽ dẫn đến mất thay đổi nào chưa lưu được thực hiện với tệp . Bạn có muốn tiếp tục mở lại không?", "reopen file": "Bạn có chắc muốn mở lại '{file}' không? Thay đổi nào chưa lưu sẽ bị mất.", "plugin min version": "{name} chỉ khả dụng trong Acode - {v-code} trở lên. Nhấp vào đây để cập nhật.", "color preview": "Xem trước màu", "confirm": "Xác nhận", "list files": "Liệt kê tất cả các tệp trong {name}? Quá nhiều tệp có thể làm sập ứng dụng.", "problems": "Vấn đề", "show side buttons": "Hiển thị các nút bên", "bug_report": "Gửi báo cáo lỗi", "verified publisher": "Nhà phát hành đã xác minh", "most_downloaded": "Tải xuống nhiều nhất", "newly_added": "Mới được thêm vào", "top_rated": "Đánh giá cao nhất", "rename not supported": "Đổi tên trên thư mục Termux không được hỗ trợ", "compress": "Nén lại", "copy uri": "Sao chép Uri", "delete entries": "Bạn có chắc muốn xoá {count} mục?", "deleting items": "Đang xoá {count} mục...", "import project zip": "Nhập Dự Án(zip)", "changelog": "Nhật Ký Thay Đổi", "notifications": "Thông báo", "no_unread_notifications": "Thông báo chưa đọc", "should_use_current_file_for_preview": "Nên sử dụng Tệp hiện tại để xem trước thay vì mặc định (index.html)", "fade fold widgets": "Các tiện ích gập và mờ dần", "quicktools:home-key": "Phím Home", "quicktools:end-key": "Phím End", "quicktools:pageup-key": "Phím PageUp", "quicktools:pagedown-key": "Phím PageDown", "quicktools:delete-key": "Phím Delete", "quicktools:tilde": "Chèn ký tự ngã", "quicktools:backtick": "Chèn ký tự nháy ngược", "quicktools:hash": "Chèn ký tự thăng", "quicktools:dollar": "Chèn ký tự đô la", "quicktools:modulo": "Chèn ký tự chia lấy dư/phần trăm", "quicktools:caret": "Chèn ký tự gạch dọc", "plugin_enabled": "Plugin đã bật", "plugin_disabled": "Plugin đã tắt", "enable_plugin": "Bật Plugin này", "disable_plugin": "Tắt Plugin này", "open_source": "Mã Nguồn Mở", "terminal settings": "Cài đặt Terminal", "font ligatures": "Phông Ghép Chữ", "letter spacing": "Khoảng Cách Chữ", "terminal:tab stop width": "Độ Rộng Tab", "terminal:scrollback": "Số Dòng Cuộn Ngược", "terminal:cursor blink": "Con Trỏ Nháy", "terminal:font weight": "Độ Đậm Phông", "terminal:cursor inactive style": "Kiểu Con Trỏ Bất Hoạt", "terminal:cursor style": "Kiểu Con Trỏ", "terminal:font family": "Họ phông chữ", "terminal:convert eol": "Chuyển Đổi Cuối Dòng", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "Nhà tài trợ", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/zh-cn.json ================================================ { "lang": "简体中文", "about": "关于", "active files": "打开的文件", "alert": "提醒", "app theme": "应用主题", "autocorrect": "启用自动校正?", "autosave": "自动保存", "cancel": "取消", "change language": "更改语言", "choose color": "选择颜色", "clear": "清空", "close app": "关闭应用程序?", "commit message": "提交说明", "console": "控制台", "conflict error": "冲突! 请等待其他提交完成.", "copy": "复制", "create folder error": "抱歉, 无法创建新文件夹", "cut": "剪切", "delete": "删除", "dependencies": "依赖项", "delay": "以毫秒为单位", "editor settings": "编辑器设置", "editor theme": "编辑器主题", "enter file name": "输入文件名", "enter folder name": "输入文件夹名", "empty folder message": "空文件夹", "enter line number": "输入行号", "error": "错误", "failed": "失败", "file already exists": "文件已存在", "file already exists force": "文件已存在. 是否覆盖?", "file changed": " 已发生改变, 重新加载文件?", "file deleted": "文件已删除", "file is not supported": "不支持此文件", "file not supported": "不支持此文件类型.", "file too large": "文件太大,无法处理。最大支持 {size} 的文件", "file renamed": "文件已重命名", "file saved": "文件已保存", "folder added": "添加文件夹", "folder already added": "文件夹已添加", "font size": "字体大小", "goto": "跳转至行...", "icons definition": "图标定义", "info": "信息", "invalid value": "无效值", "language changed": "语言已更改", "linting": "检查语法错误", "logout": "登出", "loading": "加载中", "my profile": "我的信息", "new file": "创建新文件", "new folder": "创建新文件夹", "no": "否", "no editor message": "从菜单打开或创建新文件和文件夹", "not set": "未设置", "unsaved files close app": "文件未保存,退出应用?", "notice": "注意", "open file": "打开文件", "open files and folders": "打开文件和文件夹", "open folder": "打开文件夹", "open recent": "最近打开", "ok": "确认", "overwrite": "覆盖", "paste": "粘贴", "preview mode": "预览模式", "read only file": "无法保存只读文件. 请尝试另存为", "reload": "重新加载", "rename": "重命名", "replace": "替换", "required": "此字段为必填字段", "run your web app": "运行你的 web 应用", "save": "保存", "saving": "保存中", "save as": "另存为", "save file to run": "请保存此文件以在浏览器中运行", "search": "搜索", "see logs and errors": "查看日志和错误", "select folder": "选择文件夹", "settings": "设置", "settings saved": "设置已保存", "show line numbers": "显示行号", "show hidden files": "显示隐藏文件", "show spaces": "突出显示空格符", "soft tab": "使用空格制表符", "sort by name": "按名称排序", "success": "成功", "tab size": "制表符宽度", "text wrap": "行末自动换行", "theme": "主题", "unable to delete file": "无法删除文件", "unable to open file": "无法打开文件", "unable to open folder": "无法打开文件夹", "unable to save file": "无法保存文件", "unable to rename": "无法重命名", "unsaved file": "此文件还未保存, 仍然关闭?", "warning": "警告", "use emmet": "使用 Emmet 语法", "use quick tools": "使用快捷工具栏", "yes": "是", "encoding": "文本编码打开为", "syntax highlighting": "语法高亮语言", "read only": "只读", "select all": "全选", "select branch": "选择分支", "create new branch": "建立新分支", "use branch": "使用分支", "new branch": "新分支", "branch": "分支", "key bindings": "快捷键", "edit": "编辑", "reset": "重置", "color": "颜色", "select word": "选择字词", "quick tools": "快捷工具栏", "select": "选择", "editor font": "编辑器字体", "new project": "创建新项目", "format": "格式化", "project name": "项目名", "unsupported device": "您的设备不支持应用主题。", "vibrate on tap": "点按时震动", "copy command is not supported by ftp.": "FTP 不支持复制操作.", "support title": "支持 Acode", "fullscreen": "全屏", "animation": "动画效果", "backup": "备份", "restore": "恢复", "backup successful": "备份成功", "invalid backup file": "备份文件无效", "add path": "添加路径", "live autocompletion": "实时自动补全", "file properties": "文件属性", "path": "路径", "type": "输入", "word count": "字数统计", "line count": "行数统计", "last modified": "最后修改", "size": "大小", "share": "分享", "show print margin": "显示打印页边距", "login": "登入", "scrollbar size": "滚动条大小", "cursor controller size": "文本光标控针大小", "none": "无", "small": "小", "large": "大", "floating button": "悬浮按钮", "confirm on exit": "退出前确认", "show console": "显示控制台", "image": "图像", "insert file": "插入文件到文件夹", "insert color": "插入颜色代码", "powersave mode warning": "关闭省电模式以在外部浏览器预览。", "exit": "退出", "custom": "个性化", "reset warning": "确定要重置主题?", "theme type": "主题类型", "light": "亮色", "dark": "深色", "file browser": "文件资源浏览器", "operation not permitted": "操作未被允许", "no such file or directory": "没有此文件或目录", "input/output error": "输入/输出错误", "permission denied": "没有权限", "bad address": "非法地址", "file exists": "文件存在", "not a directory": "非目录", "is a directory": "是目录", "invalid argument": "无效参数", "too many open files in system": "系统中打开的文件过多", "too many open files": "打开的文件过多", "text file busy": "文件正在被使用", "no space left on device": "设备空间不足", "read-only file system": "只读文件系统", "file name too long": "文件名过长", "too many users": "用户过多", "connection timed out": "连接超时", "connection refused": "连接被拒", "owner died": "管理文件的进程失效", "an error occurred": "发生错误", "add ftp": "添加 FTP", "add sftp": "添加 SFTP", "save file": "保存文件", "save file as": "另存文件为", "files": "打开文件", "help": "帮助", "file has been deleted": "{file} 已被删除!", "feature not available": "此功能仅在付费版中可用。", "deleted file": "删除的文件", "line height": "行高", "preview info": "如果想要运行打开的文件,长按 ▶ 图标", "manage all files": "允许在设置中打开 Acode 的管理所有文件权限以方便编辑此设备上的文件。", "close file": "关闭文件", "reset connections": "重置连接", "check file changes": "检查文件更改", "open in browser": "在浏览器中打开", "desktop mode": "桌面版网站", "toggle console": "打开关闭控制台", "new line mode": "换行符", "add a storage": "添加存储位置", "rate acode": "评价 Acode", "support": "支持", "downloading file": "正在下载 {file}", "downloading...": "下载中...", "folder name": "文件夹名称", "keyboard mode": "键盘模式", "normal": "正常", "app settings": "应用设置", "disable in-app-browser caching": "关闭内置浏览器缓存", "copied to clipboard": "复制到剪贴板", "remember opened files": "记住打开过的文件", "remember opened folders": "记住打开过的文件夹", "no suggestions": "不显示建议", "no suggestions aggressive": "强制不显示建议", "install": "安装", "installing": "安装中...", "plugins": "插件", "recently used": "最近使用", "update": "更新", "uninstall": "卸载", "download acode pro": "下载 Acode pro", "loading plugins": "正在加载插件", "faqs": "常见问题", "feedback": "反馈", "header": "水平标签栏", "sidebar": "垂直标签栏", "inapp": "应用内浏览器", "browser": "应用外浏览器", "diagonal scrolling": "对角线滚动", "reverse scrolling": "反转滚动方向", "formatter": "代码格式化工具", "format on save": "保存时格式化代码", "remove ads": "移除广告", "fast": "快速", "slow": "慢速", "scroll settings": "滚动设置", "scroll speed": "滚动速度", "loading...": "加载中...", "no plugins found": "没有找到插件", "name": "姓名", "username": "用户名", "optional": "可选", "hostname": "主机名", "password": "密码", "security type": "安全类型", "connection mode": "连接模式", "port": "端口", "key file": "密钥文件", "select key file": "选择密钥文件", "passphrase": "通行证", "connecting...": "连接中...", "type filename": "输入文件名", "unable to load files": "无法加载文件", "preview port": "预览端口", "find file": "查找文件", "system": "系统", "please select a formatter": "请选择一个代码格式化工具", "case sensitive": "大小写敏感", "regular expression": "正则表达式", "whole word": "整个字词", "edit with": "编辑于", "open with": "打开于", "no app found to handle this file": "没有找到能处理该文件的应用", "restore default settings": "恢复默认设置", "server port": "服务器端口", "preview settings": "网页预览设置", "preview settings note": "如果预览端口和服务器端口不同,应用将不会启动服务器,而会在浏览器或应用内浏览器中打开 https://:。这在你运行着其他服务器时会有用。", "backup/restore note": "这将只会备份你的设置、个性化主题和快捷键,而不备份你的 FTP/SFTP 和 Github 信息。", "host": "主机", "retry ftp/sftp when fail": "当 ftp/sftp 连接失败时重试", "more": "更多", "thank you :)": "感谢你的支持 :)", "purchase pending": "待购买", "cancelled": "已关闭", "local": "本地", "remote": "远程", "show console toggler": "显示打开/关闭控制台按钮", "binary file": "此文件包含二进制数据, 确定要打开它吗?", "relative line numbers": "相对行号", "elastic tabstops": "自适应的制表缩进风格", "line based rtl switching": "基于行的 RTL(从右到左) 转换", "hard wrap": "强制换行", "spellcheck": "拼写检查", "wrap method": "自动换行方法", "use textarea for ime": "使用用于输入法的文本输入框", "invalid plugin": "无效插件", "type command": "输入命令", "plugin": "插件", "quicktools trigger mode": "快捷工具栏触发模式", "print margin": "打印页边距", "touch move threshold": "触摸滑动阈值", "info-retryremotefsafterfail": "当 FTP/SFTP 连接失败时重试。", "info-fullscreen": "隐藏主屏幕的标题栏", "info-checkfiles": "当运行于后台时,检查文件变更。", "info-console": "选择 JavaScript 控制台,legacy 是默认的控制台,eruda 是一个第三方的控制台。", "info-keyboardmode": "输入文本时的键盘工作模式,不显示建议将关闭词汇建议与自动校正。如果不显示建议没有效果,可以尝试强制不显示建议。", "info-rememberfiles": "关闭时记住打开的文件。", "info-rememberfolders": "关闭时记住打开的文件夹。", "info-floatingbutton": "显示或隐藏快捷工具栏的浮动按钮。", "info-openfilelistpos": "设置打开的文件标签栏于何处。", "info-touchmovethreshold": "如果你的设备触摸灵敏度太高,可以尝试增大此值来防止误触滑动。", "info-scroll-settings": "这个设置中包括含有文本换行的滚动设置。", "info-animation": "如果感到使用卡顿,可以尝试关闭动画效果。", "info-quicktoolstriggermode": "如果快捷工具栏中的按钮不正常工作,可以尝试改变此值。", "info-checkForAppUpdates": "自动检查应用更新。", "info-quickTools": "显示或隐藏快捷工具栏。", "info-showHiddenFiles": "显示隐藏文件和文件夹。(文件名开头为 .)", "info-all_file_access": "在终端中启用对 /sdcard 和 /storage 的访问", "info-fontSize": "渲染普通文本的字体大小。", "info-fontFamily": "渲染普通文本的显示字体。", "info-theme": "终端的颜色主题。", "info-cursorStyle": "当焦点在终端时的光标显示样式。", "info-cursorInactiveStyle": "当焦点不在终端时的光标显示样式。", "info-fontWeight": "渲染非粗体文本的字体粗细。", "info-cursorBlink": "光标是否闪烁。", "info-scrollback": "终端的回滚行数。回滚是指当内容滚动超出初始视口时所保留的文本行数。", "info-tabStopWidth": "终端中制表位的宽度。", "info-letterSpacing": "字符之间的空隙像素量。", "info-imageSupport": "终端里是否支持图像。", "info-fontLigatures": "终端里是否启用字体连字(ligatures)。", "info-confirmTabClose": "关闭终端标签页前是否需要确认。", "info-backup": "创建关于终端安装的备份。", "info-restore": "恢复关于终端安装的备份。", "info-uninstall": "卸载终端安装。", "owned": "已拥有", "api_error": "API 服务器未回应,请稍后再试", "installed": "已安装", "all": "所有", "medium": "中等", "refund": "退款", "product not available": "产品不可用", "no-product-info": "该产品目前在您所在的国家不可用,请稍后再试。", "close": "关闭", "explore": "探索", "key bindings updated": "已更新快捷键", "search in files": "在所有文件中搜索", "exclude files": "排除的文件", "include files": "包含的文件", "search result": "{matches} 个结果在 {files} 个文件中。", "invalid regex": "非法的正则表达式:{message}.", "bottom": "底部", "save all": "保存所有", "close all": "关闭所有", "unsaved files warning": "存在未被保存的文件。点击‘确认’选择下一步或者点击‘取消’返回。", "save all warning": "确定要保存所有文件并关闭?该操作不可撤销。", "save all changes warning": "您确定要保存所有文件吗?", "close all warning": "确定要关闭所有文件?该操作将不保存文件更改并且不可撤销。", "refresh": "刷新", "shortcut buttons": "快捷键按钮", "no result": "无结果", "searching...": "搜索中...", "quicktools:ctrl-key": "Ctrl/Command 键", "quicktools:tab-key": "Tab 键", "quicktools:shift-key": "Shift 键", "quicktools:undo": "撤销", "quicktools:redo": "重做", "quicktools:search": "在文件中搜索", "quicktools:save": "保存文件", "quicktools:esc-key": "Escape 键", "quicktools:curlybracket": "插入花括号", "quicktools:squarebracket": "插入方括号", "quicktools:parentheses": "插入括号", "quicktools:anglebracket": "插入尖括号", "quicktools:left-arrow-key": "向左键", "quicktools:right-arrow-key": "向右键", "quicktools:up-arrow-key": "向上键", "quicktools:down-arrow-key": "向下键", "quicktools:moveline-up": "向上移行", "quicktools:moveline-down": "向下移行", "quicktools:copyline-up": "向上拷行", "quicktools:copyline-down": "向下拷行", "quicktools:semicolon": "插入分号", "quicktools:quotation": "插入引号", "quicktools:and": "插入并列符号", "quicktools:bar": "插入竖线", "quicktools:equal": "插入等于号", "quicktools:slash": "插入斜杠", "quicktools:exclamation": "插入感叹号", "quicktools:alt-key": "Alt 键", "quicktools:meta-key": "Windows/Meta 键", "info-quicktoolssettings": "个性化编辑器下边的快捷工具栏内的快捷键按钮和键盘按键以增强代码体验。", "info-excludefolders": "使用表达式 **/node_modules/** 以忽略所有 node_modules 文件夹下的文件。这将排除文件列表中列出的文件并阻止在这些文件中搜索。", "missed files": "自搜索开始后扫描了 {count} 个文件并将不会被包含在搜索中。", "remove": "移除", "quicktools:command-palette": "命令面板", "default file encoding": "默认文本编码", "remove entry": "确定要从保存的路径中移除 ‘{name}’ 吗?请注意该移除并不删除路径存在本身。", "delete entry": "确认删除:‘{name}’。该操作不可撤销。继续?", "change encoding": "确定要重新打开 ‘{file}’ 以使用 ‘{encoding}’ 编码编辑?该操作将丢失该文件所有未保存的修改。继续重新打开?", "reopen file": "确定要重新打开 ‘{file}’?所有未保存的修改将会丢失。", "plugin min version": "{name} 仅在 Acode - {v-code} 及以上版本有效。点击以更新。", "color preview": "颜色预览", "confirm": "确定", "list files": "列出 {name} 下的所有文件吗?过多的文件可能会导致应用崩溃。", "problems": "有问题", "show side buttons": "显示侧边按钮", "bug_report": "提交缺陷报告", "verified publisher": "已认证发布者", "most_downloaded": "最多下载", "newly_added": "最近上架", "top_rated": "最多好评", "rename not supported": "不支持修改 Termux 内的文件夹名", "compress": "压缩", "copy uri": "复制 Uri", "delete entries": "确定要删除这 {count} 个项目?", "deleting items": "正在删除这 {count} 个项目...", "import project zip": "导入项目(zip)", "changelog": "更新日志", "notifications": "消息通知", "no_unread_notifications": "没有未读消息", "should_use_current_file_for_preview": "应使当前文件用于预览页面而非使用默认文件 (index.html)", "fade fold widgets": "淡入淡出代码折叠按钮", "quicktools:home-key": "Home 键", "quicktools:end-key": "End 键", "quicktools:pageup-key": "PageUp 键", "quicktools:pagedown-key": "PageDown 键", "quicktools:delete-key": "Delete 键", "quicktools:tilde": "插入波浪号", "quicktools:backtick": "插入反引号", "quicktools:hash": "插入井号", "quicktools:dollar": "插入美元符号", "quicktools:modulo": "插入取模/百分比符号", "quicktools:caret": "插入脱字符", "plugin_enabled": "插件已启用", "plugin_disabled": "插件未启用", "enable_plugin": "启用此插件", "disable_plugin": "关闭此插件", "open_source": "开源", "terminal settings": "终端设置", "font ligatures": "字体连字", "letter spacing": "字母间距", "terminal:tab stop width": "Tab 停靠宽度", "terminal:scrollback": "终端回溯行数", "terminal:cursor blink": "光标闪烁", "terminal:font weight": "字体粗细", "terminal:cursor inactive style": "光标无活动时样式", "terminal:cursor style": "光标样式", "terminal:font family": "字体", "terminal:convert eol": "转换行尾符", "terminal:confirm tab close": "确认关闭终端标签页", "terminal:image support": "图像支持", "terminal": "终端", "allFileAccess": "所有文件读写权限", "fonts": "字体", "sponsor": "赞助", "downloads": "下载量", "reviews": "评价", "overview": "概览", "contributors": "贡献者", "quicktools:hyphen": "插入连字符", "check for app updates": "检查应用更新", "prompt update check consent message": "Acode 能够在有网时检查更新。启用更新检查?", "keywords": "关键字", "author": "作者", "filtered by": "过滤条件", "clean install state": "清除安装状态", "backup created": "备份已创建", "restore completed": "恢复已完成", "restore will include": "将会恢复", "restore warning": "该操作不可撤销,是否继续?", "reload to apply": "重新加载以应用更改?", "reload app": "重新加载应用", "preparing backup": "正在准备备份", "collecting settings": "正在收集设置", "collecting key bindings": "正在收集按键绑定", "collecting plugins": "正在收集插件信息", "creating backup": "正在创建备份文件", "validating backup": "正在验证备份", "restoring key bindings": "正在恢复按键绑定", "restoring plugins": "正在恢复插件", "restoring settings": "正在恢复设置", "legacy backup warning": "这是旧版的备份格式,可能不支持部分功能或支持有限。", "checksum mismatch": "校验码不匹配 - 备份文件可能被修改过或已损坏。", "plugin not found": "在注册表中未找到插件", "paid plugin skipped": "付费插件 - 未找到付费记录", "source not found": "源文件已不存在", "restored": "已恢复", "skipped": "已跳过", "backup not valid object": "备份文件不是一个有效对象", "backup no data": "备份文件没有需要恢复的数据", "backup legacy warning": "这是旧版备份格式(v1),可能不支持部分功能或支持有限。", "backup missing metadata": "缺失备份元数据 - 部分信息可能不可用", "backup checksum mismatch": "校验码不匹配 - 备份文件可能被修改过或已损坏。请谨慎操作。", "backup checksum verify failed": "无法验证校验码", "backup invalid settings": "无效的设置格式", "backup invalid keybindings": "无效的按键绑定格式", "backup invalid plugins": "无效的已安装插件格式", "issues found": "出现问题", "error details": "详细错误信息", "active tools": "已启用工具", "available tools": "可用的工具", "recent": "最近文件", "command palette": "打开命令面板", "change theme": "更改主题", "documentation": "文档", "open in terminal": "在终端中打开", "developer mode": "开发者模式", "info-developermode": "启用开发者工具 (Eruda),用于调试插件和检查应用状态。检查器将在应用启动时初始化。", "developer mode enabled": "开发者模式已启用。使用命令面板切换检查器 (Ctrl+Shift+I)。", "developer mode disabled": "开发者模式已禁用", "copy relative path": "复制相对路径", "shortcut request sent": "快捷方式请求已发送。点击“添加”完成操作。", "add to home screen": "添加到主屏幕", "pin shortcuts not supported": "此设备不支持主屏幕快捷方式。", "save file before home shortcut": "请先保存文件,再添加到主屏幕。", "terminal_required_message_for_lsp": "未安装终端。请先安装终端以使用 LSP 服务器。", "shift click selection": "Shift + 点击选择", "earn ad-free time": "获取免广告时间", "indent guides": "缩进指示线", "language servers": "语言服务器", "lint gutter": "显示 Lint 标记栏", "rainbow brackets": "彩虹括号", "lsp-add-custom-server": "添加自定义服务器", "lsp-binary-args": "可执行参数(JSON 数组)", "lsp-binary-command": "可执行命令", "lsp-binary-path-optional": "可执行路径(可选)", "lsp-check-command-optional": "检查命令(可选覆盖)", "lsp-checking-installation-status": "正在检查安装状态…", "lsp-configured": "已配置", "lsp-custom-server-added": "已添加自定义服务器", "lsp-default": "默认", "lsp-details-line": "详情:{details}", "lsp-edit-initialization-options": "编辑初始化选项", "lsp-empty": "空", "lsp-enabled": "已启用", "lsp-error-add-server-failed": "添加服务器失败", "lsp-error-args-must-be-array": "参数必须是 JSON 数组", "lsp-error-binary-command-required": "必须填写可执行命令", "lsp-error-language-id-required": "至少需要一个语言 ID", "lsp-error-package-required": "至少需要一个包", "lsp-error-server-id-required": "必须填写服务器 ID", "lsp-feature-completion": "代码补全", "lsp-feature-completion-info": "启用来自服务器的自动补全建议。", "lsp-feature-diagnostics": "诊断", "lsp-feature-diagnostics-info": "显示语言服务器返回的错误和警告。", "lsp-feature-formatting": "格式化", "lsp-feature-formatting-info": "启用语言服务器提供的代码格式化。", "lsp-feature-hover": "悬停信息", "lsp-feature-hover-info": "悬停时显示类型信息和文档。", "lsp-feature-inlay-hints": "内联提示", "lsp-feature-inlay-hints-info": "在编辑器中显示类型提示。", "lsp-feature-signature": "函数签名提示", "lsp-feature-signature-info": "输入时显示函数参数提示。", "lsp-feature-state-toast": "{feature} 已{state}", "lsp-initialization-options": "初始化选项", "lsp-initialization-options-json": "初始化选项(JSON)", "lsp-initialization-options-updated": "初始化选项已更新", "lsp-install-command": "安装命令", "lsp-install-command-unavailable": "安装命令不可用", "lsp-install-info-check-failed": "无法验证安装状态。", "lsp-install-info-missing": "语言服务器未安装在终端环境中。", "lsp-install-info-ready": "语言服务器已安装并可用。", "lsp-install-info-unknown": "无法自动检查安装状态。", "lsp-install-info-version-available": "可用版本:{version}", "lsp-install-method-apk": "APK 包", "lsp-install-method-cargo": "Cargo 包", "lsp-install-method-manual": "手动二进制", "lsp-install-method-npm": "npm 包", "lsp-install-method-pip": "pip 包", "lsp-install-method-shell": "自定义 Shell", "lsp-install-method-title": "安装方式", "lsp-install-repair": "安装 / 修复", "lsp-installation-status": "安装状态", "lsp-installed": "已安装", "lsp-invalid-timeout": "无效的超时值", "lsp-language-ids": "语言 ID(逗号分隔)", "lsp-packages-prompt": "{method} 包(逗号分隔)", "lsp-remove-installed-files": "移除 {server} 的已安装文件?", "lsp-server-disabled-toast": "服务器已禁用", "lsp-server-enabled-toast": "服务器已启用", "lsp-server-id": "服务器 ID", "lsp-server-label": "服务器标签", "lsp-server-not-found": "未找到服务器", "lsp-server-uninstalled": "服务器已卸载", "lsp-startup-timeout": "启动超时", "lsp-startup-timeout-ms": "启动超时(毫秒)", "lsp-startup-timeout-set": "启动超时已设置为 {timeout} ms", "lsp-state-disabled": "禁用", "lsp-state-enabled": "启用", "lsp-status-check-failed": "检查失败", "lsp-status-installed": "已安装", "lsp-status-installed-version": "已安装({version})", "lsp-status-line": "状态:{status}", "lsp-status-not-installed": "未安装", "lsp-status-unknown": "未知", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "卸载命令不可用", "lsp-uninstall-server": "卸载服务器", "lsp-update-command-optional": "更新命令(可选)", "lsp-update-command-unavailable": "更新命令不可用", "lsp-update-server": "更新服务器", "lsp-version-line": "版本:{version}", "lsp-view-initialization-options": "查看初始化选项", "settings-category-about-acode": "关于 Acode", "settings-category-advanced": "高级", "settings-category-assistance": "辅助", "settings-category-core": "核心设置", "settings-category-cursor": "光标", "settings-category-cursor-selection": "光标与选区", "settings-category-custom-servers": "自定义服务器", "settings-category-customization-tools": "自定义与工具", "settings-category-display": "显示", "settings-category-editing": "编辑", "settings-category-features": "功能", "settings-category-files-sessions": "文件与会话", "settings-category-fonts": "字体", "settings-category-general": "通用", "settings-category-guides-indicators": "指示线与标记", "settings-category-installation": "安装", "settings-category-interface": "界面", "settings-category-maintenance": "维护", "settings-category-permissions": "权限", "settings-category-preview": "预览", "settings-category-scrolling": "滚动", "settings-category-server": "服务器", "settings-category-servers": "服务器", "settings-category-session": "会话", "settings-category-support-acode": "支持 Acode", "settings-category-text-layout": "文本与布局", "settings-info-app-animation": "控制应用内的过渡动画。", "settings-info-app-check-files": "当文件在外部被修改时刷新编辑器。", "settings-info-app-clean-install-state": "清除安装流程使用的存储状态。", "settings-info-app-confirm-on-exit": "退出应用前进行确认。", "settings-info-app-console": "选择 Acode 使用的调试控制台集成方式。", "settings-info-app-default-file-encoding": "打开或创建文件时的默认编码。", "settings-info-app-exclude-folders": "搜索或扫描时跳过指定文件夹或模式。", "settings-info-app-floating-button": "显示悬浮快捷按钮。", "settings-info-app-font-manager": "安装、管理或移除应用字体。", "settings-info-app-fullscreen": "编辑时隐藏系统状态栏。", "settings-info-app-keybindings": "编辑快捷键文件或重置快捷键。", "settings-info-app-keyboard-mode": "选择软件键盘的编辑行为。", "settings-info-app-language": "选择应用语言和翻译标签。", "settings-info-app-open-file-list-position": "选择活动文件列表的位置。", "settings-info-app-quick-tools-settings": "自定义和排序快捷工具。", "settings-info-app-quick-tools-trigger-mode": "选择快捷工具的触发方式。", "settings-info-app-remember-files": "重新打开上次编辑的文件。", "settings-info-app-remember-folders": "重新打开上次使用的文件夹。", "settings-info-app-retry-remote-fs": "远程文件传输失败后自动重试。", "settings-info-app-side-buttons": "在编辑器旁显示额外操作按钮。", "settings-info-app-sponsor-sidebar": "在侧边栏显示赞助入口。", "settings-info-app-touch-move-threshold": "触摸拖动的最小移动距离。", "settings-info-app-vibrate-on-tap": "启用触控震动反馈。", "settings-info-editor-autosave": "延迟后自动保存更改。", "settings-info-editor-color-preview": "在编辑器中预览颜色值。", "settings-info-editor-fade-fold-widgets": "折叠标记在非活动时变暗。", "settings-info-editor-font-family": "选择编辑器字体。", "settings-info-editor-font-size": "设置编辑器字体大小。", "settings-info-editor-format-on-save": "保存文件时自动格式化。", "settings-info-editor-hard-wrap": "插入真实换行,而不仅是视觉换行。", "settings-info-editor-indent-guides": "显示缩进指示线。", "settings-info-editor-line-height": "调整行间距。", "settings-info-editor-line-numbers": "在边栏显示行号。", "settings-info-editor-lint-gutter": "在边栏显示诊断和 Lint 标记。", "settings-info-editor-live-autocomplete": "输入时显示自动补全建议。", "settings-info-editor-rainbow-brackets": "按嵌套深度为括号着色。", "settings-info-editor-relative-line-numbers": "显示相对行号。", "settings-info-editor-rtl-text": "按行切换从右到左文本行为。", "settings-info-editor-scroll-settings": "调整滚动条大小、速度和手势行为。", "settings-info-editor-shift-click-selection": "使用 Shift + 点击扩展选区。", "settings-info-editor-show-spaces": "显示空白字符标记。", "settings-info-editor-soft-tab": "使用空格代替 Tab 字符。", "settings-info-editor-tab-size": "设置 Tab 等效空格数。", "settings-info-editor-teardrop-size": "设置触控编辑的光标拖动手柄大小。", "settings-info-editor-text-wrap": "在编辑器中自动换行长行。", "settings-info-lsp-add-custom-server": "注册自定义语言服务器,包括安装、更新和启动命令。", "settings-info-lsp-edit-init-options": "以 JSON 形式编辑初始化选项。", "settings-info-lsp-install-server": "安装或修复此语言服务器。", "settings-info-lsp-server-enabled": "启用或禁用此语言服务器。", "settings-info-lsp-startup-timeout": "设置语言服务器启动等待时间。", "settings-info-lsp-uninstall-server": "移除此服务器的已安装包或二进制文件。", "settings-info-lsp-update-server": "如果可用,更新此语言服务器。", "settings-info-lsp-view-init-options": "以 JSON 查看有效的初始化选项。", "settings-info-main-ad-rewards": "观看广告以解锁临时免广告。", "settings-info-main-app-settings": "语言、应用行为和快捷工具。", "settings-info-main-backup-restore": "导出或恢复设置备份。", "settings-info-main-changelog": "查看最近更新和发布说明。", "settings-info-main-edit-settings": "直接编辑 settings.json。", "settings-info-main-editor-settings": "字体、Tab、建议和编辑器显示。", "settings-info-main-formatter": "为每种语言选择格式化器。", "settings-info-main-lsp-settings": "配置语言服务器和智能编辑功能。", "settings-info-main-plugins": "管理已安装插件及其操作。", "settings-info-main-preview-settings": "预览模式、端口和浏览器行为。", "settings-info-main-rateapp": "在 Google Play 上评价 Acode。", "settings-info-main-remove-ads": "解锁永久免广告。", "settings-info-main-reset": "将 Acode 重置为默认配置。", "settings-info-main-sponsors": "支持 Acode 的持续开发。", "settings-info-main-terminal-settings": "终端主题、字体、光标和会话行为。", "settings-info-main-theme": "应用主题、对比度和自定义颜色。", "settings-info-preview-disable-cache": "在预览中始终重新加载内容。", "settings-info-preview-host": "打开预览 URL 时使用的主机名。", "settings-info-preview-mode": "选择预览的打开方式。", "settings-info-preview-preview-port": "实时预览服务器使用的端口。", "settings-info-preview-server-port": "内部应用服务器使用的端口。", "settings-info-preview-show-console-toggler": "在预览中显示控制台按钮。", "settings-info-preview-use-current-file": "启动预览时优先使用当前文件。", "settings-info-terminal-convert-eol": "粘贴或渲染终端输出时转换行结尾。", "settings-note-formatter-settings": "为每种语言分配格式化器。安装格式化插件可解锁更多选项。", "settings-note-lsp-settings": "语言服务器提供补全、诊断、悬停等功能。你可以在此安装、更新或定义自定义服务器。托管安装程序在终端/proot 环境中运行。", "search result label singular": "条结果", "search result label plural": "条结果", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/zh-hant.json ================================================ { "lang": "繁體中文", "about": "關於", "active files": "打開的文件", "alert": "提醒", "app theme": "應用主題", "autocorrect": "啟用自動校正?", "autosave": "自動保存", "cancel": "取消", "change language": "更改語言", "choose color": "選擇顏色", "clear": "清空", "close app": "關閉應用程序?", "commit message": "提交說明", "console": "控製臺", "conflict error": "沖突! 請等待其他提交完成.", "copy": "復製", "create folder error": "抱歉, 無法創建新文件夾", "cut": "剪切", "delete": "刪除", "dependencies": "依賴項", "delay": "以毫秒為單位", "editor settings": "編輯器設置", "editor theme": "編輯器主題", "enter file name": "輸入文件名", "enter folder name": "輸入文件夾名", "empty folder message": "空文件夾", "enter line number": "輸入行號", "error": "錯誤", "failed": "失敗", "file already exists": "文件已存在", "file already exists force": "文件已存在. 是否覆蓋?", "file changed": " 已發生改變, 重新加載文件?", "file deleted": "文件已刪除", "file is not supported": "不支持此文件", "file not supported": "不支持此文件類型.", "file too large": "文件太大,無法處理。最大支持 {size} 的文件", "file renamed": "文件已重命名", "file saved": "文件已保存", "folder added": "添加文件夾", "folder already added": "文件夾已添加", "font size": "字體大小", "goto": "跳轉至行...", "icons definition": "圖標定義", "info": "信息", "invalid value": "無效值", "language changed": "語言已更改", "linting": "檢查語法錯誤", "logout": "登出", "loading": "加載中", "my profile": "我的信息", "new file": "創建新文件", "new folder": "創建新文件夾", "no": "否", "no editor message": "從菜單打開或創建新文件和文件夾", "not set": "未設置", "unsaved files close app": "文件未保存,退出應用?", "notice": "註意", "open file": "打開文件", "open files and folders": "打開文件和文件夾", "open folder": "打開文件夾", "open recent": "最近打開", "ok": "確認", "overwrite": "覆蓋", "paste": "粘貼", "preview mode": "預覽模式", "read only file": "無法保存只讀文件. 請嘗試另存為", "reload": "重新加載", "rename": "重命名", "replace": "替換", "required": "此字段為必填字段", "run your web app": "運行你的 web 應用", "save": "保存", "saving": "保存中", "save as": "另存為", "save file to run": "請保存此文件以在瀏覽器中運行", "search": "搜索", "see logs and errors": "查看日誌和錯誤", "select folder": "選擇文件夾", "settings": "設置", "settings saved": "設置已保存", "show line numbers": "顯示行號", "show hidden files": "顯示隱藏文件", "show spaces": "突出顯示空格符", "soft tab": "使用空格製表符", "sort by name": "按名稱排序", "success": "成功", "tab size": "製表符寬度", "text wrap": "行末自動換行", "theme": "主題", "unable to delete file": "無法刪除文件", "unable to open file": "無法打開文件", "unable to open folder": "無法打開文件夾", "unable to save file": "無法保存文件", "unable to rename": "無法重命名", "unsaved file": "此文件還未保存, 仍然關閉?", "warning": "警告", "use emmet": "使用 Emmet 語法", "use quick tools": "使用快捷工具欄", "yes": "是", "encoding": "文本編碼打開為", "syntax highlighting": "語法高亮語言", "read only": "只讀", "select all": "全選", "select branch": "選擇分支", "create new branch": "建立新分支", "use branch": "使用分支", "new branch": "新分支", "branch": "分支", "key bindings": "快捷鍵", "edit": "編輯", "reset": "重置", "color": "顏色", "select word": "選擇字詞", "quick tools": "快捷工具欄", "select": "選擇", "editor font": "編輯器字體", "new project": "創建新項目", "format": "格式化", "project name": "項目名", "unsupported device": "您的設備不支持應用主題。", "vibrate on tap": "點按時震動", "copy command is not supported by ftp.": "FTP 不支持復製操作.", "support title": "支持 Acode", "fullscreen": "全屏", "animation": "動畫效果", "backup": "備份", "restore": "恢復", "backup successful": "備份成功", "invalid backup file": "備份文件無效", "add path": "添加路徑", "live autocompletion": "實時自動補全", "file properties": "文件屬性", "path": "路徑", "type": "輸入", "word count": "字數統計", "line count": "行數統計", "last modified": "最後修改", "size": "大小", "share": "分享", "show print margin": "顯示打印頁邊距", "login": "登入", "scrollbar size": "滾動條大小", "cursor controller size": "文本光標控針大小", "none": "無", "small": "小", "large": "大", "floating button": "懸浮按鈕", "confirm on exit": "退出前確認", "show console": "顯示控製臺", "image": "圖像", "insert file": "插入文件到文件夾", "insert color": "插入顏色代碼", "powersave mode warning": "關閉省電模式以在外部瀏覽器預覽。", "exit": "退出", "custom": "個性化", "reset warning": "確定要重置主題?", "theme type": "主題類型", "light": "亮色", "dark": "深色", "file browser": "文件資源瀏覽器", "operation not permitted": "操作未被允許", "no such file or directory": "沒有此文件或目錄", "input/output error": "輸入/輸出錯誤", "permission denied": "沒有權限", "bad address": "非法地址", "file exists": "文件存在", "not a directory": "非目錄", "is a directory": "是目錄", "invalid argument": "無效參數", "too many open files in system": "系統中打開的文件過多", "too many open files": "打開的文件過多", "text file busy": "文件正在被使用", "no space left on device": "設備空間不足", "read-only file system": "只讀文件系統", "file name too long": "文件名過長", "too many users": "用戶過多", "connection timed out": "連接超時", "connection refused": "連接被拒", "owner died": "管理文件的進程失效", "an error occurred": "發生錯誤", "add ftp": "添加 FTP", "add sftp": "添加 SFTP", "save file": "保存文件", "save file as": "另存文件為", "files": "打開文件", "help": "幫助", "file has been deleted": "{file} 已被刪除!", "feature not available": "此功能僅在付費版中可用。", "deleted file": "刪除的文件", "line height": "行高", "preview info": "如果想要運行打開的文件,長按 ▶ 圖標", "manage all files": "允許在設置中打開 Acode 的管理所有文件權限以方便編輯此設備上的文件。", "close file": "關閉文件", "reset connections": "重置連接", "check file changes": "檢查文件更改", "open in browser": "在瀏覽器中打開", "desktop mode": "桌面版網站", "toggle console": "打開關閉控製臺", "new line mode": "換行符", "add a storage": "添加存儲位置", "rate acode": "評價 Acode", "support": "支持", "downloading file": "正在下載 {file}", "downloading...": "下載中...", "folder name": "文件夾名稱", "keyboard mode": "鍵盤模式", "normal": "正常", "app settings": "應用設置", "disable in-app-browser caching": "關閉內置瀏覽器緩存", "copied to clipboard": "復製到剪貼板", "remember opened files": "記住打開過的文件", "remember opened folders": "記住打開過的文件夾", "no suggestions": "不顯示建議", "no suggestions aggressive": "強製不顯示建議", "install": "安裝", "installing": "安裝中...", "plugins": "插件", "recently used": "最近使用", "update": "更新", "uninstall": "卸載", "download acode pro": "下載 Acode pro", "loading plugins": "正在加載插件", "faqs": "常見問題", "feedback": "反饋", "header": "水平標簽欄", "sidebar": "垂直標簽欄", "inapp": "應用內瀏覽器", "browser": "應用外瀏覽器", "diagonal scrolling": "對角線滾動", "reverse scrolling": "反轉滾動方向", "formatter": "代碼格式化工具", "format on save": "保存時格式化代碼", "remove ads": "移除廣告", "fast": "快速", "slow": "慢速", "scroll settings": "滾動設置", "scroll speed": "滾動速度", "loading...": "加載中...", "no plugins found": "沒有找到插件", "name": "姓名", "username": "用戶名", "optional": "可選", "hostname": "主機名", "password": "密碼", "security type": "安全類型", "connection mode": "連接模式", "port": "端口", "key file": "密鑰文件", "select key file": "選擇密鑰文件", "passphrase": "通行證", "connecting...": "連接中...", "type filename": "輸入文件名", "unable to load files": "無法加載文件", "preview port": "預覽端口", "find file": "查找文件", "system": "系統", "please select a formatter": "請選擇一個代碼格式化工具", "case sensitive": "大小寫敏感", "regular expression": "正則表達式", "whole word": "整個字詞", "edit with": "編輯於", "open with": "打開於", "no app found to handle this file": "沒有找到能處理該文件的應用", "restore default settings": "恢復默認設置", "server port": "服務器端口", "preview settings": "網頁預覽設置", "preview settings note": "如果預覽端口和服務器端口不同,應用將不會啟動服務器,而會在瀏覽器或應用內瀏覽器中打開 https://:。這在你運行著其他服務器時會有用。", "backup/restore note": "這將只會備份你的設置、個性化主題和快捷鍵,而不備份你的 FTP/SFTP 和 Github 信息。", "host": "主機", "retry ftp/sftp when fail": "當 ftp/sftp 連接失敗時重試", "more": "更多", "thank you :)": "感謝你的支持 :)", "purchase pending": "待購買", "cancelled": "已關閉", "local": "本地", "remote": "遠程", "show console toggler": "顯示打開/關閉控製臺按鈕", "binary file": "此文件包含二進製數據, 確定要打開它嗎?", "relative line numbers": "相對行號", "elastic tabstops": "自適應的製表縮進風格", "line based rtl switching": "基於行的 RTL(從右到左) 轉換", "hard wrap": "強製換行", "spellcheck": "拼寫檢查", "wrap method": "自動換行方法", "use textarea for ime": "使用用於輸入法的文本輸入框", "invalid plugin": "無效插件", "type command": "輸入命令", "plugin": "插件", "quicktools trigger mode": "快捷工具欄觸發模式", "print margin": "打印頁邊距", "touch move threshold": "觸摸滑動閾值", "info-retryremotefsafterfail": "當 FTP/SFTP 連接失敗時重試。", "info-fullscreen": "隱藏主屏幕的標題欄", "info-checkfiles": "當運行於後臺時,檢查文件變更。", "info-console": "選擇 JavaScript 控製臺,legacy 是默認的控製臺,eruda 是一個第三方的控製臺。", "info-keyboardmode": "輸入文本時的鍵盤工作模式,不顯示建議將關閉詞匯建議與自動校正。如果不顯示建議沒有效果,可以嘗試強製不顯示建議。", "info-rememberfiles": "關閉時記住打開的文件。", "info-rememberfolders": "關閉時記住打開的文件夾。", "info-floatingbutton": "顯示或隱藏快捷工具欄的浮動按鈕。", "info-openfilelistpos": "設置打開的文件標簽欄於何處。", "info-touchmovethreshold": "如果你的設備觸摸靈敏度太高,可以嘗試增大此值來防止誤觸滑動。", "info-scroll-settings": "這個設置中包括含有文本換行的滾動設置。", "info-animation": "如果感到使用卡頓,可以嘗試關閉動畫效果。", "info-quicktoolstriggermode": "如果快捷工具欄中的按鈕不正常工作,可以嘗試改變此值。", "info-checkForAppUpdates": "自動檢查應用更新。", "info-quickTools": "顯示或隱藏快捷工具欄。", "info-showHiddenFiles": "顯示隱藏文件和文件夾。(文件名開頭為 .)", "info-all_file_access": "在終端中啟用對 /sdcard 和 /storage 的訪問", "info-fontSize": "渲染普通文本的字體大小。", "info-fontFamily": "渲染普通文本的顯示字體。", "info-theme": "終端的顏色主題。", "info-cursorStyle": "當焦點在終端時的游標顯示樣式。", "info-cursorInactiveStyle": "當焦點不在終端時的游標顯示樣式。", "info-fontWeight": "渲染非粗體文字的字體粗細。", "info-cursorBlink": "游標是否閃爍。", "info-scrollback": "終端的回滾行數。回滾是指當內容滾動超出初始視口時所保留的文字行數。", "info-tabStopWidth": "終端中製表位的寬度。", "info-letterSpacing": "字符之間的空隙像素量。", "info-imageSupport": "終端裏是否支持圖像。", "info-fontLigatures": "終端裏是否啟用字體連字(ligatures)。", "info-confirmTabClose": "關閉終端分頁前是否需要確認。", "info-backup": "創建關於終端安裝的備份。", "info-restore": "恢復關於終端安裝的備份。", "info-uninstall": "卸載終端安裝。", "owned": "已擁有", "api_error": "API 服務器未回應,請稍後再試", "installed": "已安裝", "all": "所有", "medium": "中等", "refund": "退款", "product not available": "產品不可用", "no-product-info": "該產品目前在您所在的國家不可用,請稍後再試。", "close": "關閉", "explore": "探索", "key bindings updated": "已更新快捷鍵", "search in files": "在所有文件中搜索", "exclude files": "排除的文件", "include files": "包含的文件", "search result": "{matches} 個結果在 {files} 個文件中。", "invalid regex": "非法的正則表達式:{message}.", "bottom": "底部", "save all": "保存所有", "close all": "關閉所有", "unsaved files warning": "存在未被保存的文件。點擊『確認』選擇下一步或者點擊『取消』返回。", "save all warning": "確定要保存所有文件並關閉?該操作不可撤銷。", "save all changes warning": "確定要保存所有檔案並關閉?該操作不可撤銷。", "close all warning": "確定要關閉所有文件?該操作將不保存文件更改並且不可撤銷。", "refresh": "刷新", "shortcut buttons": "快捷鍵按鈕", "no result": "無結果", "searching...": "搜索中...", "quicktools:ctrl-key": "Control/Command 鍵", "quicktools:tab-key": "Tab 鍵", "quicktools:shift-key": "Shift 鍵", "quicktools:undo": "撤銷", "quicktools:redo": "重做", "quicktools:search": "在文件中搜索", "quicktools:save": "保存文件", "quicktools:esc-key": "Escape 鍵", "quicktools:curlybracket": "插入花括號", "quicktools:squarebracket": "插入方括號", "quicktools:parentheses": "插入括號", "quicktools:anglebracket": "插入尖括號", "quicktools:left-arrow-key": "向左鍵", "quicktools:right-arrow-key": "向右鍵", "quicktools:up-arrow-key": "向上鍵", "quicktools:down-arrow-key": "向下鍵", "quicktools:moveline-up": "向上移行", "quicktools:moveline-down": "向下移行", "quicktools:copyline-up": "向上拷行", "quicktools:copyline-down": "向下拷行", "quicktools:semicolon": "插入分號", "quicktools:quotation": "插入引號", "quicktools:and": "插入並列符號", "quicktools:bar": "插入豎線", "quicktools:equal": "插入等於號", "quicktools:slash": "插入斜槓", "quicktools:exclamation": "插入感嘆號", "quicktools:alt-key": "Alt 鍵", "quicktools:meta-key": "Windows/Meta 鍵", "info-quicktoolssettings": "個性化編輯器下邊的快捷工具欄內的快捷鍵按鈕和鍵盤按鍵以增強代碼體驗。", "info-excludefolders": "使用表達式 **/node_modules/** 以忽略所有 node_modules 文件夾下的文件。這將排除文件列表中列出的文件並阻止在這些文件中搜索。", "missed files": "自搜索開始後掃描了 {count} 個文件並將不會被包含在搜索中。", "remove": "移除", "quicktools:command-palette": "命令面板", "default file encoding": "默認文本編碼", "remove entry": "確定要從保存的路徑中移除 ‘{name}’ 嗎?請注意該移除並不刪除路徑存在本身。", "delete entry": "確認刪除:‘{name}’。該操作不可撤銷。繼續?", "change encoding": "確定要重新打開 ‘{file}’ 以使用 ‘{encoding}’ 編碼編輯?該操作將丟失該文件所有未保存的修改。繼續重新打開?", "reopen file": "確定要重新打開 ‘{file}’?所有未保存的修改將會丟失。", "plugin min version": "{name} 僅在 Acode - {v-code} 及以上版本有效。點擊以更新。", "color preview": "顏色預覽", "confirm": "確定", "list files": "列出 {name} 下的所有文件嗎?過多的文件可能會導緻應用崩潰。", "problems": "有問題", "show side buttons": "顯示側邊按鈕", "bug_report": "提交缺陷報告", "verified publisher": "已認證發布者", "most_downloaded": "最多下載", "newly_added": "最近上架", "top_rated": "最多好評", "rename not supported": "不支持修改 Termux 內的文件夾名", "compress": "壓縮", "copy uri": "複製 Uri", "delete entries": "確定要刪除這 {count} 個項目?", "deleting items": "正在刪除這 {count} 個項目...", "import project zip": "導入項目(zip)", "changelog": "更新日誌", "notifications": "消息通知", "no_unread_notifications": "沒有未讀消息", "should_use_current_file_for_preview": "應使當前文件用於預覽頁面而非使用默認文件 (index.html)", "fade fold widgets": "淡入淡出代碼折疊按鈕", "quicktools:home-key": "Home 鍵", "quicktools:end-key": "End 鍵", "quicktools:pageup-key": "PageUp 鍵", "quicktools:pagedown-key": "PageDown 鍵", "quicktools:delete-key": "Delete 鍵", "quicktools:tilde": "插入波浪號", "quicktools:backtick": "插入反引號", "quicktools:hash": "插入井號", "quicktools:dollar": "插入美元符號", "quicktools:modulo": "插入取模/百分比符號", "quicktools:caret": "插入脫字符", "plugin_enabled": "插件已啟用", "plugin_disabled": "插件未啟用", "enable_plugin": "啟用此插件", "disable_plugin": "關閉此插件", "open_source": "開源", "terminal settings": "終端機設定", "font ligatures": "字體連字", "letter spacing": "字母間距", "terminal:tab stop width": "Tab 停靠寬度", "terminal:scrollback": "終端回溯行數", "terminal:cursor blink": "游標閃爍", "terminal:font weight": "字體粗細", "terminal:cursor inactive style": "游標非活動時樣式", "terminal:cursor style": "游標樣式", "terminal:font family": "字體", "terminal:convert eol": "轉換行尾符", "terminal:confirm tab close": "確認關閉終端分頁", "terminal:image support": "圖像支持", "terminal": "終端機", "allFileAccess": "所有文件讀寫權限", "fonts": "字體", "sponsor": "贊助", "downloads": "下載量", "reviews": "評價", "overview": "總覽", "contributors": "貢獻者", "quicktools:hyphen": "插入連字元", "check for app updates": "檢查應用程式更新", "prompt update check consent message": "Acode 能夠在有網時檢查更新。啟用更新檢查?", "keywords": "關鍵字", "author": "作者", "filtered by": "篩選條件", "clean install state": "清除安裝狀態", "backup created": "備份已創建", "restore completed": "恢復已完成", "restore will include": "將會恢復", "restore warning": "該操作不可撤銷,是否繼續?", "reload to apply": "重新載入以套用變更?", "reload app": "重新載入應用", "preparing backup": "正在準備備份", "collecting settings": "正在收集設置", "collecting key bindings": "正在收集按鍵綁定", "collecting plugins": "正在收集插件資訊", "creating backup": "正在創建備份文件", "validating backup": "正在驗證備份", "restoring key bindings": "正在恢復按鍵綁定", "restoring plugins": "正在恢復插件", "restoring settings": "正在恢復設置", "legacy backup warning": "這是舊版的備份格式,可能不支持部分功能或支持有限。", "checksum mismatch": "校驗碼不匹配 - 備份文件可能被修改過或已損壞。", "plugin not found": "在註冊表中未找到插件", "paid plugin skipped": "付費插件 - 未找到付費記錄", "source not found": "源文件已不存在", "restored": "已恢復", "skipped": "已跳過", "backup not valid object": "備份文件不是一個有效對象", "backup no data": "備份文件沒有需要恢復的數據", "backup legacy warning": "這是舊版備份格式(v1),可能不支持部分功能或支持有限。", "backup missing metadata": "缺失備份元資料 - 部分資訊可能無法使用", "backup checksum mismatch": "校驗碼不匹配 - 備份文件可能被修改過或已損壞。請謹慎操作。", "backup checksum verify failed": "無法驗證校驗碼", "backup invalid settings": "無效的設定格式", "backup invalid keybindings": "無效的按鍵綁定格式", "backup invalid plugins": "無效的已安裝插件格式", "issues found": "發現問題", "error details": "詳細錯誤資訊", "active tools": "已啟用工具", "available tools": "可用的工具", "recent": "最近檔案", "command palette": "打開命令面板", "change theme": "更改主題", "documentation": "文件", "open in terminal": "在終端中打開", "developer mode": "開發者模式", "info-developermode": "啟用開發者工具 (Eruda),用於調試插件和檢查應用狀態。檢查器將在應用啟動時初始化。", "developer mode enabled": "開發者模式已啟用。使用命令面板切換檢查器 (Ctrl+Shift+I)。", "developer mode disabled": "開發者模式已禁用", "copy relative path": "複製相對路徑", "shortcut request sent": "快捷方式請求已發送。點擊「添加」完成操作。", "add to home screen": "添加到主屏幕", "pin shortcuts not supported": "此設備不支持主屏幕快捷方式。", "save file before home shortcut": "請先保存文件,再添加到主屏幕。", "terminal_required_message_for_lsp": "未安裝終端。請先安裝終端以使用 LSP 服務器。", "shift click selection": "Shift + 點擊選擇", "earn ad-free time": "獲取免廣告時間", "indent guides": "縮進指示線", "language servers": "語言服務器", "lint gutter": "顯示 Lint 標記欄", "rainbow brackets": "彩虹括號", "lsp-add-custom-server": "添加自定義服務器", "lsp-binary-args": "可執行參數(JSON 數組)", "lsp-binary-command": "可執行命令", "lsp-binary-path-optional": "可執行路徑(可選)", "lsp-check-command-optional": "檢查命令(可選覆蓋)", "lsp-checking-installation-status": "正在檢查安裝狀態…", "lsp-configured": "已配置", "lsp-custom-server-added": "已添加自定義服務器", "lsp-default": "默認", "lsp-details-line": "詳情:{details}", "lsp-edit-initialization-options": "編輯初始化選項", "lsp-empty": "空", "lsp-enabled": "已啟用", "lsp-error-add-server-failed": "添加服務器失敗", "lsp-error-args-must-be-array": "參數必須是 JSON 數組", "lsp-error-binary-command-required": "必須填寫可執行命令", "lsp-error-language-id-required": "至少需要一個語言 ID", "lsp-error-package-required": "至少需要一個包", "lsp-error-server-id-required": "必須填寫服務器 ID", "lsp-feature-completion": "代碼補全", "lsp-feature-completion-info": "啟用來自服務器的自動補全建議。", "lsp-feature-diagnostics": "診斷", "lsp-feature-diagnostics-info": "顯示語言服務器返回的錯誤和警告。", "lsp-feature-formatting": "格式化", "lsp-feature-formatting-info": "啟用語言服務器提供的代碼格式化。", "lsp-feature-hover": "懸停信息", "lsp-feature-hover-info": "懸停時顯示類型信息和文檔。", "lsp-feature-inlay-hints": "內聯提示", "lsp-feature-inlay-hints-info": "在編輯器中顯示類型提示。", "lsp-feature-signature": "函數簽名提示", "lsp-feature-signature-info": "輸入時顯示函數參數提示。", "lsp-feature-state-toast": "{feature} 已{state}", "lsp-initialization-options": "初始化選項", "lsp-initialization-options-json": "初始化選項(JSON)", "lsp-initialization-options-updated": "初始化選項已更新", "lsp-install-command": "安裝命令", "lsp-install-command-unavailable": "安裝命令不可用", "lsp-install-info-check-failed": "無法驗證安裝狀態。", "lsp-install-info-missing": "語言服務器未安裝在終端環境中。", "lsp-install-info-ready": "語言服務器已安裝並可用。", "lsp-install-info-unknown": "無法自動檢查安裝狀態。", "lsp-install-info-version-available": "可用版本:{version}", "lsp-install-method-apk": "APK 包", "lsp-install-method-cargo": "Cargo 包", "lsp-install-method-manual": "手動二進制", "lsp-install-method-npm": "npm 包", "lsp-install-method-pip": "pip 包", "lsp-install-method-shell": "自定義 Shell", "lsp-install-method-title": "安裝方式", "lsp-install-repair": "安裝 / 修復", "lsp-installation-status": "安裝狀態", "lsp-installed": "已安裝", "lsp-invalid-timeout": "無效的超時值", "lsp-language-ids": "語言 ID(逗號分隔)", "lsp-packages-prompt": "{method} 包(逗號分隔)", "lsp-remove-installed-files": "移除 {server} 的已安裝文件?", "lsp-server-disabled-toast": "服務器已禁用", "lsp-server-enabled-toast": "服務器已啟用", "lsp-server-id": "服務器 ID", "lsp-server-label": "服務器標籤", "lsp-server-not-found": "未找到服務器", "lsp-server-uninstalled": "服務器已卸載", "lsp-startup-timeout": "啟動超時", "lsp-startup-timeout-ms": "啟動超時(毫秒)", "lsp-startup-timeout-set": "啟動超時已設置為 {timeout} ms", "lsp-state-disabled": "禁用", "lsp-state-enabled": "啟用", "lsp-status-check-failed": "檢查失敗", "lsp-status-installed": "已安裝", "lsp-status-installed-version": "已安裝({version})", "lsp-status-line": "狀態:{status}", "lsp-status-not-installed": "未安裝", "lsp-status-unknown": "未知", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "卸載命令不可用", "lsp-uninstall-server": "卸載服務器", "lsp-update-command-optional": "更新命令(可選)", "lsp-update-command-unavailable": "更新命令不可用", "lsp-update-server": "更新服務器", "lsp-version-line": "版本:{version}", "lsp-view-initialization-options": "查看初始化選項", "settings-category-about-acode": "關於 Acode", "settings-category-advanced": "高級", "settings-category-assistance": "輔助", "settings-category-core": "核心設置", "settings-category-cursor": "光標", "settings-category-cursor-selection": "光標與選區", "settings-category-custom-servers": "自定義服務器", "settings-category-customization-tools": "自定義與工具", "settings-category-display": "顯示", "settings-category-editing": "編輯", "settings-category-features": "功能", "settings-category-files-sessions": "文件與會話", "settings-category-fonts": "字體", "settings-category-general": "通用", "settings-category-guides-indicators": "指示線與標記", "settings-category-installation": "安裝", "settings-category-interface": "界面", "settings-category-maintenance": "維護", "settings-category-permissions": "權限", "settings-category-preview": "預覽", "settings-category-scrolling": "滾動", "settings-category-server": "服務器", "settings-category-servers": "服務器", "settings-category-session": "會話", "settings-category-support-acode": "支持 Acode", "settings-category-text-layout": "文本與佈局", "settings-info-app-animation": "控制應用內的過渡動畫。", "settings-info-app-check-files": "當文件在外部被修改時刷新編輯器。", "settings-info-app-clean-install-state": "清除安裝流程使用的存儲狀態。", "settings-info-app-confirm-on-exit": "退出應用前進行確認。", "settings-info-app-console": "選擇 Acode 使用的調試控制台集成方式。", "settings-info-app-default-file-encoding": "打開或創建文件時的默認編碼。", "settings-info-app-exclude-folders": "搜索或掃描時跳過指定文件夾或模式。", "settings-info-app-floating-button": "顯示懸浮快捷按鈕。", "settings-info-app-font-manager": "安裝、管理或移除應用字體。", "settings-info-app-fullscreen": "編輯時隱藏系統狀態欄。", "settings-info-app-keybindings": "編輯快捷鍵文件或重置快捷鍵。", "settings-info-app-keyboard-mode": "選擇軟件鍵盤的編輯行為。", "settings-info-app-language": "選擇應用語言和翻譯標籤。", "settings-info-app-open-file-list-position": "選擇活動文件列表的位置。", "settings-info-app-quick-tools-settings": "自定義和排序快捷工具。", "settings-info-app-quick-tools-trigger-mode": "選擇快捷工具的觸發方式。", "settings-info-app-remember-files": "重新打開上次編輯的文件。", "settings-info-app-remember-folders": "重新打開上次使用的文件夾。", "settings-info-app-retry-remote-fs": "遠程文件傳輸失敗後自動重試。", "settings-info-app-side-buttons": "在編輯器旁顯示額外操作按鈕。", "settings-info-app-sponsor-sidebar": "在側邊欄顯示贊助入口。", "settings-info-app-touch-move-threshold": "觸摸拖動的最小移動距離。", "settings-info-app-vibrate-on-tap": "啟用觸控震動反饋。", "settings-info-editor-autosave": "延遲後自動保存更改。", "settings-info-editor-color-preview": "在編輯器中預覽顏色值。", "settings-info-editor-fade-fold-widgets": "折疊標記在非活動時變暗。", "settings-info-editor-font-family": "選擇編輯器字體。", "settings-info-editor-font-size": "設置編輯器字體大小。", "settings-info-editor-format-on-save": "保存文件時自動格式化。", "settings-info-editor-hard-wrap": "插入真實換行,而不僅是視覺換行。", "settings-info-editor-indent-guides": "顯示縮進指示線。", "settings-info-editor-line-height": "調整行間距。", "settings-info-editor-line-numbers": "在邊欄顯示行號。", "settings-info-editor-lint-gutter": "在邊欄顯示診斷和 Lint 標記。", "settings-info-editor-live-autocomplete": "輸入時顯示自動補全建議。", "settings-info-editor-rainbow-brackets": "按嵌套深度為括號著色。", "settings-info-editor-relative-line-numbers": "顯示相對行號。", "settings-info-editor-rtl-text": "按行切換從右到左文本行為。", "settings-info-editor-scroll-settings": "調整滾動條大小、速度和手勢行為。", "settings-info-editor-shift-click-selection": "使用 Shift + 點擊擴展選區。", "settings-info-editor-show-spaces": "顯示空白字符標記。", "settings-info-editor-soft-tab": "使用空格代替 Tab 字符。", "settings-info-editor-tab-size": "設置 Tab 等效空格數。", "settings-info-editor-teardrop-size": "設置觸控編輯的光標拖動手柄大小。", "settings-info-editor-text-wrap": "在編輯器中自動換行長行。", "settings-info-lsp-add-custom-server": "註冊自定義語言服務器,包括安裝、更新和啟動命令。", "settings-info-lsp-edit-init-options": "以 JSON 形式編輯初始化選項。", "settings-info-lsp-install-server": "安裝或修復此語言服務器。", "settings-info-lsp-server-enabled": "啟用或禁用此語言服務器。", "settings-info-lsp-startup-timeout": "設置語言服務器啟動等待時間。", "settings-info-lsp-uninstall-server": "移除此服務器的已安裝包或二進制文件。", "settings-info-lsp-update-server": "如果可用,更新此語言服務器。", "settings-info-lsp-view-init-options": "以 JSON 查看有效的初始化選項。", "settings-info-main-ad-rewards": "觀看廣告以解鎖臨時免廣告。", "settings-info-main-app-settings": "語言、應用行為和快捷工具。", "settings-info-main-backup-restore": "導出或恢復設置備份。", "settings-info-main-changelog": "查看最近更新和發布說明。", "settings-info-main-edit-settings": "直接編輯 settings.json。", "settings-info-main-editor-settings": "字體、Tab、建議和編輯器顯示。", "settings-info-main-formatter": "為每種語言選擇格式化器。", "settings-info-main-lsp-settings": "配置語言服務器和智能編輯功能。", "settings-info-main-plugins": "管理已安裝插件及其操作。", "settings-info-main-preview-settings": "預覽模式、端口和瀏覽器行為。", "settings-info-main-rateapp": "在 Google Play 上評價 Acode。", "settings-info-main-remove-ads": "解鎖永久免廣告。", "settings-info-main-reset": "將 Acode 重置為默認配置。", "settings-info-main-sponsors": "支持 Acode 的持續開發。", "settings-info-main-terminal-settings": "終端主題、字體、光標和會話行為。", "settings-info-main-theme": "應用主題、對比度和自定義顏色。", "settings-info-preview-disable-cache": "在預覽中始終重新加載內容。", "settings-info-preview-host": "打開預覽 URL 時使用的主機名。", "settings-info-preview-mode": "選擇預覽的打開方式。", "settings-info-preview-preview-port": "實時預覽服務器使用的端口。", "settings-info-preview-server-port": "內部應用服務器使用的端口。", "settings-info-preview-show-console-toggler": "在預覽中顯示控制台按鈕。", "settings-info-preview-use-current-file": "啟動預覽時優先使用當前文件。", "settings-info-terminal-convert-eol": "粘貼或渲染終端輸出時轉換行結尾。", "settings-note-formatter-settings": "為每種語言分配格式化器。安裝格式化插件可解鎖更多選項。", "settings-note-lsp-settings": "語言服務器提供補全、診斷、懸停等功能。你可以在此安裝、更新或定義自定義服務器。託管安裝程序在終端/proot 環境中運行。", "search result label singular": "條結果", "search result label plural": "條結果", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lang/zh-tw.json ================================================ { "lang": "繁體中文 (台灣)", "about": "關於", "active files": "開啟的檔案", "alert": "提醒", "app theme": "應用程式主題", "autocorrect": "啟用自動校正?", "autosave": "自動儲存", "cancel": "取消", "change language": "變更語言", "choose color": "選擇色彩", "clear": "清除", "close app": "關閉應用程式?", "commit message": "提交訊息", "console": "主控台", "conflict error": "發生衝突!請等待其他提交完成。", "copy": "複製", "create folder error": "抱歉,無法建立新資料夾", "cut": "剪下", "delete": "刪除", "dependencies": "相依性", "delay": "時間(毫秒)", "editor settings": "編輯器設定", "editor theme": "編輯器主題", "enter file name": "輸入檔案名稱", "enter folder name": "輸入資料夾名稱", "empty folder message": "空資料夾", "enter line number": "輸入行號", "error": "錯誤", "failed": "失敗", "file already exists": "檔案已存在", "file already exists force": "檔案已存在。是否覆蓋?", "file changed": " 已發生改變,重新載入檔案?", "file deleted": "檔案已刪除", "file is not supported": "不支援此檔案", "file not supported": "不支援此檔案類型。", "file too large": "檔案太大,無法處理。最大支援 {size} 的檔案", "file renamed": "檔案已重新命名", "file saved": "檔案已儲存", "folder added": "資料夾已加入", "folder already added": "資料夾已加入", "font size": "字體大小", "goto": "跳轉至行...", "icons definition": "圖示定義", "info": "資訊", "invalid value": "無效值", "language changed": "語言已變更", "linting": "檢查語法錯誤", "logout": "登出", "loading": "載入中", "my profile": "我的資訊", "new file": "建立新檔案", "new folder": "建立新資料夾", "no": "否", "no editor message": "從選單開啟或建立新檔案和資料夾", "not set": "沒有設定", "unsaved files close app": "有尚未儲存的檔案。確定要關閉應用程式?", "notice": "注意", "open file": "開啟檔案", "open files and folders": "開啟檔案和資料夾", "open folder": "開啟資料夾", "open recent": "最近開啟", "ok": "確認", "overwrite": "覆蓋", "paste": "貼上", "preview mode": "預覽模式", "read only file": "無法儲存唯讀檔案。請嘗試另存為", "reload": "重新載入", "rename": "重新命名", "replace": "取代", "required": "此欄位為必填欄位", "run your web app": "執行你的 web 應用程式", "save": "儲存", "saving": "儲存中", "save as": "另存為", "save file to run": "請儲存此檔案以在瀏覽器中執行", "search": "搜尋", "see logs and errors": "檢視日誌和錯誤", "select folder": "選擇資料夾", "settings": "設定", "settings saved": "設定已儲存", "show line numbers": "顯示行號", "show hidden files": "顯示隱藏檔案", "show spaces": "顯示空白字元", "soft tab": "使用 Tab 縮排", "sort by name": "依名稱排序", "success": "成功", "tab size": "Tab 寬度", "text wrap": "行末自動換行", "theme": "主題", "unable to delete file": "無法刪除檔案", "unable to open file": "無法開啟檔案", "unable to open folder": "無法開啟資料夾", "unable to save file": "無法儲存檔案", "unable to rename": "無法重新命名", "unsaved file": "此檔案還尚未儲存,仍然要關閉?", "warning": "警告", "use emmet": "使用 Emmet 語法", "use quick tools": "使用快捷工具列", "yes": "是", "encoding": "文字編碼", "syntax highlighting": "語法高亮顯示", "read only": "唯讀", "select all": "全選", "select branch": "選擇分支", "create new branch": "建立新分支", "use branch": "使用分支", "new branch": "新分支", "branch": "分支", "key bindings": "快捷鍵", "edit": "編輯", "reset": "重設", "color": "色彩", "select word": "選擇字詞", "quick tools": "快捷工具列", "select": "選擇", "editor font": "編輯器字型", "new project": "建立新專案", "format": "格式化", "project name": "專案名稱", "unsupported device": "您的裝置不支援主題。", "vibrate on tap": "點選時震動", "copy command is not supported by ftp.": "FTP 不支援複製命令。", "support title": "支持 Acode", "fullscreen": "全螢幕", "animation": "動畫效果", "backup": "備份", "restore": "還原", "backup successful": "備份成功", "invalid backup file": "備份檔案無效", "add path": "加入路徑", "live autocompletion": "即時自動補全", "file properties": "檔案屬性", "path": "路徑", "type": "類型", "word count": "字數統計", "line count": "行數統計", "last modified": "最後修改", "size": "大小", "share": "分享", "show print margin": "顯示列印頁邊距", "login": "登入", "scrollbar size": "捲軸大小", "cursor controller size": "游標控制器大小", "none": "無", "small": "小", "large": "大", "floating button": "懸浮按鈕", "confirm on exit": "退出前確認", "show console": "顯示主控台", "image": "圖片", "insert file": "插入檔案", "insert color": "插入顏色", "powersave mode warning": "關閉省電模式以在外部瀏覽器預覽。", "exit": "退出", "custom": "自訂", "reset warning": "確定要重設主題?", "theme type": "主題類型", "light": "亮色", "dark": "深色", "file browser": "檔案瀏覽器", "operation not permitted": "操作不被允許", "no such file or directory": "沒有此檔案或目錄", "input/output error": "輸入/輸出錯誤", "permission denied": "權限被拒", "bad address": "錯誤地址", "file exists": "檔案已存在", "not a directory": "不是目錄", "is a directory": "是目錄", "invalid argument": "無效參數", "too many open files in system": "系統中開啟的檔案過多", "too many open files": "開啟的檔案過多", "text file busy": "檔案正在使用中", "no space left on device": "裝置空間不足", "read-only file system": "唯讀檔案系統", "file name too long": "檔名過長", "too many users": "使用者過多", "connection timed out": "連線超時", "connection refused": "連線被拒", "owner died": "擁有者失效", "an error occurred": "發生錯誤", "add ftp": "加入 FTP", "add sftp": "加入 SFTP", "save file": "儲存檔案", "save file as": "另存檔案為", "files": "檔案", "help": "說明", "file has been deleted": "{file} 已被刪除!", "feature not available": "此功能僅在付費版中可用。", "deleted file": "已刪除的檔案", "line height": "行高", "preview info": "如果想要執行開啟的檔案,長按 ▶ 圖示", "manage all files": "允許 Acode 編輯器在設定中管理所有檔案以便於編輯裝置上的檔案。", "close file": "關閉檔案", "reset connections": "重設連線", "check file changes": "檢查檔案變更", "open in browser": "在瀏覽器中開啟", "desktop mode": "桌面模式", "toggle console": "切換主控台", "new line mode": "換行符號", "add a storage": "加入儲存空間", "rate acode": "評價 Acode", "support": "支援", "downloading file": "正在下載 {file}", "downloading...": "下載中...", "folder name": "資料夾名稱", "keyboard mode": "鍵盤模式", "normal": "正常", "app settings": "應用程式設定", "disable in-app-browser caching": "關閉內建瀏覽器快取", "copied to clipboard": "已複製到剪貼簿", "remember opened files": "記住開啟過的檔案", "remember opened folders": "記住開啟過的資料夾", "no suggestions": "不顯示建議", "no suggestions aggressive": "強制不顯示建議", "install": "安裝", "installing": "安裝中...", "plugins": "外掛", "recently used": "最近使用", "update": "更新", "uninstall": "移除", "download acode pro": "下載 Acode pro", "loading plugins": "正在載入外掛", "faqs": "常見問題", "feedback": "意見回饋", "header": "標題列", "sidebar": "側邊欄", "inapp": "內部瀏覽器", "browser": "外部瀏覽器", "diagonal scrolling": "對角線捲動", "reverse scrolling": "反向捲動", "formatter": "格式化工具", "format on save": "儲存時格式化", "remove ads": "移除廣告", "fast": "快速", "slow": "慢速", "scroll settings": "捲動設定", "scroll speed": "捲動速度", "loading...": "載入中...", "no plugins found": "沒有找到外掛", "name": "名稱", "username": "使用者名稱", "optional": "選填", "hostname": "主機名稱", "password": "密碼", "security type": "安全類型", "connection mode": "連線模式", "port": "連接埠", "key file": "金鑰檔案", "select key file": "選擇金鑰檔案", "passphrase": "通行密碼", "connecting...": "連線中...", "type filename": "輸入檔案名稱", "unable to load files": "無法載入檔案", "preview port": "預覽連接埠", "find file": "尋找檔案", "system": "系統", "please select a formatter": "請選擇一個格式化工具", "case sensitive": "區分大小寫", "regular expression": "正規表達式", "whole word": "整個字詞", "edit with": "編輯於", "open with": "開啟於", "no app found to handle this file": "沒有找到能處理該檔案的應用程式", "restore default settings": "還原預設設定", "server port": "伺服器連接埠", "preview settings": "預覽設定", "preview settings note": "如果預覽連接埠和伺服器連接埠不同,應用程式將不會啟動伺服器,而會在瀏覽器或應用程式內瀏覽器中開啟 https://:。這在你執行著其他伺服器時會有用。", "backup/restore note": "這將只會備份你的設定、自訂主題和快捷鍵,而不備份你的 FTP/SFTP。", "host": "主機", "retry ftp/sftp when fail": "當 FTP/SFTP 連線失敗時重試", "more": "更多", "thank you :)": "感謝您的支持 :)", "purchase pending": "待購買", "cancelled": "已取消", "local": "本機", "remote": "遠端", "show console toggler": "顯示主控台切換按鈕", "binary file": "此檔案包含二進位制資料,確定要開啟它嗎?", "relative line numbers": "相對行號", "elastic tabstops": "彈性的製表縮排風格", "line based rtl switching": "基於行的 RTL(從右到左)轉換", "hard wrap": "強制換行", "spellcheck": "拼寫檢查", "wrap method": "換行方法", "use textarea for ime": "使用用於輸入法的文字輸入框", "invalid plugin": "無效外掛", "type command": "輸入命令", "plugin": "外掛", "quicktools trigger mode": "快捷工具列觸發模式", "print margin": "列印邊距", "touch move threshold": "觸控移動閾值", "info-retryremotefsafterfail": "當 FTP/SFTP 連線失敗時重試。", "info-fullscreen": "隱藏主畫面的標題列。", "info-checkfiles": "當應用程式在背景執行時,檢查檔案變更。", "info-console": "選擇 JavaScript 主控台。Legacy 是預設主控台,Eruda 是第三方主控台。", "info-keyboardmode": "輸入文字時的鍵盤模式。不顯示建議將關閉詞彙建議與自動校正。如果不顯示建議無效,請嘗試強制不顯示建議。", "info-rememberfiles": "關閉時記住開啟的檔案。", "info-rememberfolders": "關閉時記住開啟的資料夾。", "info-floatingbutton": "顯示或隱藏快捷工具列的浮動按鈕。", "info-openfilelistpos": "設定開啟的檔案列表顯示位置。", "info-touchmovethreshold": "如果您的裝置觸控靈敏度過高,可以增加此值以防止誤觸移動。", "info-scroll-settings": "這些設定包含捲動設定,包括文字換行。", "info-animation": "如果應用程式感覺卡頓,請關閉動畫效果。", "info-quicktoolstriggermode": "如果快捷工具列中的按鈕不正常工作,請嘗試更改此值。", "info-checkForAppUpdates": "Check for app updates automatically.", "info-quickTools": "Show or hide quick tools.", "info-showHiddenFiles": "Show hidden files and folders. (Start with .)", "info-all_file_access": "Enable access of /sdcard and /storage in terminal.", "info-fontSize": "The font size used to render text.", "info-fontFamily": "The font family used to render text.", "info-theme": "The color theme of the terminal.", "info-cursorStyle": "The style of the cursor when the terminal is focused.", "info-cursorInactiveStyle": "The style of the cursor when the terminal is not focused.", "info-fontWeight": "The font weight used to render non-bold text.", "info-cursorBlink": "Whether the cursor blinks.", "info-scrollback": "The amount of scrollback in the terminal. Scrollback is the amount of rows that are retained when lines are scrolled beyond the initial viewport.", "info-tabStopWidth": "The size of tab stops in the terminal.", "info-letterSpacing": "The spacing in whole pixels between characters.", "info-imageSupport": "Whether images are supported in the terminal.", "info-fontLigatures": "Whether font ligatures are enabled in the terminal.", "info-confirmTabClose": "Ask for confirmation before closing terminal tabs.", "info-backup": "Creates a backup of the terminal installation.", "info-restore": "Restores a backup of the terminal installation.", "info-uninstall": "Uninstalls the terminal installation.", "owned": "已擁有", "api_error": "API 伺服器沒有回應,請稍後再試。", "installed": "已安裝", "all": "所有", "medium": "中等", "refund": "退款", "product not available": "產品無法使用", "no-product-info": "該產品目前在您所在的國家無法使用,請稍後再試。", "close": "關閉", "explore": "探索", "key bindings updated": "按鍵綁定已更新", "search in files": "在檔案中搜尋", "exclude files": "排除檔案", "include files": "包含檔案", "search result": "{matches} 個結果在 {files} 個檔案中。", "invalid regex": "無效的正規表達式:{message}。", "bottom": "底部", "save all": "儲存全部", "close all": "關閉全部", "unsaved files warning": "某些檔案還沒有儲存。點選『確認』選擇要做什麼或『取消』以返回。", "save all warning": "您確定要儲存所有檔案並關閉嗎?此操作無法復原。", "save all changes warning": "您確定要儲存所有檔案嗎?", "close all warning": "您確定要關閉所有檔案嗎?您將失去還沒有儲存的變更,且此操作無法復原。", "refresh": "重新整理", "shortcut buttons": "快捷鍵按鈕", "no result": "沒有結果", "searching...": "搜尋中...", "quicktools:ctrl-key": "Control/Command 鍵", "quicktools:tab-key": "Tab 鍵", "quicktools:shift-key": "Shift 鍵", "quicktools:undo": "復原", "quicktools:redo": "重做", "quicktools:search": "在檔案中搜尋", "quicktools:save": "儲存檔案", "quicktools:esc-key": "Escape 鍵", "quicktools:curlybracket": "插入大括號", "quicktools:squarebracket": "插入中括號", "quicktools:parentheses": "插入括號", "quicktools:anglebracket": "插入角括號", "quicktools:left-arrow-key": "左方向鍵", "quicktools:right-arrow-key": "右方向鍵", "quicktools:up-arrow-key": "上方向鍵", "quicktools:down-arrow-key": "下方向鍵", "quicktools:moveline-up": "向上移行", "quicktools:moveline-down": "向下移行", "quicktools:copyline-up": "向上複製", "quicktools:copyline-down": "向下複製", "quicktools:semicolon": "插入分號", "quicktools:quotation": "插入引號", "quicktools:and": "插入「與」符號", "quicktools:bar": "插入豎線符號", "quicktools:equal": "插入等號", "quicktools:slash": "插入斜線符號", "quicktools:exclamation": "插入驚嘆號", "quicktools:alt-key": "Alt 鍵", "quicktools:meta-key": "Windows/Meta 鍵", "info-quicktoolssettings": "自訂在編輯器下方的快捷工具內的快捷按鈕與鍵盤按鍵以增強您的開發體驗。", "info-excludefolders": "使用表達式 **/node_modules/** 以忽略所有來自 node_modules 資料夾中的所有檔案。這將排除列出的檔案並且還將避免在這些檔案中搜尋。", "missed files": "在搜尋開始後掃描了 {count} 個檔案並且將不會包含在搜尋中。", "remove": "移除", "quicktools:command-palette": "命令面板", "default file encoding": "預設檔案編碼", "remove entry": "您確定要從儲存的路徑中移除 '{name}' 嗎?請注意這個刪除不會刪除路徑本身。", "delete entry": "確認刪除:'{name}'。此動作無法復原。確定要繼續嗎?", "change encoding": "使用 '{encoding}' 重新開啟 '{file}'?此操作將遺失該檔案任何沒有儲存的變更。您是否想要繼續重新開啟?", "reopen file": "您確定要重新開啟 '{file}' 嗎?任何沒有儲存的變更都將會遺失。", "plugin min version": "{name} 僅可用於 Acode - {v-code} 以上版本。點選這裡以更新。", "color preview": "色彩預覽", "confirm": "確認", "list files": "列出在 {name} 中的所有檔案嗎?太多檔案可能會導致應用程式故障。", "problems": "問題", "show side buttons": "顯示側邊按鈕", "bug_report": "提交錯誤回報", "verified publisher": "已驗證的發行者", "most_downloaded": "最多下載", "newly_added": "最近新增", "top_rated": "最多好評", "rename not supported": "不支援重新命名位於 Termux 目錄中的項目", "compress": "壓縮", "copy uri": "複製 URI", "delete entries": "您是否確定想要刪除 {count} 個項目?", "deleting items": "正在刪除 {count} 個項目...", "import project zip": "匯入專案(zip)", "changelog": "更新日誌", "notifications": "通知", "no_unread_notifications": "沒有未讀的通知", "should_use_current_file_for_preview": "應使用目前的檔案作為預覽頁面,而不是預設檔案 (index.html)", "fade fold widgets": "Fade Fold Widgets", "quicktools:home-key": "Home Key", "quicktools:end-key": "End Key", "quicktools:pageup-key": "PageUp Key", "quicktools:pagedown-key": "PageDown Key", "quicktools:delete-key": "Delete Key", "quicktools:tilde": "Insert tilde symbol", "quicktools:backtick": "Insert backtick", "quicktools:hash": "Insert Hash symbol", "quicktools:dollar": "Insert dollar symbol", "quicktools:modulo": "Insert modulo/percent symbol", "quicktools:caret": "Insert caret symbol", "plugin_enabled": "Plugin enabled", "plugin_disabled": "Plugin disabled", "enable_plugin": "Enable this Plugin", "disable_plugin": "Disable this Plugin", "open_source": "Open Source", "terminal settings": "Terminal Settings", "font ligatures": "Font Ligatures", "letter spacing": "Letter Spacing", "terminal:tab stop width": "Tab Stop Width", "terminal:scrollback": "Scrollback Lines", "terminal:cursor blink": "Cursor Blink", "terminal:font weight": "Font Weight", "terminal:cursor inactive style": "Cursor Inactive Style", "terminal:cursor style": "Cursor Style", "terminal:font family": "Font Family", "terminal:convert eol": "Convert EOL", "terminal:confirm tab close": "Confirm terminal tab close", "terminal:image support": "Image support", "terminal": "Terminal", "allFileAccess": "All file access", "fonts": "Fonts", "sponsor": "贊助", "downloads": "downloads", "reviews": "reviews", "overview": "Overview", "contributors": "Contributors", "quicktools:hyphen": "Insert hyphen symbol", "check for app updates": "Check for app updates", "prompt update check consent message": "Acode can check for new app updates when you're online. Enable update checks?", "keywords": "Keywords", "author": "Author", "filtered by": "Filtered by", "clean install state": "Clean Install State", "backup created": "Backup created", "restore completed": "Restore completed", "restore will include": "This will restore", "restore warning": "This action cannot be undone. Continue?", "reload to apply": "Reload to apply changes?", "reload app": "Reload app", "preparing backup": "Preparing backup", "collecting settings": "Collecting settings", "collecting key bindings": "Collecting key bindings", "collecting plugins": "Collecting plugin information", "creating backup": "Creating backup file", "validating backup": "Validating backup", "restoring key bindings": "Restoring key bindings", "restoring plugins": "Restoring plugins", "restoring settings": "Restoring settings", "legacy backup warning": "This is an older backup format. Some features may be limited.", "checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted.", "plugin not found": "Plugin not found in registry", "paid plugin skipped": "Paid plugin - purchase not found", "source not found": "Source file no longer exists", "restored": "Restored", "skipped": "Skipped", "backup not valid object": "Backup file is not a valid object", "backup no data": "Backup file contains no data to restore", "backup legacy warning": "This is an older backup format (v1). Some features may be limited.", "backup missing metadata": "Missing backup metadata - some info may be unavailable", "backup checksum mismatch": "Checksum mismatch - backup file may have been modified or corrupted. Proceed with caution.", "backup checksum verify failed": "Could not verify checksum", "backup invalid settings": "Invalid settings format", "backup invalid keybindings": "Invalid keyBindings format", "backup invalid plugins": "Invalid installedPlugins format", "issues found": "Issues found", "error details": "Error details", "active tools": "Active tools", "available tools": "Available tools", "recent": "Recent Files", "command palette": "Open Command Palette", "change theme": "Change Theme", "documentation": "Documentation", "open in terminal": "Open in Terminal", "developer mode": "Developer Mode", "info-developermode": "Enable developer tools (Eruda) for debugging plugins and inspecting app state. Inspector will be initialized on app start.", "developer mode enabled": "Developer mode enabled. Use command palette to toggle inspector (Ctrl+Shift+I).", "developer mode disabled": "Developer mode disabled", "copy relative path": "Copy Relative Path", "shortcut request sent": "Shortcut request opened. Tap Add to finish.", "add to home screen": "Add to home screen", "pin shortcuts not supported": "Home screen shortcuts are not supported on this device.", "save file before home shortcut": "Save the file before adding it to the home screen.", "terminal_required_message_for_lsp": "Terminal not installed. Please install Terminal first to use LSP servers.", "shift click selection": "Shift + tap/click selection", "earn ad-free time": "Earn ad-free time", "indent guides": "Indent guides", "language servers": "Language servers", "lint gutter": "Show lint gutter", "rainbow brackets": "Rainbow brackets", "lsp-add-custom-server": "Add custom server", "lsp-binary-args": "Binary args (JSON array)", "lsp-binary-command": "Binary command", "lsp-binary-path-optional": "Binary path (optional)", "lsp-check-command-optional": "Check command (optional override)", "lsp-checking-installation-status": "Checking installation status...", "lsp-configured": "Configured", "lsp-custom-server-added": "Custom server added", "lsp-default": "Default", "lsp-details-line": "Details: {details}", "lsp-edit-initialization-options": "Edit initialization options", "lsp-empty": "Empty", "lsp-enabled": "Enabled", "lsp-error-add-server-failed": "Failed to add server", "lsp-error-args-must-be-array": "Arguments must be a JSON array", "lsp-error-binary-command-required": "Binary command is required", "lsp-error-language-id-required": "At least one language ID is required", "lsp-error-package-required": "At least one package is required", "lsp-error-server-id-required": "Server ID is required", "lsp-feature-completion": "Code completion", "lsp-feature-completion-info": "Enable autocomplete suggestions from the server.", "lsp-feature-diagnostics": "Diagnostics", "lsp-feature-diagnostics-info": "Show errors and warnings from the language server.", "lsp-feature-formatting": "Formatting", "lsp-feature-formatting-info": "Enable code formatting from the language server.", "lsp-feature-hover": "Hover information", "lsp-feature-hover-info": "Show type information and documentation on hover.", "lsp-feature-inlay-hints": "Inlay hints", "lsp-feature-inlay-hints-info": "Show inline type hints in the editor.", "lsp-feature-signature": "Signature help", "lsp-feature-signature-info": "Show function parameter hints while typing.", "lsp-feature-state-toast": "{feature} {state}", "lsp-initialization-options": "Initialization options", "lsp-initialization-options-json": "Initialization options (JSON)", "lsp-initialization-options-updated": "Initialization options updated", "lsp-install-command": "Install command", "lsp-install-command-unavailable": "Install command not available", "lsp-install-info-check-failed": "Acode could not verify the installation status.", "lsp-install-info-missing": "Language server is not installed in the terminal environment.", "lsp-install-info-ready": "Language server is installed and ready.", "lsp-install-info-unknown": "Installation status could not be checked automatically.", "lsp-install-info-version-available": "Version {version} is available.", "lsp-install-method-apk": "APK package", "lsp-install-method-cargo": "Cargo crate", "lsp-install-method-manual": "Manual binary", "lsp-install-method-npm": "npm package", "lsp-install-method-pip": "pip package", "lsp-install-method-shell": "Custom shell", "lsp-install-method-title": "Install method", "lsp-install-repair": "Install / repair", "lsp-installation-status": "Installation status", "lsp-installed": "Installed", "lsp-invalid-timeout": "Invalid timeout value", "lsp-language-ids": "Language IDs (comma separated)", "lsp-packages-prompt": "{method} packages (comma separated)", "lsp-remove-installed-files": "Remove installed files for {server}?", "lsp-server-disabled-toast": "Server disabled", "lsp-server-enabled-toast": "Server enabled", "lsp-server-id": "Server ID", "lsp-server-label": "Server label", "lsp-server-not-found": "Server not found", "lsp-server-uninstalled": "Server uninstalled", "lsp-startup-timeout": "Startup timeout", "lsp-startup-timeout-ms": "Startup timeout (milliseconds)", "lsp-startup-timeout-set": "Startup timeout set to {timeout} ms", "lsp-state-disabled": "disabled", "lsp-state-enabled": "enabled", "lsp-status-check-failed": "Check failed", "lsp-status-installed": "Installed", "lsp-status-installed-version": "Installed ({version})", "lsp-status-line": "Status: {status}", "lsp-status-not-installed": "Not installed", "lsp-status-unknown": "Unknown", "lsp-timeout-ms": "{timeout} ms", "lsp-uninstall-command-unavailable": "Uninstall command not available", "lsp-uninstall-server": "Uninstall server", "lsp-update-command-optional": "Update command (optional)", "lsp-update-command-unavailable": "Update command not available", "lsp-update-server": "Update server", "lsp-version-line": "Version: {version}", "lsp-view-initialization-options": "View initialization options", "settings-category-about-acode": "About Acode", "settings-category-advanced": "Advanced", "settings-category-assistance": "Assistance", "settings-category-core": "Core settings", "settings-category-cursor": "Cursor", "settings-category-cursor-selection": "Cursor & selection", "settings-category-custom-servers": "Custom servers", "settings-category-customization-tools": "Customization & tools", "settings-category-display": "Display", "settings-category-editing": "Editing", "settings-category-features": "Features", "settings-category-files-sessions": "Files & sessions", "settings-category-fonts": "Fonts", "settings-category-general": "General", "settings-category-guides-indicators": "Guides & indicators", "settings-category-installation": "Installation", "settings-category-interface": "Interface", "settings-category-maintenance": "Maintenance", "settings-category-permissions": "Permissions", "settings-category-preview": "Preview", "settings-category-scrolling": "Scrolling", "settings-category-server": "Server", "settings-category-servers": "Servers", "settings-category-session": "Session", "settings-category-support-acode": "Support Acode", "settings-category-text-layout": "Text & layout", "settings-info-app-animation": "Control transition animations across the app.", "settings-info-app-check-files": "Refresh editors when files change outside Acode.", "settings-info-app-clean-install-state": "Clear stored install state used by onboarding and setup flows.", "settings-info-app-confirm-on-exit": "Ask before closing the app.", "settings-info-app-console": "Choose which debug console integration Acode uses.", "settings-info-app-default-file-encoding": "Default encoding when opening or creating files.", "settings-info-app-exclude-folders": "Skip folders and patterns while searching or scanning.", "settings-info-app-floating-button": "Show the floating quick actions button.", "settings-info-app-font-manager": "Install, manage, or remove app fonts.", "settings-info-app-fullscreen": "Hide the system status bar while using Acode.", "settings-info-app-keybindings": "Edit the key bindings file or reset shortcuts.", "settings-info-app-keyboard-mode": "Choose how the software keyboard behaves while editing.", "settings-info-app-language": "Choose the app language and translated labels.", "settings-info-app-open-file-list-position": "Choose where the active files list appears.", "settings-info-app-quick-tools-settings": "Reorder and customize quick tool shortcuts.", "settings-info-app-quick-tools-trigger-mode": "Choose how quick tools open on tap or touch.", "settings-info-app-remember-files": "Reopen the files that were open last time.", "settings-info-app-remember-folders": "Reopen folders from the previous session.", "settings-info-app-retry-remote-fs": "Retry remote file operations after a failed transfer.", "settings-info-app-side-buttons": "Show extra action buttons beside the editor.", "settings-info-app-sponsor-sidebar": "Show the sponsor entry in the sidebar.", "settings-info-app-touch-move-threshold": "Minimum movement before a touch drag is detected.", "settings-info-app-vibrate-on-tap": "Enable haptic feedback for taps and controls.", "settings-info-editor-autosave": "Save changes automatically after a delay.", "settings-info-editor-color-preview": "Preview color values inline in the editor.", "settings-info-editor-fade-fold-widgets": "Dim fold markers until they are needed.", "settings-info-editor-font-family": "Choose the typeface used in the editor.", "settings-info-editor-font-size": "Set the editor text size.", "settings-info-editor-format-on-save": "Run the formatter whenever a file is saved.", "settings-info-editor-hard-wrap": "Insert real line breaks instead of only wrapping visually.", "settings-info-editor-indent-guides": "Show indentation guide lines.", "settings-info-editor-line-height": "Adjust vertical spacing between lines.", "settings-info-editor-line-numbers": "Show line numbers in the gutter.", "settings-info-editor-lint-gutter": "Show diagnostics and lint markers in the gutter.", "settings-info-editor-live-autocomplete": "Show suggestions while you type.", "settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.", "settings-info-editor-relative-line-numbers": "Show distance from the current line.", "settings-info-editor-rtl-text": "Switch right-to-left behavior per line.", "settings-info-editor-scroll-settings": "Adjust scrollbar size, speed, and gesture behavior.", "settings-info-editor-shift-click-selection": "Extend selection with Shift + tap or click.", "settings-info-editor-show-spaces": "Display visible whitespace markers.", "settings-info-editor-soft-tab": "Insert spaces instead of tab characters.", "settings-info-editor-tab-size": "Set how many spaces each tab step uses.", "settings-info-editor-teardrop-size": "Set the cursor handle size for touch editing.", "settings-info-editor-text-wrap": "Wrap long lines inside the editor.", "settings-info-lsp-add-custom-server": "Register a custom language server with install, update, and launch commands.", "settings-info-lsp-edit-init-options": "Edit initialization options as JSON.", "settings-info-lsp-install-server": "Install or repair this language server.", "settings-info-lsp-server-enabled": "Enable or disable this language server.", "settings-info-lsp-startup-timeout": "Set how long Acode waits for the server to start.", "settings-info-lsp-uninstall-server": "Remove installed packages or binaries for this server.", "settings-info-lsp-update-server": "Update this language server if an update flow is available.", "settings-info-lsp-view-init-options": "View the effective initialization options as JSON.", "settings-info-main-ad-rewards": "Watch ads to unlock temporary ad-free access.", "settings-info-main-app-settings": "Language, app behavior, and quick access tools.", "settings-info-main-backup-restore": "Export settings to a backup or restore them later.", "settings-info-main-changelog": "See recent updates and release notes.", "settings-info-main-edit-settings": "Edit the raw settings.json file directly.", "settings-info-main-editor-settings": "Fonts, tabs, suggestions, and editor display.", "settings-info-main-formatter": "Choose a formatter for each supported language.", "settings-info-main-lsp-settings": "Configure language servers and editor intelligence.", "settings-info-main-plugins": "Manage installed plugins and their available actions.", "settings-info-main-preview-settings": "Preview mode, server ports, and browser behavior.", "settings-info-main-rateapp": "Rate Acode on Google Play.", "settings-info-main-remove-ads": "Unlock permanent ad-free access.", "settings-info-main-reset": "Reset Acode to its default configuration.", "settings-info-main-sponsors": "Support ongoing Acode development.", "settings-info-main-terminal-settings": "Terminal theme, font, cursor, and session behavior.", "settings-info-main-theme": "App theme, contrast, and custom colors.", "settings-info-preview-disable-cache": "Always reload content in the in-app browser.", "settings-info-preview-host": "Hostname used when opening the preview URL.", "settings-info-preview-mode": "Choose where preview opens when you launch it.", "settings-info-preview-preview-port": "Port used by the live preview server.", "settings-info-preview-server-port": "Port used by the internal app server.", "settings-info-preview-show-console-toggler": "Show the console button in preview.", "settings-info-preview-use-current-file": "Prefer the current file when starting preview.", "settings-info-terminal-convert-eol": "Convert line endings when pasting or rendering terminal output.", "settings-note-formatter-settings": "Assign a formatter to each language. Install formatter plugins to unlock more options.", "settings-note-lsp-settings": "Language servers add autocomplete, diagnostics, hover details, and more. You can install, update, or define custom servers here. Managed installers run inside the terminal/proot environment.", "search result label singular": "result", "search result label plural": "results", "pin tab": "Pin tab", "unpin tab": "Unpin tab", "pinned tab": "Pinned tab", "unpin tab before closing": "Unpin the tab before closing it.", "app font": "App font", "settings-info-app-font-family": "Choose the font used across the app interface.", "lsp-transport-method-stdio": "STDIO (launch a binary command)", "lsp-transport-method-websocket": "WebSocket (connect to a ws/wss URL)", "lsp-websocket-url": "WebSocket URL", "lsp-websocket-server-managed-externally": "This server is managed externally over WebSocket.", "lsp-error-websocket-url-invalid": "WebSocket URL must start with ws:// or wss://", "lsp-error-websocket-url-required": "WebSocket URL is required", "lsp-remove-custom-server": "Remove custom server", "lsp-remove-custom-server-confirm": "Remove custom language server {server}?", "lsp-custom-server-removed": "Custom server removed", "settings-info-lsp-remove-custom-server": "Remove this custom language server from Acode." } ================================================ FILE: src/lib/acode.js ================================================ import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; import * as cmAutocomplete from "@codemirror/autocomplete"; import * as cmCommands from "@codemirror/commands"; import * as cmLanguage from "@codemirror/language"; 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, refreshCommandKeymap, registerExternalCommand, removeExternalCommand, executeCommand as runCommand, } from "cm/commandRegistry"; import { default as lspApi } from "cm/lsp/api"; import lspClientManager from "cm/lsp/clientManager"; import { registerLspFormatter } from "cm/lsp/formatter"; import { addMode, getModeForPath, getModes, getModesByName, removeMode, } from "cm/modelist"; import cmThemeRegistry from "cm/themes"; import Contextmenu from "components/contextmenu"; import inputhints from "components/inputhints"; import Page from "components/page"; import palette from "components/palette"; import settingsPage from "components/settingsPage"; import SideButton from "components/sideButton"; import { TerminalManager, TerminalThemeManager } from "components/terminal"; import toast from "components/toast"; import tutorial from "components/tutorial"; import alert from "dialogs/alert"; import box from "dialogs/box"; import colorPicker from "dialogs/color"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import multiPrompt from "dialogs/multiPrompt"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import { addIntentHandler, removeIntentHandler } from "handlers/intent"; import keyboardHandler from "handlers/keyboard"; import purchaseListener from "handlers/purchase"; import windowResize from "handlers/windowResize"; import actionStack from "lib/actionStack"; import commands from "lib/commands"; import EditorFile from "lib/editorFile"; import files from "lib/fileList"; import fileTypeHandler from "lib/fileTypeHandler"; import fonts from "lib/fonts"; import { BROKEN_PLUGINS, LOADED_PLUGINS, onPluginLoadCallback, onPluginsLoadCompleteCallback, } from "lib/loadPlugins"; import NotificationManager from "lib/notificationManager"; import openFolder, { addedFolder } from "lib/openFolder"; import projects from "lib/projects"; import selectionMenu from "lib/selectionMenu"; import appSettings from "lib/settings"; import FileBrowser from "pages/fileBrowser"; import formatterSettings from "settings/formatterSettings"; import ThemeBuilder from "theme/builder"; import themes from "theme/list"; import Color from "utils/color"; 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"; export default class Acode { #modules = {}; #pluginsInit = {}; #pluginUnmount = {}; // Registered formatter implementations (populated by plugins) #formatter = []; #pluginWatchers = {}; /** * Clear a plugin's broken mark (so it can be retried) * @param {string} pluginId */ clearBrokenPluginMark(pluginId) { try { if (BROKEN_PLUGINS.has(pluginId)) { BROKEN_PLUGINS.delete(pluginId); } } catch (e) { console.warn("Failed to clear broken plugin mark:", e); } } constructor() { const encodingsModule = { get encodings() { return encodings; }, encode, decode, }; const themesModule = { add: themes.add, get: themes.get, list: themes.list, update: themes.update, // Deprecated, not supported anymore apply: () => {}, }; // CodeMirror editor theme API for plugins const normalizeThemeSpec = (spec) => { if (!spec || typeof spec !== "object" || Array.isArray(spec)) { console.warn( "[editorThemes] register(spec) expects an object: { id, caption?, dark?, getExtension|extensions|extension|theme, config? }", ); return null; } const id = spec.id || spec.name; if (!id) { console.warn("[editorThemes] register(spec) requires a valid `id`."); return null; } const extensionSource = spec.getExtension || spec.extensions || spec.extension || spec.theme; if (extensionSource === undefined || extensionSource === null) { console.warn( `[editorThemes] register('${id}') requires extensions via getExtension/extensions/extension/theme.`, ); return null; } return { id, caption: spec.caption || spec.label || id, isDark: spec.isDark ?? spec.dark ?? false, getExtension: typeof extensionSource === "function" ? extensionSource : () => extensionSource, config: spec.config ?? null, }; }; const createHighlightStyle = (spec) => { if (!spec) return null; if (Array.isArray(spec)) return cmLanguage.HighlightStyle.define(spec); return spec; }; const createTheme = ({ styles, dark = false, highlightStyle, extensions = [], } = {}) => { const ext = []; if (styles && typeof styles === "object") { ext.push(cmView.EditorView.theme(styles, { dark: !!dark })); } const resolvedHighlight = createHighlightStyle(highlightStyle); if (resolvedHighlight) { ext.push(cmLanguage.syntaxHighlighting(resolvedHighlight)); } if (Array.isArray(extensions)) { ext.push(...extensions); } else if (extensions) { ext.push(extensions); } return ext; }; const editorThemesModule = { /** * Register a CodeMirror theme from plugin code. * @param {{ * id: string, * caption?: string, * dark?: boolean, * getExtension?: Function, * extensions?: unknown, * config?: object * }} spec * `isDark`, `extension`, and `theme` are accepted aliases for compatibility. * @returns {boolean} */ register: (spec) => { const resolved = normalizeThemeSpec(spec); if (!resolved) return false; return cmThemeRegistry.addTheme( resolved.id, resolved.caption, resolved.isDark, resolved.getExtension, resolved.config, ); }, unregister: (id) => cmThemeRegistry.removeTheme(id), list: () => cmThemeRegistry.getThemes(), apply: (id) => editorManager?.editor?.setTheme?.(id), get: (id) => cmThemeRegistry.getThemeById(id), getConfig: (id) => cmThemeRegistry.getThemeConfig(id), createTheme, createHighlightStyle, cm: { EditorView: cmView.EditorView, HighlightStyle: cmLanguage.HighlightStyle, syntaxHighlighting: cmLanguage.syntaxHighlighting, tags: lezerHighlight.tags, }, }; const sidebarAppsModule = { add: sidebarApps.add, get: sidebarApps.get, remove: sidebarApps.remove, }; const lspModule = { ...lspApi, clientManager: { setOptions: (options) => lspClientManager.setOptions(options), getActiveClients: () => lspClientManager.getActiveClients(), }, }; const getModeByName = (name) => { const normalized = String(name || "") .trim() .toLowerCase(); if (!normalized) return null; return getModesByName()[normalized] || null; }; const listModes = () => [...getModes()]; const listModesByName = () => ({ ...getModesByName() }); const aceModes = { addMode, removeMode, getModeForPath: (path) => getModeForPath(String(path || "")), getModes: () => listModes(), getModesByName: () => listModesByName(), getMode: (name) => getModeByName(name), }; // Preferred CodeMirror language registration API for plugins const editorLanguages = { // name: string, extensions: string|Array, caption?: string, // loader?: () => Extension | Promise register: (name, extensions, caption, loader) => addMode(name, extensions, caption, loader), unregister: (name) => removeMode(name), add: (name, extensions, caption, loader) => addMode(name, extensions, caption, loader), remove: (name) => removeMode(name), list: () => listModes(), listByName: () => listModesByName(), get: (name) => getModeByName(name), getForPath: (path) => getModeForPath(String(path || "")), }; const intent = { addHandler: addIntentHandler, removeHandler: removeIntentHandler, }; const terminalTouchSelectionMoreOptions = { add: (option) => TerminalManager.addTouchSelectionMoreOption(option), remove: (id) => TerminalManager.removeTouchSelectionMoreOption(id), list: () => TerminalManager.getTouchSelectionMoreOptions(), }; const terminalModule = { create: (options) => TerminalManager.createTerminal(options), createLocal: (options) => TerminalManager.createLocalTerminal(options), createServer: (options) => TerminalManager.createServerTerminal(options), get: (id) => TerminalManager.getTerminal(id), getAll: () => TerminalManager.getAllTerminals(), write: (id, data) => this.#secureTerminalWrite(id, data), clear: (id) => TerminalManager.clearTerminal(id), close: (id) => TerminalManager.closeTerminal(id), moreOptions: terminalTouchSelectionMoreOptions, touchSelection: { moreOptions: terminalTouchSelectionMoreOptions, }, themes: { register: (name, theme, pluginId) => TerminalThemeManager.registerTheme(name, theme, pluginId), unregister: (name, pluginId) => TerminalThemeManager.unregisterTheme(name, pluginId), get: (name) => TerminalThemeManager.getTheme(name), getAll: () => TerminalThemeManager.getAllThemes(), getNames: () => TerminalThemeManager.getThemeNames(), createVariant: (baseName, overrides) => TerminalThemeManager.createVariant(baseName, overrides), }, }; const codemirrorModule = Object.freeze({ autocomplete: cmAutocomplete, commands: cmCommands, language: cmLanguage, lezer: lezerHighlight, lint: cmLint, search: cmSearch, state: cmState, view: cmView, }); this.define("Url", Url); this.define("page", Page); this.define("Color", Color); this.define("fonts", fonts); this.define("toast", toast); this.define("alert", alert); this.define("select", select); this.define("loader", loader); this.define("dialogBox", box); this.define("prompt", prompt); this.define("intent", intent); this.define("fileList", files); this.define("fs", fsOperation); this.define("confirm", confirm); this.define("helpers", helpers); this.define("palette", palette); this.define("projects", projects); this.define("tutorial", tutorial); this.define("aceModes", aceModes); this.define("themes", themesModule); this.define("editorLanguages", editorLanguages); this.define("editorThemes", editorThemesModule); this.define("lsp", lspModule); this.define("settings", appSettings); this.define("sideButton", SideButton); this.define("EditorFile", EditorFile); this.define("inputhints", inputhints); this.define("openfolder", openFolder); this.define("colorPicker", colorPicker); this.define("actionStack", actionStack); this.define("multiPrompt", multiPrompt); this.define("addedfolder", addedFolder); this.define("contextMenu", Contextmenu); this.define("fileBrowser", FileBrowser); this.define("fsOperation", fsOperation); this.define("keyboard", keyboardHandler); this.define("windowResize", windowResize); this.define("encodings", encodingsModule); this.define("themeBuilder", ThemeBuilder); this.define("selectionMenu", selectionMenu); this.define("sidebarApps", sidebarAppsModule); this.define("terminal", terminalModule); this.define("codemirror", codemirrorModule); this.define("@codemirror/autocomplete", cmAutocomplete); this.define("@codemirror/commands", cmCommands); this.define("@codemirror/language", cmLanguage); this.define("@codemirror/lint", cmLint); this.define("@codemirror/search", cmSearch); this.define("@codemirror/state", cmState); this.define("@codemirror/view", cmView); this.define("@lezer/highlight", lezerHighlight); this.define("createKeyboardEvent", KeyboardEvent); this.define("toInternalUrl", helpers.toInternalUri); this.define("commands", this.#createCommandApi()); registerLspFormatter(this); } /** * Secure terminal write with command validation * Prevents execution of malicious or dangerous commands through plugin API * @param {string} id - Terminal ID * @param {string} data - Data to write */ #secureTerminalWrite(id, data) { if (typeof data !== "string") { console.warn("Terminal write data must be a string"); return; } // List of potentially dangerous commands/patterns to block const dangerousPatterns = [ // System commands that can cause damage /^\s*rm\s+-rf?\s+\/[^\r\n]*[\r\n]?$/m, /^\s*rm\s+-rf?\s+\*[^\r\n]*[\r\n]?$/m, /^\s*rm\s+-rf?\s+~[^\r\n]*[\r\n]?$/m, /^\s*mkfs\.[^\r\n]*[\r\n]?$/m, /^\s*dd\s+if=\/[^\r\n]*[\r\n]?$/m, /^\s*:(){ :|:& };:[^\r\n]*[\r\n]?$/m, // Fork bomb /^\s*sudo\s+dd\s+if=\/[^\r\n]*[\r\n]?$/m, /^\s*sudo\s+rm\s+-rf?\s+\/[^\r\n]*[\r\n]?$/m, /^\s*curl\s+[^\r\n]*\|\s*sh[^\r\n]*[\r\n]?$/m, /^\s*wget\s+[^\r\n]*\|\s*sh[^\r\n]*[\r\n]?$/m, /^\s*bash\s+<\s*\([^\r\n]*[\r\n]?$/m, /^\s*sh\s+<\s*\([^\r\n]*[\r\n]?$/m, // Network-based attacks /^\s*nc\s+-l\s+-p\s+\d+[^\r\n]*[\r\n]?$/m, /^\s*ncat\s+-l\s+-p\s+\d+[^\r\n]*[\r\n]?$/m, /^\s*python\s+.*SimpleHTTPServer[^\r\n]*[\r\n]?$/m, /^\s*python\s+.*http\.server[^\r\n]*[\r\n]?$/m, // Process manipulation /^\s*kill\s+-9\s+1\s*[\r\n]?$/m, /^\s*killall\s+-9\s+\*[^\r\n]*[\r\n]?$/m, // File system manipulation /^\s*chmod\s+777\s+\/[^\r\n]*[\r\n]?$/m, /^\s*chown\s+[^\s]+\s+\/[^\r\n]*[\r\n]?$/m, // Sensitive file access attempts /^\s*cat\s+\/etc\/passwd[^\r\n]*[\r\n]?$/m, /^\s*cat\s+\/etc\/shadow[^\r\n]*[\r\n]?$/m, /^\s*cat\s+\/root\/[^\r\n]*[\r\n]?$/m, // Only block null bytes /\x00/g, ]; // Check for dangerous patterns for (const pattern of dangerousPatterns) { if (pattern.test(data)) { console.warn( `Blocked potentially dangerous terminal command: ${data.substring(0, 50)}...`, ); toast("Potentially dangerous command blocked for security", 3000); return; } } // Additional checks for suspicious character sequences if (data.includes("$(") && data.includes(")")) { const commandSubstitution = /\$\([^)]*\)/g; const matches = data.match(commandSubstitution); if (matches) { for (const match of matches) { // Check if command substitution contains dangerous commands for (const pattern of dangerousPatterns) { if (pattern.test(match)) { console.warn( `Blocked command substitution with dangerous content: ${match}`, ); toast("Command substitution blocked for security", 3000); return; } } } } } // Sanitize data length to prevent memory exhaustion const maxLength = 64 * 1024; // 64KB max per write if (data.length > maxLength) { console.warn( `Terminal write data truncated - exceeded ${maxLength} characters`, ); data = data.substring(0, maxLength) + "\n[Data truncated for security]\n"; } // If all security checks pass, proceed with writing return TerminalManager.writeToTerminal(id, data); } /** * Define a module * @param {string} name * @param {Object|function} module */ define(name, module) { this.#modules[name.toLowerCase()] = module; } require(module) { return this.#modules[module.toLowerCase()]; } exec(key, val) { if (key in commands) { return commands[key](val); } return false; } /** * Installs an Acode plugin from registry * @param {string} pluginId id of the plugin to install * @param {string} installerPluginName Name of plugin attempting to install * @returns {Promise} */ installPlugin(pluginId, installerPluginName) { return new Promise((resolve, reject) => { fsOperation(Url.join(PLUGIN_DIR, pluginId)) .exists() .then((isPluginExists) => { if (isPluginExists) { reject(new Error("Plugin already installed")); return; } confirm( strings.install, `Do you want to install plugin '${pluginId}'${installerPluginName ? ` requested by ${installerPluginName}` : ""}?`, ).then((confirmation) => { if (!confirmation) { reject(new Error("User cancelled installation")); return; } let purchaseToken; let product; const pluginUrl = Url.join( constants.API_BASE, `plugin/${pluginId}`, ); fsOperation(pluginUrl) .readFile("json") .catch(() => { reject(new Error("Failed to fetch plugin details")); return null; }) .then((remotePlugin) => { if (remotePlugin) { const isPaid = remotePlugin.price > 0; helpers .promisify(iap.getProducts, [remotePlugin.sku]) .then((products) => { [product] = products; if (product) { return getPurchase(product.productId); } return null; }) .then((purchase) => { purchaseToken = purchase?.purchaseToken; if (isPaid && !purchaseToken) { if (!product) throw new Error("Product not found"); return helpers.checkAPIStatus().then((apiStatus) => { if (!apiStatus) { alert(strings.error, strings.api_error); return; } iap.setPurchaseUpdatedListener( ...purchaseListener(onpurchase, onerror), ); return helpers.promisify( iap.purchase, product.productId, ); }); } }) .then(() => { import("lib/installPlugin").then( ({ default: installPlugin }) => { installPlugin( pluginId, remotePlugin.name, purchaseToken, ).then(() => { resolve(); }); }, ); }); 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, }, }, ); purchaseToken = purchase?.purchaseToken; } async function onerror(error) { throw error; } } }); async function getPurchase(sku) { const purchases = await helpers.promisify(iap.getPurchases); const purchase = purchases.find((p) => p.productIds.includes(sku), ); return purchase; } }); }) .catch((error) => { reject(error); }); }); } [onPluginLoadCallback](pluginId) { if (this.#pluginWatchers[pluginId]) { this.#pluginWatchers[pluginId].resolve(); delete this.#pluginWatchers[pluginId]; } } [onPluginsLoadCompleteCallback]() { for (const pluginId in this.#pluginWatchers) { this.#pluginWatchers[pluginId].reject( new Error(`Plugin '${pluginId}' failed to load.`), ); } this.#pluginWatchers = {}; } waitForPlugin(pluginId) { return new Promise((resolve, reject) => { if (LOADED_PLUGINS.has(pluginId)) { return resolve(true); } this.#pluginWatchers[pluginId] = { resolve, reject, }; }); } get exitAppMessage() { const numFiles = editorManager.hasUnsavedFiles(); if (numFiles) { return strings["unsaved files close app"]; } return null; } setLoadingMessage(message) { document.body.setAttribute("data-small-msg", message); } /** * Sets plugin init function * @param {string} id * @param {() => void} initFunction * @param {{list: import('components/settingsPage').ListItem[], cb: (key: string, value: string)=>void}} settings */ setPluginInit(id, initFunction, settings) { this.#pluginsInit[id] = initFunction; if (!settings) return; appSettings.uiSettings[`plugin-${id}`] = settingsPage( id, settings.list, settings.cb, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", valueInTail: true, groupByDefault: true, }, ); } setPluginUnmount(id, unmountFunction) { this.#pluginUnmount[id] = unmountFunction; } /** * * @param {string} id plugin id * @param {string} baseUrl local plugin url * @param {HTMLElement} $page */ async initPlugin(id, baseUrl, $page, options) { if (id in this.#pluginsInit) { await this.#pluginsInit[id](baseUrl, $page, options); } } unmountPlugin(id) { if (id in this.#pluginUnmount) { this.#pluginUnmount[id](); fsOperation(Url.join(CACHE_STORAGE, id)).delete(); } delete appSettings.uiSettings[`plugin-${id}`]; } registerFormatter(id, extensions, format, displayName) { let exts; if (Array.isArray(extensions)) { exts = extensions.filter(Boolean); if (!exts.length) exts = ["*"]; } else if (typeof extensions === "string" && extensions) { exts = [extensions]; } else { exts = ["*"]; } this.#formatter.unshift({ id, name: displayName, exts: exts, format, }); } unregisterFormatter(id) { this.#formatter = this.#formatter.filter( (formatter) => formatter.id !== id, ); const { formatter } = appSettings.value; for (const mode of Object.keys(formatter)) { if (formatter[mode] === id) { delete formatter[mode]; } } appSettings.update(false); } async format(selectIfNull = true) { const file = editorManager.activeFile; if (!file || file.type !== "editor") return false; let resolvedMode = file.currentMode; if (!resolvedMode) { try { resolvedMode = getModeForPath(file.filename)?.name; } catch (_) { resolvedMode = null; } } const modeName = resolvedMode || "text"; const formatterMap = appSettings.value.formatter || {}; const formatterId = formatterMap[modeName]; const formatter = this.#formatter.find(({ id }) => id === formatterId); if (!formatter) { if (formatterId) { delete formatterMap[modeName]; await appSettings.update(false); } if (selectIfNull) { formatterSettings(modeName); this.#afterSelectFormatter(modeName); } else { toast(strings["please select a formatter"]); } return false; } try { await formatter.format(); return true; } catch (error) { helpers.error(error); return false; } } #afterSelectFormatter(name) { appSettings.on("update:formatter", format); function format() { appSettings.off("update:formatter", format); const id = appSettings.value.formatter[name]; const formatter = this.#formatter.find(({ id: _id }) => _id === id); formatter?.format(); } } fsOperation(file) { return fsOperation(file); } newEditorFile(filename, options) { new EditorFile(filename, options); } get formatters() { return this.#formatter.map(({ id, name, exts }) => ({ id, name: name || id, exts, })); } /** * * @param {string[]} extensions * @returns {Array<[id: String, name: String]>} options */ getFormatterFor(extensions) { const options = [[null, strings.none]]; for (const { id, name, exts } of this.formatters) { const supports = exts.some((ext) => extensions.includes(ext)); if (supports || exts.includes("*")) { options.push([id, name]); } } return options; } alert(title, message, onhide) { alert(title, message, onhide); } loader(title, message, cancel) { return loader.create(title, message, cancel); } joinUrl(...args) { return Url.join(...args); } /** * Adds a custom icon class that can be used with the .icon element * @param {string} className - The class name for the icon (used as .icon.className) * @param {string} src - URL or data URI of the icon image * @param {object} [options] - Optional settings * @param {boolean} [options.monochrome=false] - If true, icon will use currentColor and adapt to theme */ addIcon(className, src, options = {}) { let style = document.head.get(`style[icon="${className}"]`); if (!style) { let css; if (options.monochrome) { // Monochrome icons: use mask-image (on ::before) for currentColor/theme support // Using ::before ensures we don't mask the ::after active indicator or the background css = `.icon.${className}::before { content: ''; display: inline-block; width: 24px; height: 24px; vertical-align: middle; -webkit-mask: url(${src}) no-repeat center / contain; mask: url(${src}) no-repeat center / contain; background-color: currentColor; }`; } else { // Default: preserve original icon colors css = `.icon.${className}{ background: url(${src}) no-repeat center / 24px; }`; } style = ; document.head.appendChild(style); } } async prompt(message, defaultValue, type, options) { const response = await prompt(message, defaultValue, type, options); return response; } async confirm(title, message) { const confirmation = await confirm(title, message); return confirmation; } async select(title, options, config) { const response = await select(title, options, config); return response; } async multiPrompt(title, inputs, help) { const values = await multiPrompt(title, inputs, help); return values; } async fileBrowser(mode, info, openLast) { const res = await FileBrowser(mode, info, openLast); return res; } async toInternalUrl(url) { const internalUrl = await helpers.toInternalUri(url); return internalUrl; } /** * Push a notification * @param {string} title Title of the notification * @param {string} message Message body of the notification * @param {Object} options Notification options * @param {string} [options.icon] Icon for the notification, can be a URL or a base64 encoded image or icon class or svg string * @param {boolean} [options.autoClose=true] Whether notification should auto close * @param {Function} [options.action=null] Action callback when notification is clicked * @param {('info'|'warning'|'error'|'success')} [options.type='info'] Type of notification */ pushNotification( title, message, { icon, autoClose = true, action = null, type = "info" } = {}, ) { const nm = new NotificationManager(); nm.pushNotification({ title, message, icon, autoClose, action, type, }); } /** * Register a custom file type handler * @param {string} id Unique identifier for the handler * @param {Object} options Handler configuration * @param {string[]} options.extensions File extensions to handle (without dots) * @param {function} options.handleFile Function that handles the file opening */ registerFileHandler(id, options) { fileTypeHandler.registerFileHandler(id, options); } /** * Unregister a file type handler * @param {string} id The handler id to remove */ unregisterFileHandler(id) { fileTypeHandler.unregisterFileHandler(id); } addCommand(descriptor) { const command = registerExternalCommand(descriptor); this.#refreshCommandBindings(); return command; } removeCommand(name) { if (!name) return; removeExternalCommand(name); this.#refreshCommandBindings(); } execCommand(name, view, args) { if (!name) return false; const targetView = view || window.editorManager?.editor; return runCommand(name, targetView, args); } listCommands() { return listRegisteredCommands(); } #refreshCommandBindings() { const view = window.editorManager?.editor; if (view) refreshCommandKeymap(view); } #createCommandApi() { const commandRegistry = { add: this.addCommand, execute: this.execCommand, remove: this.removeCommand, list: this.listCommands, }; const addCommand = (descriptor) => { try { return this.addCommand(descriptor); } catch (error) { console.error("Failed to add command", descriptor?.name); throw error; } }; const removeCommand = (name) => { if (!name) return; this.removeCommand(name); }; return { addCommand, removeCommand, get registry() { return commandRegistry; }, }; } } ================================================ FILE: src/lib/actionStack.js ================================================ import confirm from "dialogs/confirm"; import appSettings from "lib/settings"; import helpers from "utils/helpers"; const stack = []; let mark = null; let onCloseAppCallback; let freeze = false; export default { /** * Length of stack * @returns {number} */ get length() { return stack.length; }, /** * Function to be called when app is closed * @returns {Function} */ get onCloseApp() { return onCloseAppCallback; }, /** * Function to be called when app is closed * @param {Function} cb */ set onCloseApp(cb) { onCloseAppCallback = cb; }, /** * Copy of actionStack for window * @deprecated */ windowCopy() { const copyStack = { ...this }; delete copyStack.windowCopy; copyStack.pop = (repeat) => { window.log( "error", "Deprecated: `window.actionStack` is deprecated, import `actionStack` instead", ); this.pop(repeat); }; return copyStack; }, /** * Push action to stack * @param {object} fun * @param {string} fun.id * @param {Function} fun.action */ push(fun) { stack.push(fun); }, /** * Pop action from stack * @param {number} repeat pop action multiple times * @returns */ async pop(repeat) { if (freeze) return; let confirmation = true; if (typeof repeat === "number" && repeat > 1) { for (let i = 0; i < repeat; ++i) { this.pop(); } return; } const fun = stack.pop(); if (fun) { fun.action(); return; } if (appSettings.value.confirmOnExit) { let closeMessage = acode.exitAppMessage || strings["close app"].capitalize(0); confirmation = await confirm(strings.warning.toUpperCase(), closeMessage); } if (confirmation) { const { exitApp } = navigator.app; if (typeof onCloseAppCallback === "function") { const res = onCloseAppCallback(); if (res instanceof Promise) { res.finally(exitApp); return; } } helpers.showInterstitialIfReady(); exitApp(); } }, get(id) { return stack.find((act) => act.id === id); }, /** * Remove action with given id from stack * @param {String} id * @returns {Boolean} */ remove(id) { for (let i = 0; i < stack.length; ++i) { let action = stack[i]; if (action.id === id) { stack.splice(i, 1); return true; } } return false; }, /** * Check if action with given id exists in stack * @param {String} id * @returns {Boolean} */ has(id) { for (let act of stack) if (act.id === id) return true; return false; }, /** * Sets a mark to recently pushed action */ setMark() { mark = stack.length; }, /** * Remove all actions that are pushed after marked positions (using `setMark()`) */ clearFromMark() { if (mark === null) return; stack.splice(mark); mark = null; }, freeze() { freeze = true; }, unfreeze() { freeze = false; }, }; ================================================ FILE: src/lib/adRewards.js ================================================ import toast from "components/toast"; import auth from "./auth"; import secureAdRewardState from "./secureAdRewardState"; const ONE_HOUR = 60 * 60 * 1000; const MAX_TIMEOUT = 2_147_483_647; const REWARDED_RESULT_TIMEOUT_MS = 90 * 1000; const OFFERS = [ { id: "quick", title: "Quick pass", description: "Watch 1 rewarded ad and pause ads for 1 hour.", adsRequired: 1, durationMs: ONE_HOUR, accentClass: "is-quick", }, { id: "focus", title: "Focus block", description: "Watch 2 rewarded ads and pause ads for a random 4, 5, or 6 hours.", adsRequired: 2, minDurationMs: 4 * ONE_HOUR, maxDurationMs: 6 * ONE_HOUR, accentClass: "is-focus", }, ]; let state = getDefaultState(); let expiryTimer = null; let activeWatchPromise = null; const listeners = new Set(); function getDefaultState() { return { adFreeUntil: 0, lastExpiredRewardUntil: 0, isActive: false, remainingMs: 0, redemptionsToday: 0, remainingRedemptions: 3, maxRedemptionsPerDay: 3, maxActivePassMs: 10 * ONE_HOUR, hasPendingExpiryNotice: false, expiryNoticePendingUntil: 0, canRedeem: true, redeemDisabledReason: "", }; } function formatDuration(durationMs) { const totalHours = Math.round(durationMs / ONE_HOUR); if (totalHours < 1) return "less than 1 hour"; if (totalHours === 1) return "1 hour"; return `${totalHours} hours`; } function formatDurationRange(minDurationMs, maxDurationMs) { if (!minDurationMs || !maxDurationMs || minDurationMs === maxDurationMs) { return formatDuration(minDurationMs || maxDurationMs || 0); } const minHours = Math.round(minDurationMs / ONE_HOUR); const maxHours = Math.round(maxDurationMs / ONE_HOUR); return `${minHours}-${maxHours} hours`; } function getRewardedUnitId() { return window.adRewardedUnitId || ""; } function getExpiryDate() { return state.adFreeUntil ? new Date(state.adFreeUntil) : null; } function emitChange() { const snapshot = { ...state, expiryDate: getExpiryDate(), }; listeners.forEach((listener) => { try { listener(snapshot); } catch (error) { console.error("Reward state listener failed.", error); } }); } function hideActiveBanner() { if (window.ad?.active) { window.ad.active = false; window.ad.hide?.(); } } function notify(title, message, type = "info") { toast(message, 4000); window.acode?.pushNotification?.(title, message, { icon: type === "success" ? "verified" : "notifications", type, }); } function normalizeStatus(status) { const fallback = getDefaultState(); if (!status || typeof status !== "object") return fallback; const adFreeUntil = Number(status.adFreeUntil) || 0; const remainingMs = Math.max(0, Number(status.remainingMs) || 0); return { ...fallback, ...status, adFreeUntil, lastExpiredRewardUntil: Number(status.lastExpiredRewardUntil) || 0, remainingMs, redemptionsToday: Number(status.redemptionsToday) || 0, remainingRedemptions: Number(status.remainingRedemptions) || 0, maxRedemptionsPerDay: Number(status.maxRedemptionsPerDay) || fallback.maxRedemptionsPerDay, maxActivePassMs: Number(status.maxActivePassMs) || fallback.maxActivePassMs, expiryNoticePendingUntil: Number(status.expiryNoticePendingUntil) || 0, isActive: Boolean(status.isActive && adFreeUntil > Date.now()), hasPendingExpiryNotice: Boolean(status.hasPendingExpiryNotice), canRedeem: Boolean(status.canRedeem), redeemDisabledReason: String(status.redeemDisabledReason || ""), }; } function clearExpiryTimer() { if (expiryTimer) { clearTimeout(expiryTimer); expiryTimer = null; } } async function refreshState({ notifyExpiry = false } = {}) { try { const nextState = normalizeStatus(await secureAdRewardState.getStatus()); state = nextState; emitChange(); scheduleExpiryCheck(); if (notifyExpiry && nextState.hasPendingExpiryNotice) { notify( "Ad-free pass ended", "Your rewarded ad-free time has expired. You can watch another rewarded ad anytime.", "warning", ); } return nextState; } catch (error) { console.warn("Failed to refresh rewarded ad state.", error); return state; } } function scheduleExpiryCheck() { clearExpiryTimer(); if (!state.adFreeUntil) return; const remainingMs = state.adFreeUntil - Date.now(); if (remainingMs <= 0) { void refreshState({ notifyExpiry: true }); return; } expiryTimer = setTimeout( () => { void refreshState({ notifyExpiry: true }); }, Math.min(remainingMs, MAX_TIMEOUT), ); } 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); } catch (error) { console.warn("Failed to resolve rewarded ad user identity.", error); return String(device?.uuid || "guest"); } } async function createRewardedAd(offer, step, sessionId) { const rewardedUnitId = getRewardedUnitId(); if (!rewardedUnitId || !admob?.RewardedAd) { throw new Error("Rewarded ads are not available in this build."); } const userId = await getRewardIdentity(); const customData = [ `session=${sessionId}`, `offer=${offer.id}`, `step=${step}`, `ads=${offer.adsRequired}`, ].join("&"); return new admob.RewardedAd({ adUnitId: rewardedUnitId, serverSideVerification: { userId, customData, }, }); } function waitForRewardedResult(ad) { return new Promise((resolve, reject) => { let earned = false; let settled = false; const timeoutId = setTimeout(() => { fail( new Error("Rewarded ad timed out before completion. Please try again."), ); }, REWARDED_RESULT_TIMEOUT_MS); const finish = (result) => { if (settled) return; settled = true; clearTimeout(timeoutId); resolve(result); }; const fail = (error) => { if (settled) return; settled = true; clearTimeout(timeoutId); reject( error instanceof Error ? error : new Error(error?.message || "Rewarded ad failed."), ); }; ad.on("reward", () => { earned = true; }); ad.on("dismiss", () => { finish({ earned }); }); ad.on("showfail", fail); ad.on("loadfail", fail); }); } async function showRewardedStep(offer, step, sessionId) { const rewardedAd = await createRewardedAd(offer, step, sessionId); const resultPromise = waitForRewardedResult(rewardedAd); await rewardedAd.load(); await rewardedAd.show(); const result = await resultPromise; if (!result.earned) { throw new Error("Reward not earned. The ad was closed before completion."); } } export default { async init() { await refreshState({ notifyExpiry: false }); }, onChange(listener) { listeners.add(listener); return () => listeners.delete(listener); }, async handleResume() { await refreshState({ notifyExpiry: true }); }, getState() { return { ...state, expiryDate: getExpiryDate(), }; }, getOffers() { return OFFERS.map((offer) => ({ ...offer, durationLabel: formatDurationRange( offer.minDurationMs || offer.durationMs, offer.maxDurationMs || offer.durationMs, ), })); }, getRemainingMs() { return Math.max(0, state.remainingMs || state.adFreeUntil - Date.now()); }, getRemainingLabel() { const remainingMs = this.getRemainingMs(); if (!remainingMs) return "No active ad-free pass"; const minutes = Math.ceil(remainingMs / (60 * 1000)); if (minutes < 60) { return `${minutes} minute${minutes === 1 ? "" : "s"} remaining`; } const hours = Math.floor(minutes / 60); const remMinutes = minutes % 60; if (!remMinutes) { return `${hours} hour${hours === 1 ? "" : "s"} remaining`; } return `${hours}h ${remMinutes}m remaining`; }, getExpiryLabel() { const expiryDate = getExpiryDate(); if (!expiryDate) return "No active pass"; return expiryDate.toLocaleString(); }, isAdFreeActive() { return Boolean(state.isActive && state.adFreeUntil > Date.now()); }, canShowAds() { return Boolean(window.IS_FREE_VERSION && !this.isAdFreeActive()); }, isRewardedSupported() { return Boolean( window.IS_FREE_VERSION && admob?.RewardedAd && getRewardedUnitId(), ); }, getRewardedUnavailableReason() { if (!window.IS_FREE_VERSION) return "Ads are already disabled on this build."; if (!admob?.RewardedAd) return "Rewarded ads are unavailable on this device."; if (!getRewardedUnitId()) { return "Rewarded ads are not configured for production yet."; } return ""; }, canRedeemNow() { return { ok: Boolean(state.canRedeem), reason: state.redeemDisabledReason || "", }; }, isWatchingReward() { return Boolean(activeWatchPromise); }, async watchOffer(offerId, { onStep } = {}) { if (activeWatchPromise) { return activeWatchPromise; } const offer = OFFERS.find((item) => item.id === offerId); if (!offer) { throw new Error("Reward offer not found."); } if (!this.isRewardedSupported()) { throw new Error(this.getRewardedUnavailableReason()); } await refreshState({ notifyExpiry: false }); const redemptionStatus = this.canRedeemNow(); if (!redemptionStatus.ok) { throw new Error(redemptionStatus.reason); } const sessionId = typeof crypto?.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; activeWatchPromise = (async () => { for (let step = 1; step <= offer.adsRequired; step += 1) { onStep?.({ step, totalSteps: offer.adsRequired, offer, }); await showRewardedStep(offer, step, sessionId); if (step < offer.adsRequired) { toast( `Reward ${step}/${offer.adsRequired} complete. Loading the next ad...`, 2500, ); } } const redeemedState = normalizeStatus( await secureAdRewardState.redeem(offer.id), ); const grantedDurationMs = Number(redeemedState.appliedDurationMs) || Number(redeemedState.grantedDurationMs) || 0; state = redeemedState; emitChange(); hideActiveBanner(); scheduleExpiryCheck(); notify( "Ad-free pass started", `${formatDuration(grantedDurationMs)} unlocked. Ads will stay hidden until ${new Date(redeemedState.adFreeUntil).toLocaleString()}.`, "success", ); return { offer, expiresAt: redeemedState.adFreeUntil, grantedDurationMs, }; })().finally(() => { activeWatchPromise = null; emitChange(); }); emitChange(); return activeWatchPromise; }, }; ================================================ FILE: src/lib/applySettings.js ================================================ import actions from "../handlers/quickTools"; import appSettings from "../lib/settings"; import themes from "../theme/list"; import constants from "./constants"; import fonts from "./fonts"; export default { beforeRender() { //animation appSettings.applyAnimationSetting(); //full-screen if (appSettings.value.fullscreen) { acode.exec("enable-fullscreen"); } //setup vibration app.addEventListener("click", function (e) { const $target = e.target; if ($target.hasAttribute("vibrate") && appSettings.value.vibrateOnTap) { navigator.vibrate(constants.VIBRATION_TIME); } }); system.setInputType(appSettings.value.keyboardMode); // Keep native context menu enabled globally; editor manager scopes disabling to CodeMirror focus. system.setNativeContextMenuDisabled(false); }, afterRender() { const { value: settings } = appSettings; if (!settings.floatingButton) { root.classList.add("hide-floating-button"); } actions("set-height", settings.quickTools); fonts.setAppFont(settings.appFont); fonts.setEditorFont(settings.editorFont); if (!themes.applied) { themes.apply("dark"); } }, }; ================================================ FILE: src/lib/auth.js ================================================ import toast from "components/toast"; import { addIntentHandler } from "handlers/intent"; const loginEvents = { listeners: new Set(), emit(data) { for (const listener of this.listeners) { listener(data); } }, on(callback) { this.listeners.add(callback); }, off(callback) { this.listeners.delete(callback); }, }; class AuthService { constructor() { addIntentHandler(this.onIntentReceiver.bind(this)); } async onIntentReceiver(event) { try { if (event?.module === "user" && event?.action === "login") { if (event?.value) { this._exec("saveToken", [event.value]); toast("Logged in successfully"); setTimeout(() => { loginEvents.emit(); }, 500); } } return null; } catch (error) { console.error("Failed to parse intent token.", error); return null; } } /** * Helper to wrap cordova.exec in a Promise */ _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"; try { await new Promise((resolve, reject) => { CustomTabs.open(url, { showTitle: true }, resolve, reject); }); } catch (error) { console.error("CustomTabs failed, opening system browser.", error); system.openInBrowser(url); } } async logout() { try { await this._exec("logout"); return true; } catch (error) { console.error("Failed to logout.", error); return false; } } async isLoggedIn() { try { // Native checks EncryptedPrefs and validates with API internally await this._exec("isLoggedIn"); return true; } catch (error) { console.error(error); // error is typically the status code (0 if no token, 401 if invalid) 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; } } async getAvatar() { try { const userData = await this.getUserInfo(); if (!userData) return null; if (userData.github) { return `https://avatars.githubusercontent.com/${userData.github}`; } if (userData.name) { return this._generateInitialsAvatar(userData.name); } return null; } catch (error) { console.error("Failed to get avatar", error); return null; } } _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(); export { loginEvents }; ================================================ FILE: src/lib/checkFiles.js ================================================ import fsOperation from "fileSystem"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; let checkFileEnabled = true; Object.defineProperty(checkFiles, "check", { set(value) { checkFileEnabled = value; }, get() { return checkFileEnabled; }, }); export default async function checkFiles() { if (!editorManager) return; if (checkFileEnabled === false) { checkFileEnabled = true; return; } const files = editorManager.files; // @ts-check /** @type {{ editor: import('@codemirror/view').EditorView }} */ const { editor } = editorManager; recursiveFileCheck([...files]); /** * Checks if the file has been changed * @param {EditorFile[]} files List of files to check */ async function recursiveFileCheck(files) { const file = files.pop(); await checkFile(file); if (files.length) { recursiveFileCheck(files); } return; } /** * @typedef {import('./editorFile').default} EditorFile */ /** * Checks a file for changes * @param {EditorFile} file File to check * @returns {Promise} */ async function checkFile(file) { if (file === undefined || file.isUnsaved || !file.loaded || file.loading) return; if (file.uri) { const fs = fsOperation(file.uri); const exists = await fs.exists(); if (!exists && !file.readOnly) { file.isUnsaved = true; file.uri = null; editorManager.onupdate("file-changed"); editorManager.emit("update", "file-changed"); await new Promise((resolve) => { alert( strings.info, strings["file has been deleted"].replace("{file}", file.filename), resolve, ); }); return; } const text = await fs.readFile(file.encoding); const loadedText = file.session.doc.toString(); if (text !== loadedText) { try { const confirmation = await confirm( strings.warning.toUpperCase(), file.filename + strings["file changed"], ); if (!confirmation) return; const cursorPos = editor.getCursorPosition(); editorManager.getFile(file.id, "id")?.makeActive(); file.markChanged = false; file.session.setValue(text); editor.gotoLine(cursorPos.row, cursorPos.column); } catch (error) { // ignore } } } } if (!editorManager.activeFile) { app.focus(); } } ================================================ FILE: src/lib/checkPluginsUpdate.js ================================================ import ajax from "@deadlyjack/ajax"; import fsOperation from "../fileSystem"; import Url from "../utils/Url"; export default async function checkPluginsUpdate() { const plugins = await fsOperation(PLUGIN_DIR).lsDir(); const promises = []; const updates = []; plugins.forEach((pluginDir) => { promises.push( (async () => { const plugin = await fsOperation( Url.join(pluginDir.url, "plugin.json"), ).readFile("json"); const res = await ajax({ url: `https://acode.app/api/plugin/check-update/${plugin.id}/${plugin.version}`, }); if (res.update) { updates.push(plugin.id); } })(), ); }); await Promise.allSettled(promises); return updates; } ================================================ FILE: src/lib/commands.js ================================================ import fsOperation from "fileSystem"; import { selectAll } from "@codemirror/commands"; import Sidebar from "components/sidebar"; import { TerminalManager } from "components/terminal"; import color from "dialogs/color"; import confirm from "dialogs/confirm"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import actions from "handlers/quickTools"; import recents from "lib/recents"; import About from "pages/about"; import FileBrowser from "pages/fileBrowser"; import plugins from "pages/plugins"; import Problems from "pages/problems/problems"; import openWelcomeTab from "pages/welcome/welcome"; import changeEncoding from "palettes/changeEncoding"; import changeMode from "palettes/changeMode"; import changeTheme from "palettes/changeTheme"; import commandPalette from "palettes/commandPalette"; import findFile from "palettes/findFile"; import browser from "plugins/browser"; import help from "settings/helpSettings"; import mainSettings from "settings/mainSettings"; import { runAllTests } from "test/tester"; 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 EditorFile from "./editorFile"; import openFile from "./openFile"; import openFolder from "./openFolder"; import run from "./run"; import saveState from "./saveState"; import appSettings from "./settings"; import showFileInfo from "./showFileInfo"; export default { async "run-tests"() { await runAllTests(); }, async "close-all-tabs"() { const closableFiles = editorManager.files.filter((file) => !file.pinned); if (!closableFiles.length) return; let save = false; const unsavedFiles = closableFiles.filter((file) => file.isUnsaved).length; if (unsavedFiles) { const confirmation = await confirm( strings["warning"], strings["unsaved files warning"], ); if (!confirmation) return; const option = await select(strings["select"], [ ["save", strings["save all"]], ["close", strings["close all"]], ["cancel", strings["cancel"]], ]); if (option === "cancel") return; if (option === "save") { const doSave = await confirm( strings["warning"], strings["save all warning"], ); if (!doSave) return; save = true; } else { const doClose = await confirm( strings["warning"], strings["close all warning"], ); if (!doClose) return; } } for (const file of [...closableFiles]) { if (save) { await file.save(); await file.remove(true, { silentPinned: true }); continue; } await file.remove(true, { silentPinned: true }); } }, async "save-all-changes"() { const doSave = await confirm( strings["warning"], strings["save all changes warning"], ); if (!doSave) return; editorManager.files.forEach((file) => { file.save(); file.isUnsaved = false; }); }, "close-current-tab"() { editorManager.activeFile.remove(); }, "toggle-pin-tab"() { editorManager.activeFile?.togglePinned?.(); }, console() { run(true, "inapp"); }, "check-files"() { if (!appSettings.value.checkFiles) return; checkFiles(); }, "command-palette"() { commandPalette(); }, "disable-fullscreen"() { app.classList.remove("fullscreen-mode"); this["resize-editor"](); }, "enable-fullscreen"() { app.classList.add("fullscreen-mode"); this["resize-editor"](); }, encoding() { changeEncoding(); }, exit() { navigator.app.exitApp(); }, "edit-with"() { editorManager.activeFile.editWith(); }, "find-file"() { findFile(); }, files() { FileBrowser("both", strings["file browser"]) .then(FileBrowser.open) .catch(FileBrowser.openError); }, find() { actions("search"); }, "file-info"(url) { showFileInfo(url); }, async goto() { const lastLine = editorManager.editor?.state?.doc?.lines; const message = lastLine ? `${strings["enter line number"]} (1..${lastLine})` : strings["enter line number"]; const res = await prompt(message, "", "number", { placeholder: "line.column", }); if (!res) return; const [lineStr, colStr] = String(res).split("."); editorManager.editor.gotoLine(lineStr, colStr); }, async "new-file"() { let filename = await prompt(strings["enter file name"], "", "filename", { match: constants.FILE_NAME_REGEX, required: true, }); filename = helpers.fixFilename(filename); if (!filename) return; new EditorFile(filename, { isUnsaved: false, }); }, "next-file"() { const len = editorManager.files.length; let fileIndex = editorManager.files.indexOf(editorManager.activeFile); if (fileIndex === len - 1) fileIndex = 0; else ++fileIndex; editorManager.files[fileIndex].makeActive(); }, open(page) { switch (page) { case "settings": mainSettings(); break; case "help": help(); break; case "problems": Problems(); break; case "plugins": plugins(); break; case "file_browser": FileBrowser(); break; case "about": About(); break; default: return; } editorManager.editor.contentDOM.blur(); }, "open-with"() { editorManager.activeFile.openWith(); }, "open-file"() { editorManager.editor.contentDOM.blur(); FileBrowser("file") .then(FileBrowser.openFile) .catch(FileBrowser.openFileError); }, "open-folder"() { editorManager.editor.contentDOM.blur(); FileBrowser("folder") .then(FileBrowser.openFolder) .catch(FileBrowser.openFolderError); }, "prev-file"() { const len = editorManager.files.length; let fileIndex = editorManager.files.indexOf(editorManager.activeFile); if (fileIndex === 0) fileIndex = len - 1; else --fileIndex; editorManager.files[fileIndex].makeActive(); }, "read-only"() { const file = editorManager.activeFile; file.editable = !file.editable; }, recent() { recents.select().then((res) => { const { type } = res; if (helpers.isFile(type)) { openFile(res.val, { render: true, }).catch((err) => { helpers.error(err); }); } else if (helpers.isDir(type)) { openFolder(res.val.url, res.val.opts); } else if (res === "clear") { recents.clear(); } }); }, replace() { this.find(); }, "resize-editor"() { // TODO : Codemirror //editorManager.editor.resize(true); }, "open-inapp-browser"(url) { browser.open(url); }, run() { editorManager.activeFile[ appSettings.value.useCurrentFileForPreview ? "runFile" : "run" ]?.(); }, "run-file"() { editorManager.activeFile.runFile?.(); }, async save(showToast) { try { await editorManager.activeFile.save(); if (showToast) { toast(strings["file saved"]); } } catch (error) { helpers.error(error); } }, async "save-as"(showToast) { try { await editorManager.activeFile.saveAs(); if (showToast) { toast(strings["file saved"]); } } catch (error) { helpers.error(error); } }, "save-state"() { saveState(); }, share() { editorManager.activeFile.share(); }, async "pin-file-shortcut"() { const file = editorManager.activeFile; if (!file?.uri) { toast(strings["save file before home shortcut"]); return; } if (typeof system?.pinFileShortcut !== "function") { toast(strings["pin shortcuts not supported"]); return; } const { uri, filename } = file; const label = filename; const description = filename; let id = uri.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); if (!id) { id = helpers.uuid(); } if (id.length > 40) { id = id.slice(-40); } id = `file-${id}`; const shortcut = { id, label, description, uri, }; const requestShortcut = new Promise((resolve, reject) => { system.pinFileShortcut( shortcut, () => resolve(true), (err) => reject(err), ); }); try { await requestShortcut; toast(strings["shortcut request sent"]); } catch (error) { if ( typeof error === "string" && error.toLowerCase().includes("not supported") ) { toast(strings["pin shortcuts not supported"]); return; } helpers.error(error); } }, syntax() { changeMode(); }, "change-app-theme"() { changeTheme("app"); }, "change-editor-theme"() { changeTheme("editor"); }, "toggle-fullscreen"() { app.classList.toggle("fullscreen-mode"); this["resize-editor"](); }, "toggle-sidebar"() { Sidebar.toggle(); }, "toggle-menu"() { tag.get("[action=toggle-menu]")?.click(); }, "toggle-editmenu"() { tag.get("[action=toggle-edit-menu")?.click(); }, async "insert-color"() { const { editor } = editorManager; const range = getColorRange(); let defaultColor = ""; if (range) { try { defaultColor = editor.state.doc.sliceString(range.from, range.to); } catch (_) { defaultColor = ""; } } editor.contentDOM.blur(); const wasFocused = editorManager.activeFile.focused; const res = await color(defaultColor, () => { if (wasFocused) { editor.focus(); } }); if (range) { editor.dispatch({ changes: { from: range.from, to: range.to, insert: res }, }); return; } editor.insert(res); }, copy() { editorManager.editor.execCommand("copy"); }, cut() { editorManager.editor.execCommand("cut"); }, paste() { editorManager.editor.execCommand("paste"); }, "select-all"() { const { editor } = editorManager; selectAll(editor); }, async rename(file) { file = file || editorManager.activeFile; if (file.mode === "single") { alert(strings.info.toUpperCase(), strings["unable to rename"]); return; } let newname = await prompt(strings.rename, file.filename, "filename", { match: constants.FILE_NAME_REGEX, capitalize: false, }); newname = helpers.fixFilename(newname); if (!newname || newname === file.filename) return; const { uri } = file; if (uri) { const fs = fsOperation(uri); try { let newUri; if (uri.startsWith("content://com.termux.documents/tree/")) { // Special handling for Termux content files const newFilePath = Url.join(Url.dirname(uri), newname); const content = await fs.readFile(); await fsOperation(Url.dirname(uri)).createFile(newname, content); await fs.delete(); newUri = newFilePath; } else { newUri = await fs.renameTo(newname); } const stat = await fsOperation(newUri).stat(); newname = stat.name; file.uri = newUri; file.filename = newname; openFolder.renameItem(uri, newUri, newname); toast(strings["file renamed"]); } catch (err) { helpers.error(err); } } else { file.filename = newname; } }, async format(selectIfNull) { const { editor } = editorManager; const pos = editor.getCursorPosition(); const didFormat = await acode.format(selectIfNull); if (didFormat) { // Restore cursor position after formatting (pos.row is now 1-based) editor.gotoLine(pos.row, pos.column); } }, async eol() { const eol = await select(strings["new line mode"], ["unix", "windows"], { default: editorManager.activeFile.eol, }); editorManager.activeFile.eol = eol; }, "open-log-file"() { openFile(Url.join(DATA_STORAGE, constants.LOG_FILE_NAME)); }, "copy-device-info"() { let webviewInfo = {}; let appInfo = {}; const getWebviewInfo = new Promise((resolve, reject) => { system.getWebviewInfo( (res) => { webviewInfo = res; resolve(); }, (error) => { console.error("Error getting WebView info:", error); reject(error); }, ); }); const getAppInfo = new Promise((resolve, reject) => { system.getAppInfo( (res) => { appInfo = res; resolve(); }, (error) => { console.error("Error getting app info:", error); reject(error); }, ); }); Promise.all([getWebviewInfo, getAppInfo]) .then(() => { let info = `Device Information: WebView Info: Package Name: ${webviewInfo?.packageName || "N/A"} Version: ${webviewInfo?.versionName || "N/A"} App Info: Name: ${appInfo?.label || "N/A"} Package Name: ${appInfo?.packageName || "N/A"} Version: ${appInfo?.versionName || "N/A"} Version Code: ${appInfo?.versionCode || "N/A"} Device Info: Android Version: ${device?.version || "N/A"} Manufacturer: ${device?.manufacturer || "N/A"} Model: ${device?.model || "N/A"} Platform: ${device?.platform || "N/A"} Cordova Version: ${device?.cordova || "N/A"} Screen Info: Width: ${screen?.width || "N/A"} Height: ${screen?.height || "N/A"} Color Depth: ${screen?.colorDepth || "N/A"} Additional Info: Language: ${navigator?.language || "N/A"} User Agent: ${navigator?.userAgent || "N/A"} `; // Copy the info to clipboard if (cordova.plugins.clipboard) { cordova.plugins.clipboard.copy(info); toast(strings["copied to clipboard"]); } }) .catch((error) => { console.error("Error getting device info:", error); toast("Failed to get device info"); }); }, async "new-terminal"() { try { await TerminalManager.createServerTerminal(); } catch (error) { console.error("Failed to create terminal:", error); window.toast("Failed to create terminal"); } }, welcome() { openWelcomeTab(); }, async "toggle-inspector"() { const devTools = (await import("lib/devTools")).default; devTools.toggle(); }, async "open-inspector"() { const devTools = (await import("lib/devTools")).default; devTools.show(); }, async "lsp-info"() { const { showLspInfoDialog } = await import("components/lspInfoDialog"); showLspInfoDialog(); }, }; ================================================ FILE: src/lib/console.js ================================================ import "core-js/stable"; import "html-tag-js/dist/polyfill"; import { parse } from "acorn"; import css from "styles/console.m.scss"; import loadPolyFill from "utils/polyfill"; (function () { loadPolyFill.apply(window); let consoleVisible = false; const originalConsole = console; const $input = tag("textarea", { id: "__c-input", onblur() { setTimeout(() => { isFocused = false; }, 0); }, }); const $inputContainer = tag("c-input", { child: $input, }); const $toggler = tag("c-toggler", { style: { transform: `translate(2px, ${innerHeight / 2}px)`, }, onclick() { consoleVisible = !consoleVisible; if (consoleVisible) { showConsole(); } else { hideConsole(); } }, ontouchstart() { document.addEventListener("touchmove", touchmove, { passive: false, }); document.ontouchend = function (e) { document.removeEventListener("touchmove", touchmove, { passive: "false", }); document.ontouchend = null; }; }, }); const $console = tag("c-console", { child: $inputContainer, onclick(e) { const el = e.target; const action = el.getAttribute("action"); switch (action) { case "use code": const value = el.getAttribute("data-code"); $input.value = value; $input.focus(); break; default: break; } }, }); const counter = {}; const timers = {}; let isFocused = false; if (!window.__objs) window.__objs = {}; if (!tag.get("c-console")) { const $style = tag("style"); $style.textContent = css; document.head.append($style); window.addEventListener("error", onError); assignCustomConsole(); if (sessionStorage.getItem("__mode") === "console") { showConsole(); return; } tag.get("html").append($toggler); $console.setAttribute("title", "Console"); sessionStorage.setItem("__console_available", true); document.addEventListener("showconsole", showConsole); document.addEventListener("hideconsole", hideConsole); } function touchmove(e) { e.preventDefault(); $toggler.style.transform = "translate(" .concat(e.touches[0].clientX - 20, "px, ") .concat(e.touches[0].clientY - 20, "px)"); } function assignCustomConsole() { window.console = { assert(condition, msg, ...substitution) { originalConsole.assert(condition, msg, ...substitution); if (!condition) { log("error", getStack(new Error()), msg, ...substitution); } }, clear() { originalConsole.clear(); if (isFocused) $input.focus(); $console.textContent = ""; $console.appendChild($inputContainer); }, count(hash = "default") { originalConsole.count(hash); if (!counter[hash]) { counter[hash] = 1; } else { ++counter[hash]; } log("log", getStack(new Error()), `${hash}: ${counter[hash]}`); }, countReset(hash) { originalConsole.countReset(hash); delete counter[hash]; }, debug(...args) { originalConsole.debug(...args); log("log", getStack(new Error()), ...args); }, dir(...args) { originalConsole.dir(...args); log("log", getStack(new Error()), ...args); }, dirxml(...args) { originalConsole.dirxml(...args); log("log", getStack(new Error()), ...args); }, error(...args) { originalConsole.error(...args); log("error", getStack(new Error()), ...args); }, group(...args) { originalConsole.group(...args); log("log", getStack(new Error()), ...args); }, groupCollapsed(...args) { originalConsole.groupCollapsed(...args); log("log", getStack(new Error()), ...args); }, groupEnd(...args) { originalConsole.groupEnd(...args); log("log", getStack(new Error()), ...args); }, info(...args) { originalConsole.info(...args); log("info", getStack(new Error()), ...args); }, log(msg, ...substitution) { originalConsole.log(msg, ...substitution); log("log", getStack(new Error()), msg, ...substitution); }, table(...args) { originalConsole.table(...args); log("log", getStack(new Error()), ...args); }, time(label = "default") { originalConsole.time(label); if (typeof label !== "string") { throw new TypeError("label must be a string"); } timers[label] = new Date().getTime(); }, timeEnd(label = "default") { originalConsole.timeEnd(label); if (typeof label !== "string") { throw new TypeError("label must be a string"); } if (!timers[label]) { throw new Error(`No such label: ${label}`); } const time = new Date().getTime() - timers[label]; log("log", getStack(new Error()), `${label}: ${time}ms`); delete timers[label]; }, timeLog(label = "default") { originalConsole.timeLog(label); if (typeof label !== "string") { throw new TypeError("label must be a string"); } if (!timers[label]) { throw new Error(`No such label: ${label}`); } const time = new Date().getTime() - timers[label]; log("log", getStack(new Error()), `${label}: ${time}ms`); }, trace(...args) { originalConsole.trace(...args); log("trace", getStack(new Error()), ...args); }, warn(msg, ...substitution) { originalConsole.warn(msg, ...substitution); log("warn", getStack(new Error()), msg, ...substitution); }, }; } function showConsole() { tag.get("html").append($console); $input.addEventListener("keydown", onCodeInput); } function hideConsole() { $console.remove(); $input.removeEventListener("keydown", onCodeInput); } function onCodeInput(e) { const key = e.key; isFocused = true; if (key === "Enter") { const regex = /[\[|{\(\)\}\]]/g; let code = this.value.trim(); let isOdd = (code.length - code.replace(regex, "").length) % 2; if (!code || isOdd) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); log("code", {}, code); $input.value = ""; const res = execute(code); if (res.type === "error") { log("error", getStack(new Error()), res.value); } else { log("log", getStack(new Error()), res.value); } } } function getBody(obj, ...keys) { if (obj instanceof Promise && !("[[PromiseStatus]]" in obj)) obj = getPromiseStatus(obj); let value = objValue(obj, ...keys); const $group = tag("c-group"); const $toggler = tag("c-type", { attr: { type: "body-toggler", }, textContent: value ? value.constructor.name : value + "", }); if (value instanceof Object) { $toggler.onclick = function () { if (this.classList.contains("__show-data")) { this.classList.remove("__show-data"); $group.textContent = null; return; } this.classList.toggle("__show-data"); const possibleKeys = []; for (let key in value) { possibleKeys.push(key); } possibleKeys.push( ...[ ...Object.keys(value), ...Object.getOwnPropertyNames(value), ...Object.keys(value["__proto__"] || {}), ], ); if (value["__proto__"]) possibleKeys.push("__proto__"); if (value["prototype"]) possibleKeys.push("prototype"); [...new Set(possibleKeys)].forEach((key) => $group.append(appendProperties(obj, ...keys, key)), ); }; $toggler.textContent = value.constructor.name; } else { const $val = getElement(value); $val.textContent = (value ?? value + "").toString(); $group.append($val); } return [$toggler, $group]; } function appendProperties(obj, ...keys) { const key = keys.pop(); const value = objValue(obj, ...keys); const getter = value.__lookupGetter__(key); const $key = tag("c-key", { textContent: key + ":", }); let $val; if (getter) { $val = tag("c-span", { style: { textDecoration: "underline", color: "#39f", margin: "0 10px", }, textContent: `...`, onclick() { const $val = getVal(value[key]); this.parentElement.replaceChild($val, this); }, }); } else { $val = getVal(value[key]); } return tag("c-line", { children: [$key, $val], }); function getVal(val) { const type = typeof val; const $val = getElement(type); if (type === "object" && val !== null) { $val.append(...getBody(obj, ...keys, key)); } else { if (type === "function") { val = parseFunction(val); } $val.textContent = val + ""; } return $val; } } function objValue(obj, ...keys) { return keys.reduce((acc, key) => acc[key], obj); } function getPromiseStatus(obj) { if (obj.info) return; let status = "pending"; let value; let result = obj.then( (val) => { status = "resolved"; value = val; }, (val) => { status = "rejected"; value = val; }, ); Object.defineProperties(result, { "[[PromiseStatus]]": { get: () => status, }, "[[PromiseValue]]": { get: () => value, }, }); return result; } function getElement(type) { return tag("c-text", { className: `__c-${type}`, }); } /** @type {import("acorn").Options} */ const acornOptions = { ecmaVersion: "latest", }; function parseFunction(data) { let parsed; let str; try { parsed = parse(data.toString(), acornOptions).body[0]; } catch (error) { try { const fun = ("(" + data.toString() + ")").replace(/\{.*\}/, "{}"); parsed = parse(fun, acornOptions).body[0]; } catch (error) { return data .toString() .replace(/({).*(})/, "$1...$2") .replace(/^function\s+[\w_$\d]+\s*/, "") .replace(/\s*/g, ""); } } if (parsed.type === "ExpressionStatement") { const expression = parsed.expression; if (expression.type === "ArrowFunctionExpression") { str = joinParams(expression.params, "arrow"); } else if (expression.type === "FunctionExpression") { str = joinParams(expression.params); } } else { let string = parsed.id.name + joinParams(parsed.params || []); str = string; } function joinParams(params, type) { let parameter = "("; params.map( (param) => (parameter += param.type === "RestElement" ? "..." + param.argument.name : param.name + ","), ); parameter = parameter.replace(/,$/, ""); parameter += ")" + (type === "arrow" ? "=>" : "") + "{...}"; return parameter; } return str; } /** * Prints to the console. * @param {'log'|'error'|'warn'|'code'|'trace'|'table'} mode * @param {{stack: string, location: string}} options * @param {...any} args */ function log(mode, options, ...args) { let location = options.location || "console"; const $messages = tag("c-message", { attr: { "log-level": mode, }, }); args = format(args); if (args.length === 1 && args[0] instanceof Error) { args.unshift(args[0].message); } for (let arg of args) { const typeofArg = typeof arg; arg = arg ?? arg + ""; let $msg; if (mode === "code") { $msg = tag("c-code"); $msg.textContent = arg.length > 50 ? arg.substring(0, 50) + "..." : arg; $msg.setAttribute("data-code", arg); $msg.setAttribute("action", "use code"); } else { $msg = getElement(typeofArg); switch (typeofArg) { case "object": $msg.append(...getBody(arg)); break; case "function": $msg.innerHTML = parseFunction(arg); $msg.append( tag("c-line", { children: getBody(arg), }), ); break; default: $msg.textContent = arg; break; } } $messages.appendChild($msg); } if (location) { const $stack = tag("c-stack"); $stack.innerHTML = `${new Date().toLocaleString()}${location}`; $messages.appendChild($stack); } $console.insertBefore($messages, $inputContainer); while ($console.childElementCount > 100) { $console.firstElementChild.remove(); } } /** * * @param {Array} args * @returns */ function format(args) { if (args.length <= 1) return [args[0]]; const originalArgs = [].concat(args); const styles = []; let msg = args.splice(0, 1)[0]; if (typeof msg !== "string") return originalArgs; let matched = matchRegex(msg); let match; while ((match = matched.next())) { if (match.done) break; let value = ""; const specifier = match.value[0]; const pos = match.value.index; if (!args.length) { value = specifier; } else { value = args.splice(0, 1)[0]; if ([undefined, null].includes(value)) { value = value + ""; } switch (specifier) { case "%c": styles.push({ value, pos, }); value = ""; break; case "%s": if (typeof value === "object") { value = value.constructor.name; } break; case "%o": case "%O": let id = new Date().getMilliseconds() + ""; window.__objs[id] = value; value = `Object`; break; case "%d": case "%i": value = Number.parseInt(value); break; case "%f": value = Number.parseFloat(value); break; default: break; } } // Only escape HTML for the %o/%O case where we're injecting actual HTML const escapedValue = specifier === "%o" || specifier === "%O" ? value : escapeHTML(value); msg = msg.substring(0, pos) + escapedValue + msg.substring(pos + 2); matched = matchRegex(msg); } if (styles.length) { const toBeStyled = []; let remainingMsg = msg; styles.reverse().forEach((style, i) => { toBeStyled.push(remainingMsg.substring(style.pos)); remainingMsg = msg.substring(0, style.pos); if (i === styles.length - 1) toBeStyled.push(msg.substring(0, style.pos)); }); msg = toBeStyled .map((str, i) => { if (i === toBeStyled.length - 1) return str; const { value } = styles[i]; return `${str}`; }) .reverse() .join(""); } msg.replace(/%%[oOsdifc]/g, "%"); args.unshift(msg); return args; /** * * @param {string} str * @returns {IterableIterator} */ function matchRegex(str) { return str.matchAll(/(?:(\d+):(\d+)/.exec(stack[1]) || []; if (!regExecRes.length) { const errorInfo = stack[1]?.split("/").pop(); regExecRes = /(.+):(\d+):(\d+)/.exec(errorInfo) || []; } let src = ""; const location = regExecRes[1]; const lineno = regExecRes[2]; const colno = regExecRes[3]; if (location && lineno) { src = escapeHTML(`${location} ${lineno}${colno ? ":" + colno : ""}`); } else { const res = /\((.*)\)/.exec(stack[1]); src = res && res[1] ? res[1] : ""; } const index = src.indexOf(")"); src = src .split("/") .pop() .substring(0, index < 0 ? undefined : index); if (src.length > 50) src = "..." + src.substring(src.length - 50); return { location: src, stack: stack.join("\n"), }; } function execute(code) { let res = null; try { const parsed = parse(code, acornOptions).body; res = execParsedCode(parsed); } catch (e) { res = execParsedCode([]); } return res; function execParsedCode(parsed) { let extra = ""; parsed.map((st) => { if (st.type === "VariableDeclaration") { if (["const", "let"].indexOf(st.kind) < 0) return; const exCode = code.substring(st.start, st.end) + ";"; extra += exCode; } }); if (extra) { const script = tag("script"); script.textContent = extra; document.body.appendChild(script); document.body.removeChild(script); return exec(code); } else { return exec(code); } } function exec(code) { let res = null; try { res = { type: "result", value: window.eval(code) }; } catch (error) { res = { type: "error", value: error }; } return res; } } function onError(err) { const error = err.error; log("error", getStack(error, true), error); } function escapeHTML(str) { if (typeof str !== "string") return str; return tag("textarea", { textContent: str, }).innerHTML; } })(); ================================================ FILE: src/lib/constants.js ================================================ export default { FILE_NAME_REGEX: /^((?![:<>"\\\|\?\*]).)*$/, FONT_SIZE: /^[0-9\.]{1,3}(px|rem|em|pt|mm|pc|in)$/, DEFAULT_FILE_SESSION: "default-session", DEFAULT_FILE_NAME: "untitled.txt", CONSOLE_PORT: 8159, SERVER_PORT: 8158, PREVIEW_PORT: 8158, VIBRATION_TIME: 30, VIBRATION_TIME_LONG: 150, SCROLL_SPEED_FAST_X2: "FAST_X2", SCROLL_SPEED_NORMAL: "NORMAL", SCROLL_SPEED_FAST: "FAST", SCROLL_SPEED_SLOW: "SLOW", SIDEBAR_SLIDE_START_THRESHOLD_PX: 20, 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 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", }; ================================================ FILE: src/lib/devTools.js ================================================ 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"; let erudaInstance = null; let isInitialized = false; /** * Developer tools module for debugging Acode */ const devTools = { /** * Check if Eruda is initialized * @returns {boolean} */ get isInitialized() { return isInitialized; }, /** * Get the Eruda instance * @returns {object|null} */ get eruda() { return erudaInstance; }, /** * Initialize Eruda for developer mode * @param {boolean} showLoader - Whether to show a loading dialog * @returns {Promise} */ async init(showLoader = false) { if (isInitialized) return; try { const erudaPath = Url.join(DATA_STORAGE, "eruda.js"); const fs = fsOperation(erudaPath); if (!(await fs.exists())) { if (showLoader) { loader.create( strings["downloading file"]?.replace("{file}", "eruda.js") || "Downloading eruda.js...", strings["downloading..."] || "Downloading...", ); } try { const erudaScript = await ajax({ url: constants.ERUDA_CDN, responseType: "text", contentType: "application/x-www-form-urlencoded", }); await fsOperation(DATA_STORAGE).createFile("eruda.js", erudaScript); } finally { if (showLoader) loader.destroy(); } } const internalUri = await helpers.toInternalUri(erudaPath); await new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = internalUri; script.id = "eruda-script"; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); if (window.eruda) { window.eruda.init({ useShadowDom: true, autoScale: true, defaults: { displaySize: 50, }, }); window.eruda._shadowRoot.querySelector( ".eruda-entry-btn", ).style.display = "none"; erudaInstance = window.eruda; isInitialized = true; } } catch (error) { console.error("Failed to initialize developer tools", error); throw error; } }, /** * Show the inspector panel */ show() { if (!isInitialized) { window.toast?.("Developer mode is not enabled"); return; } const entryBtn = erudaInstance?._shadowRoot?.querySelector(".eruda-entry-btn"); if (entryBtn) entryBtn.style.display = ""; erudaInstance?.show(); }, /** * Hide the inspector panel */ hide() { if (!isInitialized) return; erudaInstance?.hide(); const entryBtn = erudaInstance?._shadowRoot?.querySelector(".eruda-entry-btn"); if (entryBtn) entryBtn.style.display = "none"; }, /** * Toggle the inspector panel visibility */ toggle() { if (!isInitialized) { window.toast?.("Developer mode is not enabled"); return; } if (erudaInstance?._isShow) { this.hide(); } else { this.show(); } }, /** * Destroy Eruda instance */ destroy() { if (!isInitialized) return; erudaInstance?.destroy(); erudaInstance = null; isInitialized = false; const script = document.getElementById("eruda-script"); if (script) script.remove(); }, }; export default devTools; ================================================ FILE: src/lib/editorFile.js ================================================ import fsOperation from "fileSystem"; // CodeMirror imports for document state management import { EditorState, Text } from "@codemirror/state"; import { clearSelection, restoreFolds, restoreSelection, setScrollPosition, } from "cm/editorUtils"; import { getMode, getModeForPath } from "cm/modelist"; import Sidebar from "components/sidebar"; import tile from "components/tile"; import toast from "components/toast"; import confirm from "dialogs/confirm"; import DOMPurify from "dompurify"; import startDrag from "handlers/editorFileTab"; import actions from "handlers/quickTools"; import tag from "html-tag-js"; 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 openFolder from "./openFolder"; import run from "./run"; import saveFile from "./saveFile"; import appSettings from "./settings"; /** * Creates a Proxy around an EditorState that provides Ace-compatible methods. * @param {EditorState} state - The raw CodeMirror EditorState * @param {EditorFile} file - The parent EditorFile instance * @returns {Proxy} Proxied state with Ace-compatible methods */ function createSessionProxy(state, file) { if (!state) return null; /** * Convert Ace position {row, column} to CodeMirror offset */ function positionToOffset(pos, doc) { if (!pos || !doc) return 0; try { const lineNum = Math.max(1, Math.min((pos.row ?? 0) + 1, doc.lines)); const line = doc.line(lineNum); const col = Math.max(0, Math.min(pos.column ?? 0, line.length)); return line.from + col; } catch (_) { return 0; } } /** * Convert CodeMirror offset to Ace position {row, column} */ function offsetToPosition(offset, doc) { if (!doc) return { row: 0, column: 0 }; try { const line = doc.lineAt(offset); return { row: line.number - 1, column: offset - line.from }; } catch (_) { return { row: 0, column: 0 }; } } return new Proxy(state, { get(target, prop) { // Ace-compatible method: getValue() if (prop === "getValue") { return () => target.doc.toString(); } // Ace-compatible method: setValue(text) if (prop === "setValue") { return (text) => { const newText = String(text ?? ""); const { activeFile, editor } = editorManager; if (activeFile?.id === file.id && editor) { // Active file: dispatch to live EditorView editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: newText, }, }); } else { // Inactive file: update stored state file._setRawSession( target.update({ changes: { from: 0, to: target.doc.length, insert: newText }, }).state, ); } }; } // Ace-compatible method: getLine(row) if (prop === "getLine") { return (row) => { try { return target.doc.line(row + 1).text; } catch (_) { return ""; } }; } // Ace-compatible method: getLength() if (prop === "getLength") { return () => target.doc.lines; } // Ace-compatible method: getTextRange(range) if (prop === "getTextRange") { return (range) => { if (!range) return ""; try { const from = positionToOffset(range.start, target.doc); const to = positionToOffset(range.end, target.doc); return target.doc.sliceString(from, to); } catch (_) { return ""; } }; } // Ace-compatible method: insert(position, text) if (prop === "insert") { return (position, text) => { const { activeFile, editor } = editorManager; const offset = positionToOffset(position, target.doc); if (activeFile?.id === file.id && editor) { editor.dispatch({ changes: { from: offset, insert: String(text ?? "") }, }); } else { file._setRawSession( target.update({ changes: { from: offset, insert: String(text ?? "") }, }).state, ); } }; } // Ace-compatible method: remove(range) if (prop === "remove") { return (range) => { if (!range) return ""; const from = positionToOffset(range.start, target.doc); const to = positionToOffset(range.end, target.doc); const removed = target.doc.sliceString(from, to); const { activeFile, editor } = editorManager; if (activeFile?.id === file.id && editor) { editor.dispatch({ changes: { from, to, insert: "" } }); } else { file._setRawSession( target.update({ changes: { from, to, insert: "" } }).state, ); } return removed; }; } // Ace-compatible method: replace(range, text) if (prop === "replace") { return (range, text) => { if (!range) return; const from = positionToOffset(range.start, target.doc); const to = positionToOffset(range.end, target.doc); const { activeFile, editor } = editorManager; if (activeFile?.id === file.id && editor) { editor.dispatch({ changes: { from, to, insert: String(text ?? "") }, }); } else { file._setRawSession( target.update({ changes: { from, to, insert: String(text ?? "") }, }).state, ); } }; } // Ace-compatible method: getWordRange(row, column) if (prop === "getWordRange") { return (row, column) => { const offset = positionToOffset({ row, column }, target.doc); const word = target.wordAt(offset); if (word) { return { start: offsetToPosition(word.from, target.doc), end: offsetToPosition(word.to, target.doc), }; } return { start: { row, column }, end: { row, column } }; }; } // Pass through all other properties to the real EditorState const value = target[prop]; if (typeof value === "function") { return value.bind(target); } return value; }, }); } /** * @typedef {'run'|'save'|'change'|'focus'|'blur'|'close'|'rename'|'load'|'loadError'|'loadStart'|'loadEnd'|'changeMode'|'changeEncoding'|'changeReadOnly'} FileEvents */ /** * @typedef {object} FileOptions new file options * @property {boolean} [isUnsaved] weather file needs to saved * @property {render} [render] make file active * @property {string} [id] ID for the file * @property {string} [uri] uri of the file * @property {string} [text] session text * @property {boolean} [editable] enable file to edit or not * @property {boolean} [deletedFile] file do not exists at source * @property {'single' | 'tree'} [SAFMode] storage access framework mode * @property {string} [encoding] text encoding * @property {object} [cursorPos] cursor position * @property {number} [scrollLeft] scroll left * @property {number} [scrollTop] scroll top * @property {Array} [folds] folds * @property {boolean} [pinned] pin the tab to prevent accidental closing */ export default class EditorFile { /** * Type of content this file represents but use page in case of custom pages etc */ #type = "editor"; #tabIcon = "file file_type_default"; /** * Custom content element * @type {HTMLElement} */ #content = null; /** * Whether to hide quicktools for this tab * @type {boolean} */ hideQuickTools = false; /** * Custom stylesheets for tab * @type {string|string[]} */ stylesheets; /** * Custom title function for special tab types * @type {function} */ #customTitleFn = null; /** * If editor was focused before resize */ focusedBefore = false; /** * State of the editor for this file. */ focused = false; /** * Weather the file has completed loading text or not * @type {boolean} */ loaded = true; /** * Weather file is still loading the text from the source * @type {boolean} */ loading = false; /** * Weather file is deleted from source. * @type {boolean} */ deletedFile = false; /** * Raw CodeMirror EditorState. Use session getter to access with Ace-compatible methods. * @type {EditorState} */ #rawSession = null; /** * Encoding of the text e.g. 'gbk' * @type {string} */ encoding = appSettings.value.defaultFileEncoding; /** * Weather file is readonly * @type {boolean} */ readOnly = false; /** * mark change when session text is changed * @type {boolean} */ markChanged = true; /** * Storage access framework file mode * @type {'single' | 'tree' | null} */ #SAFMode = null; /** * Name of the file * @type {string} */ #name = constants.DEFAULT_FILE_NAME; /** * Location of the file * @type {string} */ #uri; /** * Unique ID of the file, changed when file is renamed or location/uri is changed. * @type {string} */ #id = constants.DEFAULT_FILE_SESSION; /** * Associated tile for the file, that is append in the open file list, * when clicked make the file active. * @type {HTMLElement} */ #tab; /** * Weather file can be edited or not * @type {boolean} */ #editable = true; /** * Prevents the tab from being closed until it is unpinned. * @type {boolean} */ #pinned = false; /** * contains information about cursor position, scroll left, scroll top, folds. */ #loadOptions; /** * Weather file is changed and needs to be saved * @type {boolean} */ #isUnsaved = false; /** * Whether to show run button or not */ #canRun = Promise.resolve(false); /** * @type {function} event handler */ #onFilePosChange; #events = { save: [], change: [], focus: [], blur: [], close: [], rename: [], load: [], loaderror: [], loadstart: [], loadend: [], changemode: [], run: [], canrun: [], }; onsave; onchange; onfocus; onblur; onclose; onrename; onload; onloaderror; onloadstart; onloadend; onchangemode; onrun; oncanrun; onpinstatechange; /** * * @param {string} [filename] name of file. * @param {FileOptions} [options] file create options */ constructor(filename, options) { const { addFile, getFile } = editorManager; let doesExists = null; this.hideQuickTools = options?.hideQuickTools || false; // if options are passed if (options) { // if options doesn't contains id, and provide a new id if (!options.id) { if (options.uri) this.#id = options.uri.hashCode(); else this.#id = helpers.uuid(); } 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; } if (options?.type) { this.#type = options.type; if (this.#type !== "editor") { let container; let shadow; if (this.#type === "terminal") { container = tag("div", { className: "tab-page-container", }); const content = tag("div", { className: "tab-page-content", }); content.appendChild(options?.content); container.appendChild(content); this.#content = container; } else { container =
        ; // shadow dom shadow = container.attachShadow({ mode: "open" }); // Add base styles to shadow DOM first shadow.appendChild(); // Handle custom stylesheets if provided if (options.stylesheets) { this.#addCustomStyles(options.stylesheets, shadow); } const content =
        ; if (typeof options.content === "string") { content.innerHTML = DOMPurify.sanitize(options.content); } else { content.appendChild(options.content); } // Append content container to shadow DOM shadow.appendChild(content); this.#content = container; } } else { this.#content = options.content; } if (options.tabIcon) { this.#tabIcon = options.tabIcon; } } this.#uri = options?.uri; if (this.#id) doesExists = getFile(this.#id, "id"); else if (this.#uri) doesExists = getFile(this.#uri, "uri"); if (doesExists) { doesExists.makeActive(); return; } if (filename) this.#name = filename; this.#tab = tile({ text: this.#name, ...(this.#type !== "editor" && { lead: ( ), }), tail: tag("span", { className: "icon cancel", dataset: { action: "close-file", }, }), }); const editable = options?.editable ?? true; this.#SAFMode = options?.SAFMode; this.isUnsaved = options?.isUnsaved ?? false; if (options?.encoding) { this.encoding = options.encoding; } // if options contains text property then there is no need to load // set loaded true if (this.#id !== constants.DEFAULT_FILE_SESSION) { this.loaded = options?.text !== undefined; } // if not loaded then create load options if (!this.loaded) { this.#loadOptions = { cursorPos: options?.cursorPos, scrollLeft: options?.scrollLeft, scrollTop: options?.scrollTop, folds: options?.folds, editable, }; } else { this.editable = editable; } this.#onFilePosChange = () => { const { openFileListPos } = appSettings.value; if ( openFileListPos === appSettings.OPEN_FILE_LIST_POS_HEADER || openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM ) { this.#tab.oncontextmenu = startDrag; } else { this.#tab.oncontextmenu = null; } }; this.#onFilePosChange(); this.#tab.addEventListener("click", tabOnclick.bind(this)); appSettings.on("update:openFileListPos", this.#onFilePosChange); this.pinned = !!options?.pinned; addFile(this); editorManager.emit("new-file", this); if (this.#type === "editor") { this.#rawSession = EditorState.create({ doc: options?.text || "", }); this.setMode(); this.#setupSession(); } if (options?.render ?? true) this.render(); } get type() { return this.#type; } get tabIcon() { return this.#tabIcon; } get content() { return this.#content; } /** * Session with Ace-compatible methods * Returns a Proxy over the raw EditorState. * @returns {Proxy} */ get session() { return createSessionProxy(this.#rawSession, this); } /** * Set the session * @param {EditorState} value */ set session(value) { this.#rawSession = value; } /** * Internal method to update the raw session state. * Used by the Proxy for inactive file updates. * @param {EditorState} state */ _setRawSession(state) { this.#rawSession = state; } /** * File unique id. */ get id() { return this.#id; } /** * File unique id. * @param {string} value */ set id(value) { this.#renameCacheFile(value); this.#id = value; } /** * File name */ get filename() { return this.#name; } /** * File name * @param {string} value */ set filename(value) { if (!value || this.#SAFMode === "single") return; if (this.#name === value) return; const event = createFileEvent(this); this.#emit("rename", event); if (event.defaultPrevented) return; (async () => { if (this.id === constants.DEFAULT_FILE_SESSION) { this.id = helpers.uuid(); } if (editorManager.activeFile.id === this.id) { editorManager.header.text = value; } // const oldExt = helpers.extname(this.#name); const oldExt = Url.extname(this.#name); // const newExt = helpers.extname(value); const newExt = Url.extname(value); this.#tab.text = value; this.#name = value; if (oldExt !== newExt) this.setMode(); editorManager.onupdate("rename-file"); editorManager.emit("rename-file", this); })(); } /** * Location of the file i.e. dirname */ get location() { if (this.#SAFMode === "single") return null; if (this.#uri) { try { return Url.dirname(this.#uri); } catch (error) { return null; } } return null; } /** * Location of the file i.e. dirname * @param {string} value */ set location(value) { if (!value) return; if (this.#SAFMode === "single") return; if (this.location === value) return; const event = createFileEvent(this); this.#emit("rename", event); if (event.defaultPrevented) return; this.uri = Url.join(value, this.filename); this.readOnly = false; } /** * File location on the device */ get uri() { return this.#uri; } /** * File location on the device * @param {string} value */ set uri(value) { if (this.#uri === value) return; if (!value) { this.deletedFile = true; this.isUnsaved = true; this.#uri = null; this.id = helpers.uuid(); } else { this.#uri = value; this.deletedFile = false; this.readOnly = false; this.id = value.hashCode(); } editorManager.onupdate("rename-file"); editorManager.emit("rename-file", this); // if this file is active set sub text of header if (editorManager.activeFile.id === this.id) { editorManager.header.subText = this.#getTitle(); } } /** * End of line */ get eol() { return /\r/.test(this.session.doc.toString()) ? "windows" : "unix"; } /** * End of line * @param {'windows'|'unit'} value */ set eol(value) { if (this.type !== "editor") return; if (this.eol === value) return; let text = this.session.doc.toString(); if (value === "windows") { text = text.replace(/(? { const fs = fsOperation(fileUri); const oldText = await fs.readFile(this.encoding); return await system.compareTexts(oldText, text); }; if (/s?ftp:/.test(protocol)) { // FTP/SFTP files use cached local file const cacheFilename = protocol.slice(0, -1) + this.id; const cacheFileUri = Url.join(CACHE_STORAGE, cacheFilename); try { return await system.compareFileText(cacheFileUri, this.encoding, text); } catch (error) { console.error( "Native compareFileText failed, using JS fallback:", error, ); try { return await jsCompare(cacheFileUri); } catch (fallbackError) { console.error(fallbackError); return false; } } } if (/^(file|content):/.test(protocol)) { // file:// and content:// URIs - try native first, fallback to JS try { return await system.compareFileText(this.uri, this.encoding, text); } catch (error) { console.error( "Native compareFileText failed, using JS fallback:", error, ); try { return await jsCompare(this.uri); } catch (fallbackError) { console.error(fallbackError); return false; } } } // Other protocols - JS reads file, native compares strings try { return await jsCompare(this.uri); } catch (error) { console.error(error); return false; } } async canRun() { if (!this.loaded || this.loading) return false; await this.readCanRun(); return this.#canRun; } async readCanRun() { try { const event = createFileEvent(this); this.#emit("canrun", event); if (event.defaultPrevented) return; const folder = openFolder.find(this.uri); if (folder) { const url = Url.join(folder.url, "index.html"); const fs = fsOperation(url); if (await fs.exists()) { this.#canRun = Promise.resolve(true); return; } } const runnableFile = /\.((html?)|(md)|(js)|(svg))$/; if (runnableFile.test(this.filename)) { this.#canRun = Promise.resolve(true); return; } this.#canRun = Promise.resolve(false); } catch (error) { if (err instanceof Error) throw err; else throw new Error(err); } } /** * Set weather to show run button or not * @param {()=>(boolean|Promise)} cb callback function that return true if file can run */ async writeCanRun(cb) { if (!cb || typeof cb !== "function") return; const res = cb(); if (res instanceof Promise) { this.#canRun = res; return; } this.#canRun = Promise.resolve(res); } /** * Remove and closes the file. * @param {boolean} force if true, will prompt to save the file */ async remove(force = false, options = {}) { const { ignorePinned = false, silentPinned = false } = options || {}; if ( this.id === constants.DEFAULT_FILE_SESSION && !editorManager.files.length ) return false; if (this.pinned && !ignorePinned) { if (!silentPinned) { toast( strings["unpin tab before closing"] || "Unpin the tab before closing it.", ); } return false; } if (!force && this.isUnsaved) { const confirmation = await confirm( strings.warning.toUpperCase(), strings["unsaved file"], ); if (!confirmation) return false; } this.#destroy(); editorManager.files = editorManager.files.filter( (file) => file.id !== this.id, ); const { files, activeFile } = editorManager; if (activeFile.id === this.id) { editorManager.activeFile = null; } if (!files.length) { Sidebar.hide(); editorManager.activeFile = null; new EditorFile(); } else { files[files.length - 1].makeActive(); } editorManager.onupdate("remove-file"); editorManager.emit("remove-file", this); return true; } /** * Saves the file. * @returns {Promise} true if file is saved, false if not. */ save() { if (this.type !== "editor") return Promise.resolve(false); return this.#save(false); } /** * Saves the file to a new location. * @returns {Promise} true if file is saved, false if not. */ saveAs() { if (this.type !== "editor") return Promise.resolve(false); return this.#save(true); } setReadOnly(value) { try { const { editor, readOnlyCompartment } = editorManager; if (!editor) return; if (!readOnlyCompartment) return; editor.dispatch({ effects: readOnlyCompartment.reconfigure( EditorState.readOnly.of(!!value), ), }); } catch (error) { console.warn( `Failed to update read-only state for ${this.filename || this.uri}`, error, ); } // Sync internal flags and header this.readOnly = !!value; this.#editable = !this.readOnly; if (editorManager.activeFile?.id === this.id) { editorManager.header.subText = this.#getTitle(); } } /** * Sets syntax highlighting of the file. * @param {string} [mode] */ setMode(mode) { if (this.type !== "editor") return; const event = createFileEvent(this); this.#emit("changemode", event); if (event.defaultPrevented) return; if (!mode) { const ext = Path.extname(this.filename); const modes = helpers.parseJSON(localStorage.modeassoc); if (modes?.[ext]) { mode = modes[ext]; } } let modeInfo = mode ? getMode(mode) : null; if (!modeInfo) { modeInfo = getModeForPath(this.filename); } mode = modeInfo?.name || String(mode || "text").toLowerCase(); // Store mode info for later use when creating editor view this.currentMode = mode; this.currentLanguageExtension = modeInfo?.getExtension() || null; // sets file icon this.#tab.lead( , ); } /** * Makes this file active */ makeActive() { const { activeFile, editor, switchFile } = editorManager; if (activeFile) { if (activeFile.id === this.id) return; activeFile.focusedBefore = activeFile.focused; activeFile.removeActive(); // Hide previous content if it exists if (activeFile.type !== "editor" && activeFile.content) { activeFile.content.style.display = "none"; } } switchFile(this.id); // Show/hide appropriate content if (this.type === "editor") { editorManager.container.style.display = "block"; if (this.focused) { editor.focus(); } else { editor.contentDOM.blur(); // Ensure any native DOM selection is cleared on blur to avoid sticky selection handles try { document.getSelection()?.removeAllRanges(); } catch (error) { console.warn("Failed to clear native text selection.", error); } } } else { editorManager.container.style.display = "none"; if (this.content) { this.content.style.display = "block"; if (!this.content.parentElement) { editorManager.container.parentElement.appendChild(this.content); } } if (activeFile && activeFile.type === "editor") { clearSelection(editorManager.editor); } } this.#tab.classList.add("active"); this.#tab.scrollIntoView(); if (this.type === "editor" && !this.loaded && !this.loading) { this.#loadText(); } // Handle quicktools visibility based on hideQuickTools property if (this.hideQuickTools) { root.classList.add("hide-floating-button"); actions("set-height", { height: 0, save: false }); } else { root.classList.remove("hide-floating-button"); const quickToolsHeight = appSettings.value.quickTools !== undefined ? appSettings.value.quickTools : 1; actions("set-height", { height: quickToolsHeight, save: false }); } editorManager.header.subText = this.#getTitle(); this.#emit("focus", createFileEvent(this)); } removeActive() { this.#emit("blur", createFileEvent(this)); } openWith() { this.#fileAction("VIEW"); } editWith() { this.#fileAction("EDIT", "text/plain"); } share() { this.#fileAction("SEND"); } runAction() { this.#fileAction("RUN"); } run() { this.#run(false); } runFile() { this.#run(true); } render() { this.makeActive(); if (this.id !== constants.DEFAULT_FILE_SESSION) { const defaultFile = editorManager.getFile( constants.DEFAULT_FILE_SESSION, "id", ); defaultFile?.remove(); } // Show/hide editor based on content type if (this.#type === "editor") { editorManager.container.style.display = "block"; if (this.#content) this.#content.style.display = "none"; } else { editorManager.container.style.display = "none"; if (this.#content) { this.#content.style.display = "block"; editorManager.container.parentElement.appendChild(this.#content); } } } /** * Add event listener * @param {string} event * @param {(this:File, e:Event)=>void} callback */ on(event, callback) { this.#events[event.toLowerCase()]?.push(callback); } /** * Remove event listener * @param {string} event * @param {(this:File, e:Event)=>void} callback */ off(event, callback) { const events = this.#events[event.toLowerCase()]; if (!events) return; const index = events.indexOf(callback); if (index > -1) events.splice(index, 1); } /** * Add custom stylesheets to shadow DOM * @param {string|string[]} styles URLs or CSS strings * @param {ShadowRoot} shadow Shadow DOM root */ #addCustomStyles(styles, shadow) { if (typeof styles === "string") { styles = [styles]; } styles.forEach((style) => { if (style.startsWith("http") || style.startsWith("/")) { // External stylesheet const link = tag("link", { rel: "stylesheet", href: style, }); shadow.appendChild(link); } else { // Inline CSS const styleElement = tag("style", { textContent: style, }); shadow.appendChild(styleElement); } }); } /** * Add stylesheet to tab's shadow DOM * @param {string} style URL or CSS string */ addStyle(style) { if (this.#type === "editor" || !this.#content) return; const shadow = this.#content.shadowRoot; this.#addCustomStyles(style, shadow); } /** * Set custom title function for special tab types * @param {function} titleFn Function that returns the title string */ setCustomTitle(titleFn) { this.#customTitleFn = titleFn; // Update header if this file is currently active if (editorManager.activeFile && editorManager.activeFile.id === this.id) { editorManager.header.subText = this.#getTitle(); } } get headerSubtitle() { return this.#getTitle(); } /** * * @param {FileAction} action */ async #fileAction(action, mimeType) { try { const uri = await this.#getShareableUri(); if (!mimeType) mimeType = mimeTypes.lookup(this.name) || "text/plain"; system.fileAction( uri, this.filename, action, mimeType, this.#showNoAppError, ); } catch (error) { toast(strings.error); } } async #getShareableUri() { if (!this.uri) return null; const fs = fsOperation(this.uri); if (/^s?ftp:/.test(this.uri)) return fs.localName; const { url } = await fs.stat(); return url; } /** * Rename cache file. * @param {String} newId */ async #renameCacheFile(newId) { try { const fs = fsOperation(this.cacheFile); if (!(await fs.exists())) return; fs.renameTo(newId); } catch (error) { window.log("error", "renameCacheFile"); window.log("error", error); } } /** * Removes cache file */ async #removeCache() { try { const fs = fsOperation(this.cacheFile); if (!(await fs.exists())) return; await fs.delete(); } catch (error) { window.log("error", error); } } async #loadText() { if (this.#type !== "editor") return; let value = ""; const { cursorPos, scrollLeft, scrollTop, folds, editable } = this.#loadOptions; const { editor } = editorManager; this.#loadOptions = null; this.setReadOnly(true); this.loading = true; this.markChanged = false; this.#emit("loadstart", createFileEvent(this)); this.session.setValue(strings["loading..."]); // Immediately reflect "loading..." in the visible editor if this tab is active try { const { activeFile, emit } = editorManager; if (activeFile?.id === this.id) { emit("file-loaded", this); } } catch (error) { console.warn("Failed to emit interim file-loaded event.", error); } try { const cacheFs = fsOperation(this.cacheFile); const cacheExists = await cacheFs.exists(); if (cacheExists) { value = await cacheFs.readFile(this.encoding); } if (this.uri) { const file = fsOperation(this.uri); const fileExists = await file.exists(); if (!fileExists && cacheExists) { this.deletedFile = true; this.isUnsaved = true; } else if (!cacheExists && fileExists) { value = await file.readFile(this.encoding); } else if (!cacheExists && !fileExists) { window.log("error", "unable to load file"); throw new Error("Unable to load file"); } } this.markChanged = false; this.session.setValue(value); this.loaded = true; this.loading = false; const { activeFile, emit } = editorManager; if (activeFile.id === this.id) { this.setReadOnly(false); } setTimeout(() => { this.#emit("load", createFileEvent(this)); emit("file-loaded", this); if (cursorPos) { restoreSelection(editor, cursorPos); } if (scrollTop || scrollLeft) { setScrollPosition(editor, scrollTop, scrollLeft); } if (editable !== undefined) this.editable = editable; restoreFolds(editor, folds); }, 0); } catch (error) { this.#emit("loaderror", createFileEvent(this)); this.remove(false, { ignorePinned: true }); toast(`Unable to load: ${this.filename}`); window.log("error", "Unable to load: " + this.filename); window.log("error", error); } finally { this.#emit("loadend", createFileEvent(this)); } } // TODO: Implement CodeMirror equivalents for folding and scroll events // static #onfold(e) { // editorManager.editor._emit("fold", e); // } // static #onscrolltop(e) { // editorManager.editor._emit("scrolltop", e); // } // static #onscrollleft(e) { // editorManager.editor._emit("scrollleft", e); // } #save(as) { const event = createFileEvent(this); this.#emit("save", event); if (event.defaultPrevented) return Promise.resolve(false); return Promise.all([this.writeToCache(), saveFile(this, as)]); } #run(file) { const event = createFileEvent(this); this.#emit("run", event); if (event.defaultPrevented) return; run(false, appSettings.value.previewMode, file); } #updateTab() { if (!this.#tab) return; if (this.#isUnsaved) { this.tab.classList.add("notice"); } else { this.tab.classList.remove("notice"); } this.tab.classList.toggle("pinned", this.#pinned); this.#tab.tail(this.#createTabTail()); } /** * Setup CodeMirror EditorState for the file */ #setupSession() { if (this.type !== "editor") return; // CodeMirror configuration will be handled in the EditorView // Store settings for when the editor view is created this.editorSettings = { tabSize: appSettings.value.tabSize, softTab: appSettings.value.softTab, textWrap: appSettings.value.textWrap, }; } #destroy() { this.#emit("close", createFileEvent(this)); appSettings.off("update:openFileListPos", this.#onFilePosChange); if (this.type === "editor") { this.#removeCache(); // CodeMirror EditorState doesn't need explicit cleanup this.session = null; } else if (this.content) { this.content.remove(); } this.#tab.remove(); this.#tab = null; } #showNoAppError() { toast(strings["no app found to handle this file"]); } #createTabTail() { if (!this.#pinned) { return tag("span", { className: "icon cancel", dataset: { action: "close-file", }, }); } return tag("span", { className: "icon pin", title: strings["unpin tab"] || "Unpin tab", dataset: { action: "toggle-pin", }, }); } #getTitle() { // Use custom title function if provided if (this.#customTitleFn) { return this.#customTitleFn(); } let text = this.location || this.uri; if (text && !this.readOnly) { text = helpers.getVirtualPath(text); if (text.length > 30) text = "..." + text.slice(text.length - 27); } else if (this.readOnly) { text = strings["read only"]; } else if (this.deletedFile) { text = strings["deleted file"]; } else { text = strings["new file"]; } return text; } /** * Emits an event * @param {FileEvents} eventName * @param {FileEvent} event */ #emit(eventName, event) { this[`on${eventName}`]?.(event); if (!event.BUBBLING_PHASE) return; this.#events[eventName]?.some((fn) => { fn(event); return !event.BUBBLING_PHASE; }); } } /** * * @param {MouseEvent} e * @returns */ function tabOnclick(e) { e.preventDefault(); const { action } = e.target.dataset; if (action === "close-file") { this.remove(); return; } if (action === "toggle-pin") { this.togglePinned(); return; } this.makeActive(); } function createFileEvent(file) { return new FileEvent(file); } class FileEvent { #bubblingPhase = true; #defaultPrevented = false; target; constructor(file) { this.target = file; } stopPropagation() { this.#bubblingPhase = false; } preventDefault() { this.#defaultPrevented = true; } get BUBBLING_PHASE() { return this.#bubblingPhase; } get defaultPrevented() { return this.#defaultPrevented; } } ================================================ FILE: src/lib/editorManager.js ================================================ import sidebarApps from "sidebarApps"; import { indentUnit } from "@codemirror/language"; import { search } from "@codemirror/search"; import { Compartment, EditorState, Prec, StateEffect } from "@codemirror/state"; import { oneDark } from "@codemirror/theme-one-dark"; import { closeHoverTooltips, EditorView, hasHoverTooltips, highlightActiveLineGutter, highlightTrailingWhitespace, highlightWhitespace, keymap, lineNumbers, } from "@codemirror/view"; import { abbreviationTracker, EmmetKnownSyntax, emmetCompletionSource, emmetConfig, expandAbbreviation, wrapWithAbbreviation, } from "@emmetio/codemirror6-plugin"; import createBaseExtensions from "cm/baseExtensions"; import { setKeyBindings as applyKeyBindings, executeCommand, getCommandKeymapExtension, getRegisteredCommands, refreshCommandKeymap, registerExternalCommand, removeExternalCommand, } from "cm/commandRegistry"; import lspApi from "cm/lsp/api"; import lspClientManager from "cm/lsp/clientManager"; import { getLspDiagnostics, LSP_DIAGNOSTICS_EVENT, lspDiagnosticsClientExtension, lspDiagnosticsUiExtension, } from "cm/lsp/diagnostics"; import { stopManagedServer } from "cm/lsp/serverLauncher"; import createMainEditorExtensions from "cm/mainEditorExtensions"; // CodeMirror mode management import { getMode, getModeForPath, getModes, getModesByName, initModes, } from "cm/modelist"; import createTouchSelectionMenu from "cm/touchSelectionMenu"; import "cm/supportedModes"; import { autocompletion } from "@codemirror/autocomplete"; import colorView from "cm/colorView"; import { getAllFolds, restoreFolds, restoreSelection, setScrollPosition, } from "cm/editorUtils"; import indentGuides from "cm/indentGuides"; import rainbowBrackets, { getRainbowBracketColors } from "cm/rainbowBrackets"; import { getThemeConfig, getThemeExtensions } from "cm/themes"; import list from "components/collapsableList"; import quickTools from "components/quickTools"; import ScrollBar from "components/scrollbar"; import SideButton, { sideButtonContainer } from "components/sideButton"; import keyboardHandler, { keydownState } from "handlers/keyboard"; import EditorFile from "./editorFile"; import openFile from "./openFile"; import { addedFolder } from "./openFolder"; import appSettings from "./settings"; import { getSystemConfiguration, HARDKEYBOARDHIDDEN_NO, } from "./systemConfiguration"; /** * Represents an editor manager that handles multiple files and provides various editor configurations and event listeners. * @param {HTMLElement} $header - The header element. * @param {HTMLElement} $body - The body element. * @returns {Promise} A promise that resolves to the editor manager object. */ async function EditorManager($header, $body) { /** * @type {Collapsible & HTMLElement} */ let $openFileList; let TIMEOUT_VALUE = 500; let preventScrollbarV = false; let preventScrollbarH = false; let scrollBarVisibilityCount = 0; let timeoutQuicktoolsToggler; let timeoutHeaderToggler; let isScrolling = false; let lastScrollTop = 0; let lastScrollLeft = 0; // Debounce timers for CodeMirror change handling let checkTimeout = null; let autosaveTimeout = null; let touchSelectionController = null; let touchSelectionSyncRaf = 0; let nativeContextMenuDisabled = null; const recoverableWarningKeys = new Set(); function warnRecoverable(message, error, key) { if (key) { if (recoverableWarningKeys.has(key)) return; recoverableWarningKeys.add(key); } console.warn(message, error); } function isCoarsePointerDevice() { if (typeof window !== "undefined") { try { if (window.matchMedia?.("(pointer: coarse)").matches) { return true; } } catch (_) { // Ignore matchMedia capability errors and fall through. } } return ( typeof navigator !== "undefined" && Number(navigator.maxTouchPoints || 0) > 0 ); } const setNativeContextMenuDisabled = (disabled) => { const value = !!disabled; if (nativeContextMenuDisabled === value) return; nativeContextMenuDisabled = value; const api = globalThis.system?.setNativeContextMenuDisabled; if (typeof api !== "function") return; try { api.call(globalThis.system, value); } catch (error) { console.warn("Failed to update native context menu state", error); } }; const { scrollbarSize } = appSettings.value; const events = { "switch-file": [], "rename-file": [], "save-file": [], "file-loaded": [], "file-content-changed": [], "add-folder": [], "remove-folder": [], update: [], "new-file": [], "remove-file": [], "int-open-file-list": [], emit(event, ...args) { if (!events[event]) return; events[event].forEach((fn) => fn(...args)); }, }; const $container =
        ; // Ensure the container participates well in flex layouts and can constrain the editor $container.style.flex = "1 1 auto"; $container.style.minHeight = "0"; // allow child scroller to size correctly $container.style.height = "100%"; $container.style.width = "100%"; const problemButton = SideButton({ text: strings.problems, icon: "warningreport_problem", backgroundColor: "var(--danger-color)", textColor: "var(--danger-text-color)", onclick() { acode.exec("open", "problems"); }, }); const pointerCursorVisibilityExtension = EditorView.updateListener.of( (update) => { if (!update.selectionSet) return; const pointerTriggered = update.transactions.some( (tr) => tr.isUserEvent("pointer") || tr.isUserEvent("select") || tr.isUserEvent("select.pointer") || tr.isUserEvent("touch") || tr.isUserEvent("select.touch"), ); if (!pointerTriggered) return; requestAnimationFrame(() => { if (!isCursorVisible()) scrollCursorIntoView({ behavior: "instant" }); }); }, ); const isShiftSelectionActive = (event) => { if (!appSettings.value.shiftClickSelection) return false; return !!event?.shiftKey || quickTools?.$footer?.dataset?.shift != null; }; const shiftClickSelectionExtension = EditorView.domEventHandlers({ click(event) { if (!touchSelectionController?.consumePendingShiftSelectionClick(event)) { return false; } event.preventDefault(); return true; }, }); const touchSelectionUpdateExtension = EditorView.updateListener.of( (update) => { if (!touchSelectionController) return; const pointerTriggered = update.transactions.some( (tr) => tr.isUserEvent("pointer") || tr.isUserEvent("select") || tr.isUserEvent("select.pointer") || tr.isUserEvent("touch") || tr.isUserEvent("select.touch"), ); if (update.selectionSet || pointerTriggered) { cancelAnimationFrame(touchSelectionSyncRaf); touchSelectionSyncRaf = requestAnimationFrame(() => { touchSelectionController?.onStateChanged({ pointerTriggered, selectionChanged: update.selectionSet, }); }); } }, ); // Compartment to swap editor theme dynamically const themeCompartment = new Compartment(); // Compartments to control indentation, tab width, and font styling dynamically const indentUnitCompartment = new Compartment(); const tabSizeCompartment = new Compartment(); const fontStyleCompartment = new Compartment(); // Compartment for line wrapping const wrapCompartment = new Compartment(); // Compartment for line numbers const lineNumberCompartment = new Compartment(); // Compartment for text direction (RTL/LTR) const rtlCompartment = new Compartment(); // Compartment for whitespace visualization const whitespaceCompartment = new Compartment(); // Compartment for fold gutter theme (fade) const foldThemeCompartment = new Compartment(); // Compartment for autocompletion behavior const completionCompartment = new Compartment(); // Compartment for rainbow bracket colorizer const rainbowCompartment = new Compartment(); // Compartment for indent guides const indentGuidesCompartment = new Compartment(); // Compartment for read-only toggling const readOnlyCompartment = new Compartment(); // Compartment for language mode (allows async loading/reconfigure) const languageCompartment = new Compartment(); // Compartment for LSP extensions so we can swap per file const lspCompartment = new Compartment(); const diagnosticsClientExt = lspDiagnosticsClientExtension(); const buildDiagnosticsUiExt = () => lspDiagnosticsUiExtension(appSettings?.value?.lintGutter !== false); let lspRequestToken = 0; let lastLspUri = null; const UNTITLED_URI_PREFIX = "untitled://acode/"; function getEditorFontFamily() { const font = appSettings?.value?.editorFont || "Roboto Mono"; return `${font}, Noto Mono, Monaco, monospace`; } function makeFontTheme() { const fontSize = appSettings?.value?.fontSize || "12px"; const lineHeight = appSettings?.value?.lineHeight || 1.6; const fontFamily = getEditorFontFamily(); return EditorView.theme({ "&": { fontSize, lineHeight: String(lineHeight) }, ".cm-content": { fontFamily }, ".cm-gutter": { fontFamily }, ".cm-tooltip, .cm-tooltip *": { fontFamily }, }); } function makeWrapExtension() { return appSettings?.value?.textWrap ? EditorView.lineWrapping : []; } function makeLineNumberExtension() { const { linenumbers = true, relativeLineNumbers = false } = appSettings?.value || {}; if (!linenumbers) return EditorView.theme({ ".cm-gutter": { display: "none !important", width: "0px !important", minWidth: "0px !important", border: "none !important", }, }); if (!relativeLineNumbers) return Prec.highest([lineNumbers(), highlightActiveLineGutter()]); return Prec.highest([ lineNumbers({ formatNumber: (lineNo, state) => { try { const cur = state.doc.lineAt(state.selection.main.head).number; const diff = Math.abs(lineNo - cur); return diff === 0 ? String(lineNo) : String(diff); } catch (_) { return String(lineNo); } }, }), highlightActiveLineGutter(), ]); } function makeIndentExtensions() { const { softTab = true, tabSize = 2 } = appSettings?.value || {}; const unit = softTab ? " ".repeat(Math.max(1, Number(tabSize) || 2)) : "\t"; return { indentExt: indentUnit.of(unit), tabSizeExt: EditorState.tabSize.of(Math.max(1, Number(tabSize) || 2)), }; } function makeRainbowBracketExtension() { const enabled = appSettings?.value?.rainbowBrackets ?? true; if (!enabled) return []; const themeId = appSettings?.value?.editorTheme || "one_dark"; return rainbowBrackets({ colors: getRainbowBracketColors(getThemeConfig(themeId)), }); } function makeWhitespaceTheme() { return EditorView.theme({ ".cm-highlightSpace": { backgroundImage: "radial-gradient(circle at 50% 54%, var(--cm-space-marker-color) 0.08em, transparent 0.1em)", backgroundPosition: "center", backgroundRepeat: "no-repeat", opacity: "0.5", }, ".cm-highlightTab": { backgroundSize: "auto 70%", backgroundPosition: "right 60%", opacity: "0.65", }, ".cm-trailingSpace": { backgroundColor: "var(--cm-trailing-space-color)", borderRadius: "2px", }, "&": { "--cm-space-marker-color": "rgba(127, 127, 127, 0.6)", "--cm-trailing-space-color": "rgba(255, 77, 77, 0.2)", }, }); } // Centralised CodeMirror options registry for organized configuration // Each spec declares related settings keys, its compartment(s), and a builder returning extension(s) const cmOptionSpecs = [ { keys: ["linenumbers", "relativeLineNumbers"], compartments: [lineNumberCompartment], build() { return makeLineNumberExtension(); }, }, { keys: ["rainbowBrackets"], compartments: [rainbowCompartment], build() { return makeRainbowBracketExtension(); }, }, { keys: ["indentGuides"], compartments: [indentGuidesCompartment], build() { const enabled = appSettings?.value?.indentGuides ?? true; if (!enabled) return []; return indentGuides({ highlightActiveGuide: true, hideOnBlankLines: false, }); }, }, { keys: ["fontSize", "editorFont", "lineHeight"], compartments: [fontStyleCompartment], build() { return makeFontTheme(); }, }, { keys: ["textWrap"], compartments: [wrapCompartment], build() { return makeWrapExtension(); }, }, { keys: ["softTab", "tabSize"], compartments: [indentUnitCompartment, tabSizeCompartment], build() { const { indentExt, tabSizeExt } = makeIndentExtensions(); return [indentExt, tabSizeExt]; }, }, { keys: ["rtlText"], compartments: [rtlCompartment], build() { const rtl = !!appSettings?.value?.rtlText; return EditorView.theme({ "&": { direction: rtl ? "rtl" : "ltr" }, }); }, }, { keys: ["showSpaces"], compartments: [whitespaceCompartment], build() { const show = !!appSettings?.value?.showSpaces; return show ? [ highlightWhitespace(), highlightTrailingWhitespace(), makeWhitespaceTheme(), ] : []; }, }, { keys: ["fadeFoldWidgets"], compartments: [foldThemeCompartment], build() { const fade = !!appSettings?.value?.fadeFoldWidgets; if (!fade) return []; return EditorView.theme({ ".cm-gutter.cm-foldGutter .cm-gutterElement": { opacity: 0, pointerEvents: "none", transition: "opacity .12s ease", }, ".cm-gutter.cm-foldGutter:hover .cm-gutterElement, .cm-gutter.cm-foldGutter .cm-gutterElement:hover": { opacity: 1, pointerEvents: "auto", }, }); }, }, { keys: ["liveAutoCompletion"], compartments: [completionCompartment], build() { const live = !!appSettings?.value?.liveAutoCompletion; return autocompletion({ activateOnTyping: live, activateOnTypingDelay: isCoarsePointerDevice() ? 220 : 100, }); }, }, ]; function getBaseExtensionsFromOptions() { /** @type {import("@codemirror/state").Extension[]} */ const exts = []; for (const spec of cmOptionSpecs) { const built = spec.build(); if (spec.compartments.length === 1) { exts.push(spec.compartments[0].of(built)); } else { const arr = Array.isArray(built) ? built : [built]; for (let i = 0; i < spec.compartments.length; i++) { const comp = spec.compartments[i]; const ext = arr[i]; if (ext !== undefined) exts.push(comp.of(ext)); } } } return exts; } function createEmmetExtensionSet({ syntax, tracker = {}, config: emmetOverrides = {}, } = {}) { const resolvedSyntax = syntax === undefined ? EmmetKnownSyntax.html : syntax; if (!resolvedSyntax) return []; const trackerExtension = abbreviationTracker({ syntax: resolvedSyntax, ...tracker, }); const { autocompleteTab = ["markup", "stylesheet"], ...restOverrides } = emmetOverrides || {}; const emmetConfigExtension = emmetConfig.of({ syntax: resolvedSyntax, autocompleteTab, ...restOverrides, }); return [ Prec.high(trackerExtension), wrapWithAbbreviation(), keymap.of([{ key: "Mod-e", run: expandAbbreviation }]), emmetConfigExtension, ]; } function applyOptions(keys) { const filter = keys ? new Set(keys) : null; for (const spec of cmOptionSpecs) { if (filter && !spec.keys.some((k) => filter.has(k))) continue; const built = spec.build(); const effects = []; if (spec.compartments.length === 1) { effects.push(spec.compartments[0].reconfigure(built)); } else { const arr = Array.isArray(built) ? built : [built]; for (let i = 0; i < spec.compartments.length; i++) { const comp = spec.compartments[i]; const ext = arr[i] ?? []; effects.push(comp.reconfigure(ext)); } } editor.dispatch({ effects }); } } function buildLspMetadata(file) { if (!file || file.type !== "editor") return null; const uri = getFileLspUri(file); if (!uri) return null; const languageId = getFileLanguageId(file); return { uri, languageId, languageName: file.currentMode || file.mode || languageId, view: editor, file, rootUri: resolveRootUriForContext({ uri, file }), }; } async function configureLspForFile(file) { const metadata = buildLspMetadata(file); const token = ++lspRequestToken; if (!metadata) { detachActiveLsp(); editor.dispatch({ effects: lspCompartment.reconfigure([]) }); return; } if (metadata.uri !== lastLspUri) { detachActiveLsp(); } try { const extensions = (await lspClientManager.getExtensionsForFile(metadata)) || []; if (token !== lspRequestToken) return; if (!extensions.length) { lastLspUri = null; editor.dispatch({ effects: lspCompartment.reconfigure([]) }); return; } lastLspUri = metadata.uri; editor.dispatch({ effects: lspCompartment.reconfigure(extensions), }); } catch (error) { if (token !== lspRequestToken) return; console.error("Failed to configure LSP", error); lastLspUri = null; editor.dispatch({ effects: lspCompartment.reconfigure([]) }); } } function detachLspForFile(file) { if (!file || file.type !== "editor") return; const uri = getFileLspUri(file); if (!uri) return; try { lspClientManager.detach(uri); } catch (error) { console.warn(`Failed to detach LSP client for ${uri}`, error); } if (uri === lastLspUri && manager.activeFile?.id === file.id) { lastLspUri = null; editor.dispatch({ effects: lspCompartment.reconfigure([]) }); } } // Plugin already wires CSS completions; attach extras for related syntaxes. const emmetCompletionSyntaxes = new Set([ EmmetKnownSyntax.scss, EmmetKnownSyntax.less, EmmetKnownSyntax.sass, EmmetKnownSyntax.sss, EmmetKnownSyntax.stylus, EmmetKnownSyntax.postcss, ]); function maybeAttachEmmetCompletions(targetExtensions, syntax) { if (emmetCompletionSyntaxes.has(syntax)) { targetExtensions.push( EditorState.languageData.of(() => [ { autocomplete: emmetCompletionSource }, ]), ); } } function getFileLspUri(file) { if (!file) return null; if (file.uri) return file.uri; return `${UNTITLED_URI_PREFIX}${file.id}`; } function getFileLanguageId(file) { if (!file) return "plaintext"; const mode = file.currentMode || file.mode; if (mode) { const modeInfo = getMode(String(mode)); if (modeInfo?.name) return String(modeInfo.name).toLowerCase(); return String(mode).toLowerCase(); } try { const guess = getModeForPath(file.filename || file.name || ""); if (guess?.name) return String(guess.name).toLowerCase(); } catch (error) { warnRecoverable( `Failed to resolve language id for ${file.filename || file.name || "untitled file"}`, error, "language-id-resolution", ); } return "plaintext"; } function resolveRootUriForContext(context = {}) { const uri = context.uri || context.file?.uri; if (!uri) return null; for (const folder of addedFolder) { const base = typeof folder?.url === "string" ? folder.url : ""; if (!base) continue; if (uri.startsWith(base)) return base; } return uri; } function detachActiveLsp() { if (!lastLspUri) return; try { lspClientManager.detach(lastLspUri, editor); } catch (error) { console.warn(`Failed to detach LSP session for ${lastLspUri}`, error); } lastLspUri = null; } function applyLspSettings() { const { lsp } = appSettings.value || {}; if (!lsp) return; const overrides = lsp.servers || {}; for (const [id, config] of Object.entries(overrides)) { if (!config || typeof config !== "object") continue; const key = String(id || "") .trim() .toLowerCase(); if (!key) continue; const existing = lspApi.servers.get(key); if (existing) { lspApi.servers.update(key, (current) => { const next = { ...current }; if (Array.isArray(config.languages) && config.languages.length) { next.languages = config.languages.map((lang) => String(lang).toLowerCase(), ); } if (config.transport && typeof config.transport === "object") { next.transport = { ...current.transport, ...config.transport }; delete next.transport.protocols; } if (config.clientConfig && typeof config.clientConfig === "object") { next.clientConfig = { ...current.clientConfig, ...config.clientConfig, }; } if ( config.initializationOptions && typeof config.initializationOptions === "object" ) { next.initializationOptions = { ...current.initializationOptions, ...config.initializationOptions, }; } if ( typeof config.startupTimeout === "number" && Number.isFinite(config.startupTimeout) && config.startupTimeout > 0 ) { next.startupTimeout = Math.floor(config.startupTimeout); } if (config.launcher && typeof config.launcher === "object") { next.launcher = { ...current.launcher, ...config.launcher }; } if (Object.prototype.hasOwnProperty.call(config, "enabled")) { next.enabled = !!config.enabled; } return next; }); if (config.enabled === false) { stopManagedServer(key); } } else if ( Array.isArray(config.languages) && config.languages.length && config.transport && typeof config.transport === "object" ) { try { lspApi.upsert({ id: key, label: config.label || key, languages: config.languages, transport: config.transport, clientConfig: config.clientConfig, initializationOptions: config.initializationOptions, startupTimeout: config.startupTimeout, launcher: config.launcher, enabled: config.enabled !== false, }); lspApi.servers.update(key, (current) => { if (current.transport?.protocols) { const updated = { ...current }; updated.transport = { ...current.transport }; delete updated.transport.protocols; return updated; } return current; }); if (config.enabled === false) { stopManagedServer(key); } } catch (error) { console.warn( `Failed to register LSP server override for ${key}`, error, ); } } } } // Create minimal CodeMirror editor const editorState = EditorState.create({ doc: "", extensions: createMainEditorExtensions({ // Emmet needs highest precedence so place before default keymaps emmetExtensions: createEmmetExtensionSet({ syntax: EmmetKnownSyntax.html, }), baseExtensions: createBaseExtensions(), commandKeymapExtension: getCommandKeymapExtension(), themeExtension: themeCompartment.of(oneDark), pointerCursorVisibilityExtension, shiftClickSelectionExtension, touchSelectionUpdateExtension, searchExtension: search(), // Ensure read-only can be toggled later via compartment readOnlyExtension: readOnlyCompartment.of(EditorState.readOnly.of(false)), // Editor options driven by settings via compartments optionExtensions: getBaseExtensionsFromOptions(), }), }); const editor = new EditorView({ state: editorState, parent: $container, }); await applyKeyBindings(editor); editor.execCommand = function (commandName, args) { if (!commandName) return false; return executeCommand(String(commandName), editor, args); }; editor.commands = { addCommand(descriptor) { const command = registerExternalCommand(descriptor); refreshCommandKeymap(editor); return command; }, removeCommand(name) { if (!name) return; removeExternalCommand(name); refreshCommandKeymap(editor); }, }; Object.defineProperty(editor.commands, "commands", { get() { const map = {}; getRegisteredCommands().forEach((cmd) => { map[cmd.name] = cmd; }); return map; }, }); // Provide editor.session for Ace API compatibility // Returns the active file's session (Proxy with Ace-like methods) Object.defineProperty(editor, "session", { get() { return manager.activeFile?.session ?? null; }, }); touchSelectionController = createTouchSelectionMenu(editor, { container: $container, getActiveFile: () => manager?.activeFile || null, isShiftSelectionActive, }); // Provide minimal Ace-like API compatibility used by plugins /** * Insert text at the current selection/cursor in the editor * @param {string} text * @returns {boolean} success */ editor.insert = function (text) { try { const { from, to } = editor.state.selection.main; const insertText = String(text ?? ""); // Replace current selection and move cursor to end of inserted text editor.dispatch({ changes: { from, to, insert: insertText }, selection: { anchor: from + insertText.length, head: from + insertText.length, }, }); return true; } catch (_) { return false; } }; // Set CodeMirror theme by id registered in our registry editor.setTheme = function (themeId) { try { const id = String(themeId || ""); const ext = getThemeExtensions(id, [oneDark]); editor.dispatch({ effects: themeCompartment.reconfigure(ext) }); return true; } catch (_) { return false; } }; /** * Go to a specific line and column in the editor (CodeMirror implementation) * Supports multiple input formats: * - Simple line number: gotoLine(16) or gotoLine(16, 5) * - Relative offsets: gotoLine("+5") or gotoLine("-3") * - Percentages: gotoLine("50%") or gotoLine("25%") * - Line:column format: gotoLine("16:5") * - Mixed formats: gotoLine("+5:10") or gotoLine("50%:5") * * @param {number|string} line - Line number (1-based), or string with special formats * @param {number} column - Column number (0-based) - only used with numeric line parameter * @param {boolean} animate - Whether to animate (not used in CodeMirror, for compatibility) * @returns {boolean} success */ editor.gotoLine = function (line, column = 0, animate = false) { try { const { state } = editor; const { doc } = state; let targetLine, targetColumn = column; // If line is a string, parse it for special formats if (typeof line === "string") { const match = /^([+-])?(\d+)?(:\d+)?(%)?$/.exec(line.trim()); if (!match) { console.warn("Invalid gotoLine format:", line); return false; } const currentLine = doc.lineAt(state.selection.main.head); const [, sign, lineNum, colonColumn, percent] = match; // Parse column if specified in line:column format if (colonColumn) { targetColumn = Math.max(0, +colonColumn.slice(1) - 1); // Convert to 0-based } // Parse line number let parsedLine = lineNum ? +lineNum : currentLine.number; if (lineNum && percent) { // Percentage format: "50%" or "+10%" let percentage = parsedLine / 100; if (sign) { percentage = percentage * (sign === "-" ? -1 : 1) + currentLine.number / doc.lines; } targetLine = Math.round(doc.lines * percentage); } else if (lineNum && sign) { // Relative format: "+5" or "-3" targetLine = parsedLine * (sign === "-" ? -1 : 1) + currentLine.number; } else if (lineNum) { // Absolute line number targetLine = parsedLine; } else { // No line number specified, stay on current line targetLine = currentLine.number; } } else { // Simple numeric line parameter targetLine = line; } // Clamp line number to valid range const lineNum = Math.max(1, Math.min(targetLine, doc.lines)); const docLine = doc.line(lineNum); // Clamp column to line length const col = Math.max(0, Math.min(targetColumn, docLine.length)); const pos = docLine.from + col; // Move cursor and scroll into view editor.dispatch({ selection: { anchor: pos, head: pos }, effects: EditorView.scrollIntoView(pos, { y: "center" }), }); editor.focus(); return true; } catch (error) { console.error("Error in gotoLine:", error); return false; } }; /** * Get current cursor position) * @returns {{row: number, column: number}} Cursor position */ editor.getCursorPosition = function () { try { const head = editor.state.selection.main.head; const cursor = editor.state.doc.lineAt(head); const line = cursor.number; const col = head - cursor.from; return { row: line, column: col }; } catch (_) { return { row: 1, column: 0 }; } }; /** * Ace-compatible selection range getter with 0-based rows. * @returns {{start: {row: number, column: number}, end: {row: number, column: number}}} */ editor.getSelectionRange = function () { try { const { from, to } = editor.state.selection.main; const fromLine = editor.state.doc.lineAt(from); const toLine = editor.state.doc.lineAt(to); return { start: { row: Math.max(0, fromLine.number - 1), column: from - fromLine.from, }, end: { row: Math.max(0, toLine.number - 1), column: to - toLine.from, }, }; } catch (_) { return { start: { row: 0, column: 0 }, end: { row: 0, column: 0 } }; } }; /** * Ace-compatible row scrolling helper. * @param {number} row - 0-based row index, supports Infinity to jump to end. * @returns {boolean} */ editor.scrollToRow = function (row) { try { const scroller = editor.scrollDOM; if (!scroller) return false; if (row === Number.POSITIVE_INFINITY) { scroller.scrollTop = Math.max( scroller.scrollHeight - scroller.clientHeight, 0, ); return true; } const parsedRow = Number(row); if (!Number.isFinite(parsedRow)) return false; const aceRow = Math.max(0, Math.floor(parsedRow)); const lineNum = Math.min(editor.state.doc.lines, aceRow + 1); const line = editor.state.doc.line(lineNum); editor.dispatch({ effects: EditorView.scrollIntoView(line.from, { y: "start" }), }); return true; } catch (_) { return false; } }; /** * Move cursor to specific position * @param {{row: number, column: number}} pos - Position to move to */ editor.moveCursorToPosition = function (pos) { try { const lineNum = Math.max(1, pos.row || 1); const col = Math.max(0, pos.column || 0); editor.gotoLine(lineNum, col); } catch (_) { // ignore } }; /** * Get the entire document value * @returns {string} Document content */ editor.getValue = function () { try { return editor.state.doc.toString(); } catch (_) { return ""; } }; /** * Compatibility object for selection-related methods */ editor.selection = { /** * Get current selection anchor * @returns {number} Anchor position */ get anchor() { try { return editor.state.selection.main.anchor; } catch (_) { return 0; } }, /** * Get current selection range * @returns {{start: {row: number, column: number}, end: {row: number, column: number}}} Selection range */ getRange: function () { try { const { from, to } = editor.state.selection.main; const fromLine = editor.state.doc.lineAt(from); const toLine = editor.state.doc.lineAt(to); return { start: { row: fromLine.number, column: from - fromLine.from, }, end: { row: toLine.number, column: to - toLine.from, }, }; } catch (_) { return { start: { row: 1, column: 0 }, end: { row: 1, column: 0 } }; // Default to line 1 } }, /** * Get cursor position * @returns {{row: number, column: number}} Cursor position */ getCursor: function () { return editor.getCursorPosition(); }, }; /** * Get selected text or text under cursor (CodeMirror implementation) * @returns {string} Selected text */ editor.getCopyText = function () { try { const { from, to } = editor.state.selection.main; if (from === to) return ""; // No selection return editor.state.doc.sliceString(from, to); } catch (_) { return ""; } }; editor.setSelection = function (value) { touchSelectionController?.setSelection(!!value); }; editor.setMenu = function (value) { touchSelectionController?.setMenu(!!value); }; // Helper: apply a file's content and language to the editor view function applyFileToEditor(file) { if (!file || file.type !== "editor") return; const syntax = getEmmetSyntaxForFile(file); const baseExtensions = createMainEditorExtensions({ // Emmet needs to precede default keymaps so tracker Tab wins over indent emmetExtensions: createEmmetExtensionSet({ syntax }), baseExtensions: createBaseExtensions(), commandKeymapExtension: getCommandKeymapExtension(), // keep compartment in the state to allow dynamic theme changes later themeExtension: themeCompartment.of(oneDark), pointerCursorVisibilityExtension, shiftClickSelectionExtension, touchSelectionUpdateExtension, searchExtension: search(), // Keep dynamic compartments across state swaps optionExtensions: getBaseExtensionsFromOptions(), }); const exts = [...baseExtensions]; maybeAttachEmmetCompletions(exts, syntax); try { const langExtFn = file.currentLanguageExtension; let initialLang = []; if (typeof langExtFn === "function") { let result; try { result = langExtFn(); } catch (_) { result = []; } // If the loader returns a Promise, reconfigure when it resolves if (result && typeof result.then === "function") { initialLang = []; result .then((ext) => { try { editor.dispatch({ effects: languageCompartment.reconfigure(ext || []), }); } catch (error) { warnRecoverable( "Failed to apply async language extensions.", error, "async-language-reconfigure", ); } }) .catch(() => { // ignore load errors; remain in plain text }); } else { initialLang = result || []; } } // Ensure language compartment is present (empty -> plain text) exts.push(languageCompartment.of(initialLang)); } catch (e) { // ignore language extension errors; fallback to plain text } // Color preview plugin when enabled if (appSettings.value.colorPreview) { exts.push(colorView(true)); } // Apply read-only state based on file.editable/loading using Compartment try { const ro = !file.editable || !!file.loading; exts.push(readOnlyCompartment.of(EditorState.readOnly.of(ro))); } catch (e) { // safe to ignore; editor will remain editable by default } // Keep file.session in sync and handle caching/autosave exts.push(getDocSyncListener()); exts.push(lspCompartment.of([])); // Preserve previous state for restoring selection/folds after swap const prevState = file.session || null; const doc = prevState ? prevState.doc.toString() : ""; const state = EditorState.create({ doc, extensions: exts }); file.session = state; editor.setState(state); touchSelectionController?.onSessionChanged(); // Re-apply selected theme after state replacement const desiredTheme = appSettings?.value?.editorTheme; if (desiredTheme) editor.setTheme(desiredTheme); // Ensure dynamic compartments reflect current settings applyOptions(); // Restore selection from previous state if available try { const sel = prevState?.selection; if (sel && Array.isArray(sel.ranges)) { const ranges = sel.ranges.map((r) => ({ from: r.from, to: r.to })); const mainIndex = sel.mainIndex ?? 0; restoreSelection(editor, { ranges, mainIndex }); } } catch (error) { warnRecoverable( "Failed to restore selection from previous session state.", error, "restore-selection", ); } // Restore folds from previous state if available try { const folds = prevState ? getAllFolds(prevState) : []; if (folds && folds.length) { restoreFolds(editor, folds); } } catch (error) { warnRecoverable( "Failed to restore folded regions from previous session state.", error, "restore-folds", ); } // Restore last known scroll position if present if ( typeof file.lastScrollTop === "number" || typeof file.lastScrollLeft === "number" ) { setScrollPosition(editor, file.lastScrollTop, file.lastScrollLeft); } void configureLspForFile(file); } function getEmmetSyntaxForFile(file) { const mode = (file?.currentMode || "").toLowerCase(); const name = (file?.filename || "").toLowerCase(); const ext = name.includes(".") ? name.split(".").pop() : ""; if (ext === "tsx" || mode.includes("tsx")) return EmmetKnownSyntax.tsx; if (ext === "jsx" || mode.includes("jsx")) return EmmetKnownSyntax.jsx; if (mode.includes("javascript") && (ext === "jsx" || ext === "tsx")) { return ext === "tsx" ? EmmetKnownSyntax.tsx : EmmetKnownSyntax.jsx; } if (ext === "css" || mode.includes("css")) return EmmetKnownSyntax.css; if (ext === "scss" || mode.includes("scss")) return EmmetKnownSyntax.scss; if (ext === "sass" || mode.includes("sass")) return EmmetKnownSyntax.sass; if (ext === "less" || mode.includes("less")) return EmmetKnownSyntax.less; if (ext === "sss" || mode.includes("sss")) return EmmetKnownSyntax.sss; if (ext === "styl" || ext === "stylus" || mode.includes("styl")) return EmmetKnownSyntax.stylus; if (ext === "postcss" || mode.includes("postcss")) return EmmetKnownSyntax.postcss; if (ext === "xml" || mode.includes("xml")) return EmmetKnownSyntax.xml; if (ext === "xsl" || mode.includes("xsl")) return EmmetKnownSyntax.xsl; if (ext === "haml" || mode.includes("haml")) return EmmetKnownSyntax.haml; if ( ext === "pug" || ext === "jade" || mode.includes("pug") || mode.includes("jade") ) return EmmetKnownSyntax.pug; if (ext === "slim" || mode.includes("slim")) return EmmetKnownSyntax.slim; if (ext === "vue" || mode.includes("vue")) return EmmetKnownSyntax.vue; if (ext === "php" || mode.includes("php")) return EmmetKnownSyntax.html; if ( ext === "htm" || ext === "html" || ext === "xhtml" || mode.includes("html") ) return EmmetKnownSyntax.html; return null; } const $vScrollbar = ScrollBar({ width: scrollbarSize, onscroll: onscrollV, onscrollend: onscrollVend, parent: $body, }); const $hScrollbar = ScrollBar({ width: scrollbarSize, onscroll: onscrollH, onscrollend: onscrollHEnd, parent: $body, placement: "bottom", }); const manager = { files: [], onupdate: () => {}, activeFile: null, isCodeMirror: true, addFile, editor, readOnlyCompartment, getFile, switchFile, moveFileByPinnedState, normalizePinnedTabOrder, syncOpenFileList, hasUnsavedFiles, getEditorHeight, getEditorWidth, header: $header, container: $container, getLspMetadata: buildLspMetadata, get isScrolling() { return isScrolling; }, get openFileList() { if (!$openFileList) initFileTabContainer(); return $openFileList; }, get TIMEOUT_VALUE() { return TIMEOUT_VALUE; }, on(types, callback) { if (!Array.isArray(types)) types = [types]; types.forEach((type) => { if (!events[type]) events[type] = []; events[type].push(callback); }); }, off(types, callback) { if (!Array.isArray(types)) types = [types]; types.forEach((type) => { if (!events[type]) return; events[type] = events[type].filter((c) => c !== callback); }); }, emit(event, ...args) { let detailedEvent; let detailedEventArgs = args.slice(1); if (event === "update") { const subEvent = args[0]; if (subEvent) { detailedEvent = `${event}:${subEvent}`; } } events.emit(event, ...args); if (detailedEvent) { events.emit(detailedEvent, ...detailedEventArgs); } }, /** * Restart LSP for the active file * Useful after stopping/restarting language servers */ restartLsp() { const activeFile = manager.activeFile; if (activeFile?.type === "editor") { void configureLspForFile(activeFile); } }, }; if (typeof document !== "undefined") { const globalTarget = typeof globalThis !== "undefined" ? globalThis : document; const diagnosticsListenerKey = "__acodeDiagnosticsListener"; const existing = globalTarget?.[diagnosticsListenerKey]; if (typeof existing === "function") { document.removeEventListener(LSP_DIAGNOSTICS_EVENT, existing); } let diagnosticsButtonSyncRaf = 0; const listener = () => { cancelAnimationFrame(diagnosticsButtonSyncRaf); diagnosticsButtonSyncRaf = requestAnimationFrame(() => { diagnosticsButtonSyncRaf = 0; const active = manager.activeFile; if (active?.type === "editor") { active.session = editor.state; } toggleProblemButton(); }); }; document.addEventListener(LSP_DIAGNOSTICS_EVENT, listener); if (globalTarget) { globalTarget[diagnosticsListenerKey] = listener; } } lspClientManager.setOptions({ resolveRoot: resolveRootUriForContext, onClientIdle: ({ server }) => { if (server?.id) stopManagedServer(server.id); }, displayFile: async (targetUri) => { if (!targetUri) return null; // Decode URI components (e.g., %40 -> @) since LSP returns encoded URIs const decodedUri = decodeURIComponent(targetUri); const existing = manager.getFile(decodedUri, "uri"); if (existing?.type === "editor") { existing.makeActive(); return editor; } try { await openFile(decodedUri, { render: true }); const opened = manager.getFile(decodedUri, "uri"); if (opened?.type === "editor") { opened.makeActive(); return editor; } } catch (error) { console.error("[LSP] Failed to open file", decodedUri, error); } return null; }, openFile: async (targetUri) => { if (!targetUri) return null; // Decode URI components (e.g., %40 -> @) const decodedUri = decodeURIComponent(targetUri); const existing = manager.getFile(decodedUri, "uri"); if (existing?.type === "editor") { existing.makeActive(); return editor; } try { await openFile(decodedUri, { render: true }); const opened = manager.getFile(decodedUri, "uri"); if (opened?.type === "editor") { opened.makeActive(); return editor; } } catch (error) { console.error("[LSP] Failed to open file", decodedUri, error); } return null; }, resolveLanguageId: (uri) => { if (!uri) return "plaintext"; try { const mode = getModeForPath(uri); if (mode?.name) return String(mode.name).toLowerCase(); } catch (error) { warnRecoverable( `Failed to resolve language id for URI: ${uri}`, error, "lsp-language-id-resolution", ); } return "plaintext"; }, clientExtensions: [diagnosticsClientExt], diagnosticsUiExtension: buildDiagnosticsUiExt(), }); applyLspSettings(); $body.append($container); initModes(); // Initialize CodeMirror modes await setupEditor(); // Initialize theme from settings or fallback try { const desired = appSettings?.value?.editorTheme || "one_dark"; editor.setTheme(desired); } catch (error) { warnRecoverable( "Failed to apply configured editor theme. Falling back to one_dark.", error, "initial-editor-theme", ); editor.setTheme("one_dark"); } // Ensure initial options reflect settings applyOptions(); $hScrollbar.onshow = $vScrollbar.onshow = updateFloatingButton.bind( {}, false, ); $hScrollbar.onhide = $vScrollbar.onhide = updateFloatingButton.bind({}, true); appSettings.on("update:textWrap", function () { updateMargin(); applyOptions(["textWrap"]); }); function updateEditorIndentationSettings() { applyOptions(["softTab", "tabSize"]); } function updateEditorStyleFromSettings() { applyOptions(["fontSize", "editorFont", "lineHeight"]); } function updateEditorWrapFromSettings() { applyOptions(["textWrap"]); if (appSettings.value.textWrap) { $hScrollbar.hide(); } } function updateEditorLineNumbersFromSettings() { applyOptions(["linenumbers", "relativeLineNumbers"]); } appSettings.on("update:tabSize", function () { updateEditorIndentationSettings(); }); appSettings.on("update:softTab", function () { updateEditorIndentationSettings(); }); // Show spaces/tabs and trailing whitespace appSettings.on("update:showSpaces", function () { applyOptions(["showSpaces"]); }); // Font size update for CodeMirror appSettings.on("update:fontSize", function () { updateEditorStyleFromSettings(); }); // Font family update for CodeMirror appSettings.on("update:editorFont", function () { updateEditorStyleFromSettings(); }); appSettings.on("update:lsp", async function () { applyLspSettings(); const active = manager.activeFile; if (active?.type === "editor") { void configureLspForFile(active); } else { detachActiveLsp(); editor.dispatch({ effects: lspCompartment.reconfigure([]) }); await lspClientManager.dispose(); } }); appSettings.on("update:openFileListPos", function (value) { initFileTabContainer(); $vScrollbar.resize(); }); // appSettings.on("update:showPrintMargin", function (value) { // // manager.editor.setOption("showPrintMargin", value); // }); appSettings.on("update:scrollbarSize", function (value) { $vScrollbar.size = value; $hScrollbar.size = value; }); // Live autocompletion (activateOnTyping) appSettings.on("update:liveAutoCompletion", function () { applyOptions(["liveAutoCompletion"]); }); appSettings.on("update:linenumbers", function () { updateMargin(true); updateEditorLineNumbersFromSettings(); }); // Line height update for CodeMirror appSettings.on("update:lineHeight", function () { updateEditorStyleFromSettings(); }); appSettings.on("update:relativeLineNumbers", function () { updateEditorLineNumbersFromSettings(); }); appSettings.on("update:editorTheme", function () { const desiredTheme = appSettings?.value?.editorTheme || "one_dark"; editor.setTheme(desiredTheme); applyOptions(["rainbowBrackets"]); }); appSettings.on("update:lintGutter", function (value) { lspClientManager.setOptions({ diagnosticsUiExtension: lspDiagnosticsUiExtension(value !== false), }); const active = manager.activeFile; if (active?.type === "editor") { void configureLspForFile(active); } }); // appSettings.on("update:elasticTabstops", function (_value) { // // Not applicable in CodeMirror (Ace-era). No-op for now. // }); appSettings.on("update:rtlText", function () { applyOptions(["rtlText"]); }); // appSettings.on("update:hardWrap", function (_value) { // // Not applicable in CodeMirror (Ace-era). No-op for now. // }); // appSettings.on("update:printMargin", function (_value) { // // Not applicable in CodeMirror (Ace-era). No-op for now. // }); appSettings.on("update:colorPreview", function () { const file = manager.activeFile; if (file?.type === "editor") applyFileToEditor(file); }); appSettings.on("update:showSideButtons", function () { updateMargin(); updateSideButtonContainer(); toggleProblemButton(); }); appSettings.on("update:showAnnotations", function () { updateMargin(true); }); appSettings.on("update:fadeFoldWidgets", function () { applyOptions(["fadeFoldWidgets"]); }); // Toggle rainbow brackets appSettings.on("update:rainbowBrackets", function () { applyOptions(["rainbowBrackets"]); }); // Toggle indent guides appSettings.on("update:indentGuides", function () { applyOptions(["indentGuides"]); }); // Keep file.session and cache in sync on every edit function getDocSyncListener() { return EditorView.updateListener.of((update) => { const file = manager.activeFile; if (!file || file.type !== "editor") return; // Only run expensive work when the document actually changed if (!update.docChanged) return; // Mirror latest state only on doc changes to avoid clobbering async loads file.session = update.state; // Debounced change handling (unsaved flag, cache, autosave) if (checkTimeout) clearTimeout(checkTimeout); if (autosaveTimeout) clearTimeout(autosaveTimeout); checkTimeout = setTimeout(async () => { const changed = await file.isChanged(); file.isUnsaved = changed; try { await file.writeToCache(); } catch (error) { warnRecoverable( `Failed to write cache for ${file.filename || file.uri}`, error, `cache-write-${file.id}`, ); } events.emit("file-content-changed", file); manager.onupdate("file-changed"); manager.emit("update", "file-changed"); toggleProblemButton(); const { autosave } = appSettings.value; if (file.uri && changed && autosave) { autosaveTimeout = setTimeout(() => { acode.exec("save", false); }, autosave); } file.markChanged = true; }, TIMEOUT_VALUE); }); } // Register critical listeners manager.on(["file-loaded"], (file) => { if (!file) return; if (manager.activeFile?.id === file.id && file.type === "editor") { applyFileToEditor(file); } }); manager.on(["update:read-only"], () => { const file = manager.activeFile; if (file?.type !== "editor") return; try { const ro = !file.editable || !!file.loading; editor.dispatch({ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(ro)), }); touchSelectionController?.onStateChanged(); } catch (error) { warnRecoverable( "Failed to apply read-only compartment update. Recreating editor state.", error, "readonly-reconfigure", ); // Fallback: full re-apply applyFileToEditor(file); } }); manager.on(["remove-file"], (file) => { detachLspForFile(file); toggleProblemButton(); }); manager.on(["rename-file"], (file) => { if (file?.type !== "editor") return; if (manager.activeFile?.id === file.id) { // Re-apply file to editor to update language/syntax highlighting applyFileToEditor(file); void configureLspForFile(file); } }); // Attach doc-sync listener to the current editor instance try { editor.dispatch({ effects: StateEffect.appendConfig.of(getDocSyncListener()), }); } catch (error) { warnRecoverable( "Failed to attach document sync listener to editor.", error, "doc-sync-listener", ); } return manager; /** * Adds a file to the manager's file list and updates the UI. * @param {File} file - The file to be added. */ function addFile(file) { if (manager.files.includes(file)) return; const insertAt = file.pinned ? getPinnedInsertIndex() : manager.files.length; manager.files.splice(insertAt, 0, file); syncOpenFileList(); $header.text = file.name; toggleProblemButton(); } function getPinnedInsertIndex(skipFile = null) { return manager.files.reduce((count, file) => { if (file === skipFile) return count; return count + (file.pinned ? 1 : 0); }, 0); } function syncOpenFileList() { const $list = manager.openFileList; manager.files.forEach((file) => { $list.append(file.tab); }); } function moveFileByPinnedState(file) { if (!manager.files.includes(file)) return; if (manager.activeFile?.id === file.id) { file.tab.scrollIntoView(); } } function normalizePinnedTabOrder(nextFiles = manager.files) { const pinnedFiles = []; const regularFiles = []; nextFiles.forEach((file) => { if (file.pinned) { pinnedFiles.push(file); return; } regularFiles.push(file); }); manager.files = [...pinnedFiles, ...regularFiles]; syncOpenFileList(); return manager.files; } /** * Sets up the editor with various configurations and event listeners. * @returns {Promise} A promise that resolves once the editor is set up. */ async function setupEditor() { const settings = appSettings.value; const { leftMargin, textWrap, colorPreview, fontSize, lineHeight } = appSettings.value; const scrollMarginTop = 0; const scrollMarginLeft = 0; const scrollMarginRight = textWrap ? 0 : leftMargin; const scrollMarginBottom = 0; let checkTimeout = null; let autosaveTimeout; let scrollTimeout; let scrollSyncRaf = 0; const scroller = editor.scrollDOM; function syncScrollUi() { scrollSyncRaf = 0; onscrolltop(); onscrollleft(); } function handleEditorScroll() { if (!scroller) return; if (!isScrolling) { isScrolling = true; if (hasHoverTooltips(editor.state)) { editor.dispatch({ effects: closeHoverTooltips }); } touchSelectionController?.onScrollStart(); } if (!scrollSyncRaf) { scrollSyncRaf = requestAnimationFrame(syncScrollUi); } clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { isScrolling = false; touchSelectionController?.onScrollEnd(); }, 100); } scroller?.addEventListener("scroll", handleEditorScroll, { passive: true }); syncScrollUi(); keyboardHandler.on("keyboardShowStart", () => { requestAnimationFrame(() => { scrollCursorIntoView({ behavior: "instant" }); }); }); keyboardHandler.on("keyboardShow", scrollCursorIntoView); // Attach native DOM event listeners directly to the editor's contentDOM const contentDOM = editor.contentDOM; const isFocused = contentDOM === document.activeElement || contentDOM.contains(document.activeElement); setNativeContextMenuDisabled(isFocused); contentDOM.addEventListener("focus", (_event) => { setNativeContextMenuDisabled(true); const { activeFile } = manager; if (activeFile) { activeFile.focused = true; } touchSelectionController?.onStateChanged(); }); contentDOM.addEventListener("blur", async (_event) => { setNativeContextMenuDisabled(false); touchSelectionController?.setMenu(false); const { hardKeyboardHidden, keyboardHeight } = await getSystemConfiguration(); const blur = () => { const { activeFile } = manager; if (activeFile) { activeFile.focused = false; activeFile.focusedBefore = false; } }; if ( hardKeyboardHidden === HARDKEYBOARDHIDDEN_NO && keyboardHeight < 100 ) { // external keyboard - blur immediately blur(); return; } // soft keyboard - wait for keyboard to hide const onKeyboardHide = () => { keyboardHandler.off("keyboardHide", onKeyboardHide); blur(); }; keyboardHandler.on("keyboardHide", onKeyboardHide); }); contentDOM.addEventListener("keydown", (event) => { if (event.key === "Escape") { keydownState.esc = { value: true, target: contentDOM }; } }); updateMargin(true); updateSideButtonContainer(); toggleProblemButton(); } /** * Scrolls the cursor into view if it is not currently visible. */ function scrollCursorIntoView(options = {}) { const view = editor; const scroller = view?.scrollDOM; if (!view || !scroller) return; const { behavior = "smooth" } = options; const { head } = view.state.selection.main; const caret = safeCoordsAtPos(view, head); if (!caret) return; const scrollerRect = scroller.getBoundingClientRect(); const relativeTop = caret.top - scrollerRect.top + scroller.scrollTop; const relativeBottom = caret.bottom - scrollerRect.top + scroller.scrollTop; const topMargin = 16; const bottomMargin = 24; const scrollTop = scroller.scrollTop; const visibleTop = scrollTop + topMargin; const visibleBottom = scrollTop + scroller.clientHeight - bottomMargin; if (relativeTop < visibleTop) { const nextTop = Math.max(relativeTop - topMargin, 0); scroller.scrollTo({ top: nextTop, behavior }); } else if (relativeBottom > visibleBottom) { const delta = relativeBottom - visibleBottom; scroller.scrollTo({ top: scrollTop + delta, behavior }); } } /** * Checks if the cursor is visible within the CodeMirror viewport. * @returns {boolean} - True if the cursor is visible, false otherwise. */ function isCursorVisible() { const view = editor; const scroller = view?.scrollDOM; if (!view || !scroller) return true; const { head } = view.state.selection.main; const caret = safeCoordsAtPos(view, head); if (!caret) return true; const scrollerRect = scroller.getBoundingClientRect(); return caret.top >= scrollerRect.top && caret.bottom <= scrollerRect.bottom; } function safeCoordsAtPos(view, pos) { try { return view.coordsAtPos(pos); } catch (_) { return null; } } /** * Sets the vertical scroll value of the editor. This is called when the editor is scrolled horizontally using the scrollbar. * @param {Number} value */ function onscrollV(value) { const scroller = editor?.scrollDOM; if (!scroller) return; const normalized = clamp01(value); const maxScroll = Math.max( scroller.scrollHeight - scroller.clientHeight, 0, ); preventScrollbarV = true; scroller.scrollTop = normalized * maxScroll; lastScrollTop = scroller.scrollTop; } /** * Handles the onscroll event for the vend element. */ function onscrollVend() { preventScrollbarV = false; setVScrollValue(); } /** * Sets the horizontal scroll value of the editor. This is called when the editor is scrolled vertically using the scrollbar. * @param {number} value - The scroll value. */ function onscrollH(value) { if (appSettings.value.textWrap) return; const scroller = editor?.scrollDOM; if (!scroller) return; const normalized = clamp01(value); const maxScroll = Math.max(scroller.scrollWidth - scroller.clientWidth, 0); preventScrollbarH = true; scroller.scrollLeft = normalized * maxScroll; lastScrollLeft = scroller.scrollLeft; } /** * Handles the event when the horizontal scrollbar reaches the end. */ function onscrollHEnd() { preventScrollbarH = false; setHScrollValue(); } /** * Sets scrollbars value based on the editor's scroll position. */ function setHScrollValue() { if (appSettings.value.textWrap || preventScrollbarH) return; const scroller = editor?.scrollDOM; if (!scroller) return; const maxScroll = Math.max(scroller.scrollWidth - scroller.clientWidth, 0); if (maxScroll <= 0) { lastScrollLeft = 0; $hScrollbar.value = 0; return; } const scrollLeft = scroller.scrollLeft; if (scrollLeft === lastScrollLeft) return; lastScrollLeft = scrollLeft; const factor = scrollLeft / maxScroll; $hScrollbar.value = clamp01(factor); } /** * Handles the scroll left event. * Updates the horizontal scroll value and renders the horizontal scrollbar. */ function onscrollleft() { if (appSettings.value.textWrap) { $hScrollbar.hide(); return; } const scroller = editor?.scrollDOM; if (!scroller) return; const maxScroll = Math.max(scroller.scrollWidth - scroller.clientWidth, 0); if (maxScroll <= 0) { $hScrollbar.hide(); lastScrollLeft = 0; $hScrollbar.value = 0; return; } setHScrollValue(); $hScrollbar.render(); } /** * Sets scrollbars value based on the editor's scroll position. */ function setVScrollValue() { if (preventScrollbarV) return; const scroller = editor?.scrollDOM; if (!scroller) return; const maxScroll = Math.max( scroller.scrollHeight - scroller.clientHeight, 0, ); if (maxScroll <= 0) { lastScrollTop = 0; $vScrollbar.value = 0; return; } const scrollTop = scroller.scrollTop; if (scrollTop === lastScrollTop) return; lastScrollTop = scrollTop; const factor = scrollTop / maxScroll; $vScrollbar.value = clamp01(factor); } /** * Handles the scroll top event. * Updates the vertical scroll value and renders the vertical scrollbar. */ function onscrolltop() { const scroller = editor?.scrollDOM; if (!scroller) return; const maxScroll = Math.max( scroller.scrollHeight - scroller.clientHeight, 0, ); if (maxScroll <= 0) { $vScrollbar.hide(); lastScrollTop = 0; $vScrollbar.value = 0; return; } setVScrollValue(); $vScrollbar.render(); } function clamp01(value) { if (value <= 0) return 0; if (value >= 1) return 1; return value; } /** * Updates the floating button visibility based on the provided show parameter. * @param {boolean} [show=false] - Indicates whether to show the floating button. */ function updateFloatingButton(show = false) { const { $headerToggler } = acode; const { $toggler } = quickTools; if (show) { if (scrollBarVisibilityCount) --scrollBarVisibilityCount; if (!scrollBarVisibilityCount) { clearTimeout(timeoutHeaderToggler); clearTimeout(timeoutQuicktoolsToggler); if (appSettings.value.floatingButton) { $toggler.classList.remove("hide"); root.appendOuter($toggler); } $headerToggler.classList.remove("hide"); root.appendOuter($headerToggler); } return; } if (!scrollBarVisibilityCount) { if ($toggler.isConnected) { $toggler.classList.add("hide"); timeoutQuicktoolsToggler = setTimeout(() => $toggler.remove(), 300); } if ($headerToggler.isConnected) { $headerToggler.classList.add("hide"); timeoutHeaderToggler = setTimeout(() => $headerToggler.remove(), 300); } } ++scrollBarVisibilityCount; } /** * Toggles the visibility of the problem button based on the presence of annotations in the files. */ function fileHasProblems(file) { const state = getDiagnosticStateForFile(file); if (!state) return false; const session = file.session; if (session && typeof session.getAnnotations === "function") { try { const annotations = session.getAnnotations() || []; if (annotations.length) return true; } catch (error) { warnRecoverable( "Failed to read editor annotations while checking problems.", error, "read-annotations", ); } } if (typeof state.field !== "function") return false; try { const diagnostics = getLspDiagnostics(state); return diagnostics.length > 0; } catch (error) { warnRecoverable( "Failed to read LSP diagnostics while checking problems.", error, "read-lsp-diagnostics", ); } return false; } function toggleProblemButton() { const { showSideButtons } = appSettings.value; if (!showSideButtons) { problemButton.hide(); return; } const hasProblems = manager.files.some((file) => fileHasProblems(file)); if (hasProblems) { problemButton.show(); } else { problemButton.hide(); } } function getDiagnosticStateForFile(file) { if (!file || file.type !== "editor") return null; if (manager.activeFile?.id === file.id && editor?.state) { return editor.state; } return file.session || null; } /** * Updates the side button container based on the value of `showSideButtons` in `appSettings`. * If `showSideButtons` is `false`, the side button container is removed from the DOM. * If `showSideButtons` is `true`, the side button container is appended to the body element. */ function updateSideButtonContainer() { const { showSideButtons } = appSettings.value; if (!showSideButtons) { sideButtonContainer.remove(); return; } $body.append(sideButtonContainer); } /** * Updates the margin of the editor and optionally updates the gutter settings. * @param {boolean} [updateGutter=false] - Whether to update the gutter settings. */ function updateMargin(updateGutter = false) { const { showSideButtons, linenumbers, showAnnotations } = appSettings.value; const top = 0; const bottom = 0; const right = showSideButtons ? 15 : 0; const left = linenumbers ? (showAnnotations ? 0 : -16) : 0; // TODO //editor.renderer.setMargin(top, bottom, left, right); if (!updateGutter) return; // editor.setOptions({ // showGutter: linenumbers || showAnnotations, // showLineNumbers: linenumbers, // }); } /** * Switches the active file in the editor. * @param {string} id - The ID of the file to switch to. */ function switchFile(id) { const { id: activeFileId } = manager.activeFile || {}; if (activeFileId === id) return; const file = manager.getFile(id); if (!file) return; manager.activeFile?.tab.classList.remove("active"); // Hide previous content if it was non-editor if (manager.activeFile?.type !== "editor" && manager.activeFile?.content) { manager.activeFile.content.style.display = "none"; } // Persist the previous editor's state before switching away const prev = manager.activeFile; if (prev?.type === "editor") { prev.session = editor.state; prev.lastScrollTop = editor.scrollDOM?.scrollTop || 0; prev.lastScrollLeft = editor.scrollDOM?.scrollLeft || 0; } manager.activeFile = file; if (file.type === "editor") { touchSelectionController?.setEnabled(true); // Apply active file content and language to CodeMirror applyFileToEditor(file); $container.style.display = "block"; $hScrollbar.hideImmediately(); $vScrollbar.hideImmediately(); setVScrollValue(); if (!appSettings.value.textWrap) { setHScrollValue(); } } else { touchSelectionController?.setEnabled(false); $container.style.display = "none"; if (file.content) { file.content.style.display = "block"; if (!file.content.parentElement) { $container.parentElement.appendChild(file.content); } } } file.tab.classList.add("active"); file.tab.scrollIntoView(); $header.text = file.filename; $header.subText = file.headerSubtitle || ""; manager.onupdate("switch-file"); events.emit("switch-file", file); toggleProblemButton(); } /** * Initializes the file tab container. */ function initFileTabContainer() { let $list; if ($openFileList) { if ($openFileList.classList.contains("collapsible")) { $list = Array.from($openFileList.$ul.children); } else { $list = Array.from($openFileList.children); } $openFileList.remove(); } // show open file list in header const { openFileListPos } = appSettings.value; if ( openFileListPos === appSettings.OPEN_FILE_LIST_POS_HEADER || openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM ) { if (!$openFileList?.classList.contains("open-file-list")) { $openFileList =
          ; } if ($list) $openFileList.append(...$list); if (openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM) { $container.parentElement.insertAdjacentElement( "afterend", $openFileList, ); } else { $header.insertAdjacentElement("afterend", $openFileList); } root.classList.add("top-bar"); const oldAppend = $openFileList.append; $openFileList.append = (...args) => { oldAppend.apply($openFileList, args); }; } else { $openFileList = list(strings["active files"]); $openFileList.classList.add("file-list"); if ($list) $openFileList.$ul.append(...$list); $openFileList.expand(); const oldAppend = $openFileList.$ul.append; $openFileList.append = (...args) => { oldAppend.apply($openFileList.$ul, args); }; const files = sidebarApps.get("files"); files.insertBefore($openFileList, files.firstElementChild); root.classList.remove("top-bar"); } root.setAttribute("open-file-list-pos", openFileListPos); manager.emit("int-open-file-list", openFileListPos); } /** * Checks if there are any unsaved files in the manager. * @returns {number} The number of unsaved files. */ function hasUnsavedFiles() { const unsavedFiles = manager.files.filter((file) => file.isUnsaved); return unsavedFiles.length; } /** * Gets a file from the file manager * @param {string|number} checkFor * @param {"id"|"name"|"uri"} [type] * @returns {File} */ function getFile(checkFor, type = "id") { return manager.files.find((file) => { switch (type) { case "id": if (file.id === checkFor) return true; return false; case "name": if (file.filename === checkFor) return true; return false; case "uri": if (file.uri === checkFor) return true; return false; default: return false; } }); } /** * Gets the height of the editor * @param {object} editor * @returns */ function getEditorHeight(editor) { try { const view = editor; if (!view || !view.scrollDOM) return 0; const total = view.scrollDOM.scrollHeight || 0; const viewport = view.scrollDOM.clientHeight || 0; return Math.max(total - viewport, 0); } catch (_) { return 0; } } /** * Gets the height of the editor * @param {object} editor * @returns */ function getEditorWidth(editor) { try { const view = editor; if (!view || !view.scrollDOM) return 0; const total = view.scrollDOM.scrollWidth || 0; const viewport = view.scrollDOM.clientWidth || 0; let width = Math.max(total - viewport, 0); if (!appSettings.value.textWrap) { const { leftMargin = 0 } = appSettings.value; width += leftMargin || 0; } return width; } catch (_) { return 0; } } } export default EditorManager; ================================================ FILE: src/lib/fileList.js ================================================ import fsOperation from "fileSystem"; import toast from "components/toast"; import picomatch from "picomatch/posix"; import Url from "utils/Url"; import { addedFolder } from "./openFolder"; import settings from "./settings"; /** * @typedef {import('fileSystem').File} File */ const filesTree = {}; const events = { "add-file": [], "remove-file": [], "add-folder": [], "remove-folder": [], refresh: [], }; export function initFileList() { if (editorManager?.activeFile.loading) { editorManager.activeFile.on("loadend", initFileList); return; } // editorManager.on('add-folder', onAddFolder); editorManager.on("remove-folder", onRemoveFolder); settings.on("update:excludeFolders:after", refresh); } /** * Add a file to the list * @param {string} parent file directory * @param {string} child file url */ export async function append(parent, child) { const tree = getTree(Object.values(filesTree), parent); if (!tree || !tree.children) return; const childTree = await Tree.create(child); tree.children.push(childTree); getAllFiles(childTree); emit("add-file", childTree); } /** * Remove a file from the list * @param {string} item url */ export function remove(item) { if (filesTree[item]) { delete filesTree[item]; emit("remove-file", item); return; } const tree = getTree(Object.values(filesTree), item); if (!tree) return; const { parent } = tree; const index = parent.children.indexOf(tree); parent.children.splice(index, 1); emit("remove-file", tree); } /** * Refresh file list */ export async function refresh() { Object.keys(filesTree).forEach((key) => { delete filesTree[key]; }); await Promise.all( addedFolder.map(async ({ url, title }) => { const tree = await Tree.createRoot(url, title); filesTree[url] = tree; getAllFiles(tree); }), ); emit("refresh", filesTree); } /** * Renames a tree * @param {string} oldUrl * @param {string} newUrl * @returns */ export function rename(oldUrl, newUrl) { const tree = getTree(Object.values(filesTree), oldUrl); if (!tree) return; tree.update(newUrl); } /** * Get all files in a folder * @param {string|()=>object} dir * @returns {Tree[]} */ export default function files(dir) { const listedDirs = []; let transform = (item) => item; if (typeof dir === "string") { return Object.values(filesTree).find((item) => getFile(dir, item)); } else if (typeof dir === "function") { transform = dir; } const allFiles = []; Object.values(filesTree).forEach((item) => { allFiles.push(...flattenTree(item, transform, listedDirs)); }); return allFiles; } /** * @typedef {'add-file'|'remove-file'|'add-folder'|'remove-folder'|'refresh'} FileListEvent */ /** * Adds event listener for file list * @param {FileListEvent} event - Event name * @param {(tree:Tree)=>void} callback - Callback function */ files.on = function (event, callback) { if (!events[event]) events[event] = []; events[event].push(callback); }; /** * Removes event listener for file list * @param {FileListEvent} event - Event name * @param {(tree:Tree)=>void} callback - Callback function */ files.off = function (event, callback) { if (!events[event]) return; events[event] = events[event].filter((cb) => cb !== callback); }; /** * Get directory tree * @param {Tree[]} treeList list of tree * @param {string} dir path to find * @returns {Tree} */ function getTree(treeList, dir) { if (!treeList) return; let tree = treeList.find(({ url }) => url === dir); if (tree) return tree; for (const item of treeList) { tree = getTree(item.children, dir); if (tree) return tree; } return null; } /** * Get all files in a folder * e.g /dir1/dir2/dir3 * This function will first test if dir1 exists in the tree, * if not, it will return null, otherwise it will traverse the tree * and return the files in dir3 * @param {string} path - Folder path * @param {Tree} tree - Files tree */ function getFile(path, tree) { const { children } = tree; let { url } = tree; if (url === path) return tree; if (!children) return null; const len = children.length; for (let i = 0; i < len; i++) { const item = children[i]; const result = getFile(path, item); if (result) return result; } return null; } /** * Get all files * @param {Tree} tree * @param {(item:Tree)=>object} transform */ function flattenTree(tree, transform, listedDirs) { const list = []; const { children } = tree; if (!children) { return [transform(tree)]; } if (listedDirs.includes(tree.url)) return list; listedDirs.push(tree.url); children.forEach((item) => { if (item.children) list.push(...flattenTree(item, transform, listedDirs)); else list.push(transform(item)); }); return list; } /** * Called when a folder is added * @param {{url: string, name: string}} folder - Folder path */ export async function addRoot({ url, name }) { try { const TERMUX_STORAGE = "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome::/data/data/com.termux/files/home/storage"; const TERMUX_SHARED = "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome::/data/data/com.termux/files/home/storage/shared"; if (url === TERMUX_STORAGE) return; if (url === TERMUX_SHARED) return; const tree = await Tree.createRoot(url, name); filesTree[url] = tree; getAllFiles(tree); emit("add-folder", tree); } catch (error) { // ignore window.log("error", error); } } /** * Called when a folder is removed * @param {{url: string, name: string}} folder - Folder path */ function onRemoveFolder({ url }) { const tree = filesTree[url]; if (!tree) return; delete filesTree[url]; emit("remove-folder", tree); } /** * Get all file recursively * @param {Tree} parent - An array to store files * @param {Tree} [root] - Root path */ async function getAllFiles(parent, root) { root = root || parent.root; if (!parent.children || !root.isConnected) return; try { const entries = await fsOperation(parent.url).lsDir(); const promises = []; for (const item of entries) { promises.push(createChildTree(parent, item, root)); } await Promise.all(promises); } catch (error) { // retry after 3s parent.retriedCount += 1; if (parent.retriedCount > settings.value.maxRetryCount) return; if (settings.value.showRetryToast) { toast(`retrying: ${parent.path}`); } setTimeout(() => { // why not outside? because parent may be removed if (!root.isConnected) return; parent.children.length = 0; getAllFiles(parent); }, 3000); } } /** * Emit an event * @param {string} event * @param {...any} args */ function emit(event, ...args) { const list = events[event]; if (!list) return; list.forEach((fn) => fn(...args)); } /** * Create a child tree * @param {Tree} parent * @param {File} item * @param {Tree} root */ async function createChildTree(parent, item, root) { if (!root.isConnected) return; const { name, url, isDirectory } = item; const exists = parent.children.findIndex(({ value }) => value === url); if (exists > -1) { return; } const file = await Tree.create(url, name, isDirectory); if (!root.isConnected) return; const existingTree = getTree(Object.values(filesTree), file.url); if (existingTree) { file.children = existingTree.children; parent.children.push(file); return; } parent.children.push(file); if (isDirectory) { const ignore = picomatch.isMatch( Url.join(file.path, ""), settings.value.excludeFolders, { matchBase: true }, ); if (ignore) return; getAllFiles(file, root); return; } emit("push-file", file); } export class Tree { /**@type {string}*/ #name; /**@type {string}*/ #url; /**@type {string}*/ #path; /**@type {Array}*/ #children; /**@type {Tree}*/ #parent; retriedCount = 0; /** * Create a tree using constructor * @param {string} name * @param {string} root * @param {string} url * @param {boolean} isDirectory */ constructor(name, url, isDirectory) { this.#name = name; this.#url = url; this.#children = isDirectory ? this.#childrenArray() : null; this.#parent = null; } #childrenArray() { const ar = []; const oldPush = ar.push; ar.push = (...args) => { args.forEach((item) => { if (!(item instanceof Tree)) throw new Error("Invalid tree"); item.parent = this; oldPush.call(ar, item); }); }; return ar; } /** * Create a tree * @param {string} url file url * @param {string} [name] file name * @param {boolean} [isDirectory] if the file is a directory */ static async create(url, name, isDirectory) { if (!name && !isDirectory) { const stat = await fsOperation(url).stat(); name = stat.name; isDirectory = stat.isDirectory; } return new Tree(name, url, isDirectory); } /** * Create a root tree * @param {string} url * @param {string} name * @returns */ static async createRoot(url, name) { const tree = await Tree.create(url, name, true); tree.#path = name; return tree; } /**@returns {string} */ get name() { return this.#name; } /**@returns {string} */ get url() { return this.#url; } /**@returns {string} */ get path() { return this.#path; } /**@returns {Array} */ get children() { return this.#children; } set children(value) { if (!Array.isArray(value)) throw new Error("Invalid children"); this.#children = value; } /**@returns {Tree} */ get parent() { return this.#parent; } /**@param {Tree} value */ set parent(value) { if (!(value instanceof Tree)) throw new Error("Invalid parent"); this.#parent = value; if (this.#parent) { this.#path = Url.join(this.#parent.path, this.#name); } } /** * Check if the root of the tree is added to the open folder list. * @returns {boolean} */ get isConnected() { const root = this.root; return !!addedFolder.find(({ url }) => url === root.url); } /** * Get the root of the tree * @returns {Tree} */ get root() { let root = this; while (root.parent) { root = root.parent; } return root; } /** * Update tree name and url * @param {string} url * @param {string} [name] */ update(url, name) { if (!name) name = Url.basename(url); this.#url = url; this.#name = name; this.#path = Url.join(this.#parent.path, name); getAllFiles(this); } /** * @typedef {object} TreeJson * @property {string} name * @property {string} url * @property {string} path * @property {string} parent * @property {boolean} isDirectory */ /** * To tree object to json * @returns {TreeJson} */ toJSON() { return { name: this.#name, url: this.#url, path: this.#path, parent: this.#parent?.url, isDirectory: !!this.#children, }; } /** * Create a tree from json * @param {TreeJson} json * @returns {Tree} */ static fromJSON(json) { const { name, url, path, parent, isDirectory } = json; const tree = new Tree(name, url, isDirectory); tree.#parent = getTree(Object.values(filesTree), parent); tree.#path = path; return tree; } } ================================================ FILE: src/lib/fileTypeHandler.js ================================================ /** * @typedef {Object} FileTypeHandler * @property {string} id - Unique identifier for the handler * @property {string[]} extensions - File extensions this handler supports (without dots) * @property {function} handleFile - Function that handles the file */ /** * @typedef {Object} FileInfo * @property {string} name - File name * @property {string} uri - File URI * @property {Object} stats - File stats * @property {boolean} readOnly - Whether the file is read-only * @property {Object} options - Additional options passed during file open */ class FileTypeHandlerRegistry { #handlers = new Map(); /** * Register a file type handler * @param {string} id - Unique identifier for the handler * @param {Object} options - Handler options * @param {string[]} options.extensions - File extensions to handle (without dots) * @param {function(FileInfo): Promise} options.handleFile - Async function to handle the file * @throws {Error} If id is already registered or required options are missing */ registerFileHandler(id, { extensions, handleFile }) { if (this.#handlers.has(id)) { throw new Error(`Handler with id '${id}' is already registered`); } if (!extensions?.length) { throw new Error("extensions array is required"); } if (typeof handleFile !== "function") { throw new Error("handleFile function is required"); } // Normalize extensions (remove dots if present, convert to lowercase) const normalizedExts = extensions.map((ext) => ext.toLowerCase().replace(/^\./, ""), ); this.#handlers.set(id, { extensions: normalizedExts, handleFile, }); } /** * Unregister a file type handler * @param {string} id - The handler id to remove */ unregisterFileHandler(id) { this.#handlers.delete(id); } /** * Get a file handler for a given filename * @param {string} filename * @returns {Object|null} The matching handler or null if none found */ getFileHandler(filename) { const ext = filename.split(".").pop().toLowerCase(); for (const [id, handler] of this.#handlers) { if ( handler.extensions.includes(ext) || handler.extensions.includes("*") ) { return { id, ...handler, }; } } return null; } /** * Get all registered handlers * @returns {Map} Map of all registered handlers */ getHandlers() { return new Map(this.#handlers); } } export const fileTypeHandler = new FileTypeHandlerRegistry(); export default fileTypeHandler; ================================================ FILE: src/lib/fonts.js ================================================ import fsOperation from "fileSystem"; import loader from "dialogs/loader"; import helpers from "utils/helpers"; import Url from "utils/Url"; import firaCode from "../res/fonts/FiraCode.ttf"; import MesloLGSNFRegular from "../res/fonts/MesloLGSNFRegular.ttf"; import robotoMono from "../res/fonts/RobotoMono.ttf"; const fonts = new Map(); const customFontNames = new Set(); const CUSTOM_FONTS_KEY = "custom_fonts"; const FONT_FACE_STYLE_ID = "font-face-style"; const EDITOR_STYLE_ID = "editor-font-style"; const APP_STYLE_ID = "app-font-style"; const DEFAULT_EDITOR_FONT = "Roboto Mono"; const DEFAULT_APP_FONT_STACK = `"Roboto", sans-serif`; add( "Fira Code", `@font-face { font-family: 'Fira Code'; src: url(${firaCode}) format('truetype'); font-weight: 300 700; font-style: normal; }`, ); add( "Roboto Mono", `@font-face { font-family: 'Roboto Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(${robotoMono}) format('truetype'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }`, ); add( "MesloLGS NF Regular", `@font-face { font-family: 'MesloLGS NF Regular'; font-style: normal; font-weight: normal; src: url(${MesloLGSNFRegular}) format('truetype'); }`, ); add( "Source Code", `@font-face { font-family: 'Source Code'; src: url(https://acode.app/SourceCodePro.ttf) format('truetype'); font-weight: 300 700; font-style: normal; }`, ); add( "Victor Mono Italic", `@font-face { font-family: 'Victor Mono Italic'; src: url(https://acode.app/VictorMono-Italic.otf) format('truetype'); font-style: normal; }`, ); add( "Victor Mono Medium", `@font-face { font-family: 'Victor Mono Medium'; src: url(https://acode.app/VictorMono-Medium.otf) format('truetype'); font-weight: medium; font-style: normal; }`, ); add( "Cascadia Code", `@font-face { font-family: 'Cascadia Code'; src: url(https://acode.app/CascadiaCode.ttf) format('truetype'); font-weight: 300 700; font-style: normal; }`, ); add( "Proggy Clean", `@font-face { font-family: 'Proggy Clean'; src: url(https://acode.app/ProggyClean.ttf) format('truetype'); font-weight: 300 700; font-style: normal; }`, ); add( "JetBrains Mono Bold", `@font-face { font-family: 'JetBrains Mono Bold'; src: url(https://acode.app/JetBrainsMono-Bold.ttf) format('truetype'); font-weight: bold; }`, ); add( "JetBrains Mono Regular", `@font-face { font-family: 'JetBrains Mono Regular'; src: url(https://acode.app/JetBrainsMono-Regular.ttf) format('truetype'); font-weight: 300 700; font-style: normal; }`, ); add( "Noto Mono", `@font-face { font-display: swap; font-family: 'Noto Mono'; src: url(https://acode.app/NotoMono-Regular.woff) format("woff"); font-weight: 400; font-style: normal; unicode-range: U+0590-06FF; }`, ); // Load custom fonts on module initialization loadCustomFonts(); function add(name, css) { fonts.set(name, css); } function addCustom(name, css) { fonts.set(name, css); customFontNames.add(name); saveCustomFonts(); } function saveCustomFonts() { const customFonts = {}; for (const name of customFontNames) { const css = fonts.get(name); if (css) { customFonts[name] = css; } } localStorage.setItem(CUSTOM_FONTS_KEY, JSON.stringify(customFonts)); } function loadCustomFonts() { try { const customFonts = localStorage.getItem(CUSTOM_FONTS_KEY); if (customFonts) { const parsed = JSON.parse(customFonts); for (const [name, css] of Object.entries(parsed)) { fonts.set(name, css); customFontNames.add(name); } } } catch (error) { console.error("Failed to load custom fonts:", error); } } function get(name) { return fonts.get(name); } function getNames() { return [...fonts.keys()]; } function remove(name) { const result = fonts.delete(name); if (result) { customFontNames.delete(name); saveCustomFonts(); } return result; } function has(name) { return fonts.has(name); } function isCustom(name) { return customFontNames.has(name); } async function setEditorFont(name) { loader.showTitleLoader(); try { await loadFont(name); const $style = ensureStyleElement(EDITOR_STYLE_ID); $style.textContent = `.editor-container.ace_editor{ font-family: "${name}", NotoMono, Monaco, MONOSPACE !important; } .ace_text{ font-family: inherit !important; }`; } catch (error) { toast(`${name} font not found`, "error"); setEditorFont(DEFAULT_EDITOR_FONT); } finally { loader.removeTitleLoader(); } } async function setAppFont(name) { const $style = ensureStyleElement(APP_STYLE_ID); if (!name) { $style.textContent = `:root { --app-font-family: ${DEFAULT_APP_FONT_STACK}; }`; return; } try { await loadFont(name); $style.textContent = `:root { --app-font-family: "${name}", ${DEFAULT_APP_FONT_STACK}; }`; } catch (error) { toast(`${name} font not found`, "error"); $style.textContent = `:root { --app-font-family: ${DEFAULT_APP_FONT_STACK}; }`; } } async function downloadFont(name, link) { const FONT_DIR = Url.join(DATA_STORAGE, "fonts"); const FONT_FILE = Url.join(FONT_DIR, name); const fs = fsOperation(FONT_FILE); if (await fs.exists()) return FONT_FILE; if (!(await fsOperation(FONT_DIR).exists())) { await fsOperation(DATA_STORAGE).createDirectory("fonts"); } const font = await fsOperation(link).readFile(); console.log("fonts content : ", font); await fsOperation(FONT_DIR).createFile(name, font); return FONT_FILE; } async function loadFont(name) { const $style = ensureStyleElement(FONT_FACE_STYLE_ID); let css = get(name); if (!css) { throw new Error(`Font ${name} not found`); } // Get all URL font references const urls = [...css.matchAll(/url\((.*?)\)/g)].map((match) => match[1]); // Download and replace URLs for (const url of urls) { if (!/^https?/.test(url)) continue; if (/^https?:\/\/localhost/.test(url)) continue; const fontFile = await downloadFont(name, url); const internalUrl = await helpers.toInternalUri(fontFile); css = css.replace(url, internalUrl); } // Add font face to document if not already present if (!$style.textContent.includes(`font-family: '${name}'`)) { $style.textContent = `${$style.textContent}\n${css}`; } return css; } function ensureStyleElement(id) { const selector = `style#${id}`; const $style = tag.get(selector) ?? ; if (!$style.isConnected) { document.head.append($style); } return $style; } export default { add, addCustom, get, getNames, remove, has, isCustom, setFont: setEditorFont, setEditorFont, setAppFont, loadFont, }; ================================================ FILE: src/lib/installPlugin.js ================================================ import fsOperation from "fileSystem"; import ajax from "@deadlyjack/ajax"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import purchaseListener from "handlers/purchase"; import JSZip from "jszip"; import helpers from "utils/helpers"; import Url from "utils/Url"; import constants from "./constants"; import InstallState from "./installState"; import loadPlugin from "./loadPlugin"; /** @type {import("dialogs/loader").Loader} */ let loaderDialog; /** @type {Array<() => Promise>} */ let depsLoaders; /** * Installs a plugin. * @param {string} id * @param {string} name * @param {string} purchaseToken * @param {boolean} isDependency */ export default async function installPlugin( id, name, purchaseToken, isDependency, ) { if (!isDependency) { loaderDialog = loader.create(name || "Plugin", strings.installing, { timeout: 6000, }); depsLoaders = []; } let pluginDir; let pluginUrl; let state; try { if (!(await fsOperation(PLUGIN_DIR).exists())) { await fsOperation(DATA_STORAGE).createDirectory("plugins"); } } catch (error) { window.log("error", error); } if (!/^(https?|file|content):/.test(id)) { pluginUrl = Url.join( constants.API_BASE, "plugin/download/", `${id}?device=${device.uuid}`, ); if (purchaseToken) pluginUrl += `&token=${purchaseToken}`; pluginUrl += `&package=${BuildInfo.packageName}`; pluginUrl += `&version=${device.version}`; pluginDir = Url.join(PLUGIN_DIR, id); } else { pluginUrl = id; } try { if (!isDependency) loaderDialog.show(); let plugin; if ( pluginUrl.includes(constants.API_BASE) || pluginUrl.startsWith("file:") || pluginUrl.startsWith("content:") ) { // Use fsOperation for Acode registry URL plugin = await fsOperation(pluginUrl).readFile( undefined, (loaded, total) => { loaderDialog.setMessage( `${strings.loading} ${((loaded / total) * 100).toFixed(2)}%`, ); }, ); } else { // cordova http plugin for others plugin = await new Promise((resolve, reject) => { cordova.plugin.http.sendRequest( pluginUrl, { method: "GET", responseType: "arraybuffer", }, (response) => { resolve(response.data); loaderDialog.setMessage(`${strings.loading} 100%`); }, (error) => { reject(error); }, ); }); } if (plugin) { const zip = new JSZip(); await zip.loadAsync(plugin); if (!zip.files["plugin.json"]) { throw new Error(strings["invalid plugin"]); } /** @type {{ dependencies: string[] }} */ const pluginJson = JSON.parse( await zip.files["plugin.json"].async("text"), ); /** patch main in manifest */ if (!zip.files[pluginJson.main]) { pluginJson.main = "main.js"; } /** patch icon in manifest */ if (!zip.files[pluginJson.icon]) { pluginJson.icon = "icon.png"; } /** patch readme in manifest */ if (!zip.files[pluginJson.readme]) { pluginJson.readme = "readme.md"; } if (!zip.files[pluginJson.main]) { throw new Error(strings["invalid plugin"]); } if (!isDependency && pluginJson.dependencies) { const manifests = await resolveDepsManifest(pluginJson.dependencies); let titleText; if (manifests.length > 1) { titleText = "Acode wants to install the following dependencies:"; } else { titleText = "Acode wants to install the following dependency:"; } const shouldInstall = await confirm( "Installer Notice", titleText + "

          " + manifests.map((value) => value.name).join(", "), true, ); if (shouldInstall) { for (const manifest of manifests) { const hasError = await resolveDep(manifest); if (hasError) throw new Error(strings.failed); } } else { return; } } if (!pluginDir) { pluginJson.source = pluginUrl; id = pluginJson.id; pluginDir = Url.join(PLUGIN_DIR, id); } state = await InstallState.new(id); if (!(await fsOperation(pluginDir).exists())) { await fsOperation(PLUGIN_DIR).createDirectory(id); } // Track unsafe absolute entries to skip const ignoredUnsafeEntries = new Set(); const files = Object.keys(zip.files); const limit = 2; async function processFile(file) { try { const entry = zip.files[file]; let correctFile = file.replace(/\\/g, "/"); const isDirEntry = entry.dir || correctFile.endsWith("/"); if (isUnsafeAbsolutePath(file)) { ignoredUnsafeEntries.add(file); return; } correctFile = sanitizeZipPath(correctFile, isDirEntry); if (!correctFile) return; const fileUrl = Url.join(pluginDir, correctFile); // Handle directory entries if (isDirEntry) { await createFileRecursive(pluginDir, correctFile, true); return; } // Ensure parent directory exists const lastSlash = correctFile.lastIndexOf("/"); if (lastSlash !== -1) { const parentRel = correctFile.slice(0, lastSlash + 1); await createFileRecursive(pluginDir, parentRel, true); } if (!state.exists(correctFile)) { await createFileRecursive(pluginDir, correctFile, false); } let data = await entry.async("ArrayBuffer"); if (file === "plugin.json") { data = JSON.stringify(pluginJson); } if (!(await state.isUpdated(correctFile, data))) return; await fsOperation(fileUrl).writeFile(data); } catch (error) { console.error(`Error processing file ${file}:`, error); } } // Process in batches for (let i = 0; i < files.length; i += limit) { const batch = files.slice(i, i + limit); await Promise.allSettled(batch.map(processFile)); // Allow UI thread to breathe await new Promise((r) => setTimeout(r, 0)); } // Emit a non-blocking warning if any unsafe entries were skipped if (!isDependency && ignoredUnsafeEntries.size) { const sample = Array.from(ignoredUnsafeEntries).slice(0, 3).join(", "); loaderDialog.setMessage( `Skipped ${ignoredUnsafeEntries.size} unsafe archive entr${ ignoredUnsafeEntries.size === 1 ? "y" : "ies" } (e.g., ${sample})`, ); console.warn( "Plugin installer: skipped unsafe absolute paths in archive:", Array.from(ignoredUnsafeEntries), ); } if (isDependency) { depsLoaders.push(async () => { await loadPlugin(id, true); }); } else { for (const loader of depsLoaders) { await loader(); } await loadPlugin(id, true); } await state.save(); deleteRedundantFiles(pluginDir, state); } } catch (err) { try { // Clear the install state if installation fails if (state) await state.clear(); // Delete the plugin directory if it was created if (pluginDir && (await fsOperation(pluginDir).exists())) { await fsOperation(pluginDir).delete(); } } catch (cleanupError) { console.error("Cleanup failed:", cleanupError); } throw err; } finally { if (!isDependency) { loaderDialog.destroy(); } } } /** * Create directory recursively * @param {string} parent * @param {Array | string} dir */ async function createFileRecursive(parent, dir, shouldBeDirAtEnd) { let wantDirEnd = !!shouldBeDirAtEnd; /** @type {string[]} */ let parts; if (typeof dir === "string") { if (dir.endsWith("/")) wantDirEnd = true; dir = dir.replace(/\\/g, "/"); parts = dir.split("/"); } else { parts = dir; } parts = parts.filter((d) => d); const cd = parts.shift(); if (!cd) return; const newParent = Url.join(parent, cd); const isLast = parts.length === 0; const needDir = !isLast || wantDirEnd; if (!(await fsOperation(newParent).exists())) { if (needDir) { try { await fsOperation(parent).createDirectory(cd); } catch (e) { // If another concurrent task created it, consider it fine if (!(await fsOperation(newParent).exists())) throw e; } } else { try { await fsOperation(parent).createFile(cd); } catch (e) { if (!(await fsOperation(newParent).exists())) throw e; } } } if (parts.length) { await createFileRecursive(newParent, parts, wantDirEnd); } } /** * Sanitize zip entry path to ensure it's relative and safe under pluginDir * - Normalizes separators to '/' * - Strips leading slashes and Windows drive prefixes (e.g., C:/) * - Resolves '.' and '..' segments * - Preserves trailing slash for directory entries * @param {string} p * @param {boolean} isDir * @returns {string} sanitized relative path */ function sanitizeZipPath(p, isDir) { if (!p) return ""; let path = String(p); // Normalize separators path = path.replace(/\\/g, "/"); // Remove URL-like scheme if present accidentally path = path.replace(/^[a-zA-Z]+:\/\//, ""); // Strip leading slashes path = path.replace(/^\/+/, ""); // Strip Windows drive letter, e.g., C:/ path = path.replace(/^[A-Za-z]:\//, ""); const parts = path.split("/"); const stack = []; for (const part of parts) { if (!part || part === ".") continue; if (part === "..") { if (stack.length) stack.pop(); continue; } stack.push(part); } let safe = stack.join("/"); if (isDir && safe && !safe.endsWith("/")) safe += "/"; return safe; } /** * Detects unsafe absolute paths in zip entries that should be ignored. * Treats leading '/' as absolute, Windows drive roots like 'C:/' as absolute, * and common Android/Linux device roots like '/data', '/root', '/system'. * @param {string} p */ function isUnsafeAbsolutePath(p) { if (!p) return false; const s = String(p); if (/^[A-Za-z]:[\\\/]/.test(s)) return true; // Windows drive root if (s.startsWith("//")) return true; // network path if (s.startsWith("/")) { return ( s.startsWith("/data") || s.startsWith("/system") || s.startsWith("/vendor") || s.startsWith("/storage") || s.startsWith("/sdcard") || s.startsWith("/root") || true // any leading slash is unsafe ); } return false; } /** * Resolves Dependencies Manifest with given ids. * @param {string[]} deps dependencies */ async function resolveDepsManifest(deps) { const resolved = []; for (const dependency of deps) { const remoteDependency = await fsOperation( constants.API_BASE, `plugin/${dependency}`, ) .readFile("json") .catch(() => null); if (!remoteDependency) throw new Error(`Unknown plugin dependency: ${dependency}`); const version = await getInstalledPluginVersion(remoteDependency.id); if (remoteDependency?.version === version) continue; if (remoteDependency.dependencies) { const manifests = await resolveDepsManifest( remoteDependency.dependencies, ); resolved.push(manifests); } resolved.push(remoteDependency); } /** * * @param {string} id * @returns {Promise} plugin version */ async function getInstalledPluginVersion(id) { if (await fsOperation(PLUGIN_DIR, id).exists()) { const plugin = await fsOperation(PLUGIN_DIR, id, "plugin.json").readFile( "json", ); return plugin.version; } } return resolved; } /** Resolve dependency * @param {object} manifest * @returns {Promise} has error */ async function resolveDep(manifest) { let purchaseToken; let product; let isPaid = false; isPaid = manifest.price > 0; [product] = await helpers.promisify(iap.getProducts, [manifest.sku]); if (product) { const purchase = await getPurchase(product.productId); purchaseToken = purchase?.purchaseToken; } if (isPaid && !purchaseToken) { if (!product) throw new Error("Product not found"); const apiStatus = await helpers.checkAPIStatus(); if (!apiStatus) { alert(strings.error, strings.api_error); return true; } iap.setPurchaseUpdatedListener(...purchaseListener(onpurchase, onerror)); loaderDialog.setMessage(strings["loading..."]); await helpers.promisify(iap.purchase, product.productId); async function onpurchase(e) { const purchase = await getPurchase(product.productId); await ajax.post(Url.join(constants.API_BASE, "plugin/order"), { data: { id: manifest.id, token: purchase?.purchaseToken, package: BuildInfo.packageName, }, }); purchaseToken = purchase?.purchaseToken; } async function onerror(error) { throw error; } } loaderDialog.setMessage( `${strings.installing.replace("...", "")} ${manifest.name}...`, ); await installPlugin(manifest.id, undefined, purchaseToken, true); async function getPurchase(sku) { const purchases = await helpers.promisify(iap.getPurchases); const purchase = purchases.find((p) => p.productIds.includes(sku)); return purchase; } } /** * * @param {string} dir * @param {Array} files */ async function listFileRecursive(dir, files) { for (const child of await fsOperation(dir).lsDir()) { const fileUrl = Url.join(dir, child.name); if (child.isDirectory) { await listFileRecursive(fileUrl, files); } else { files.push(fileUrl); } } } /** * * @param {Record} files */ async function deleteRedundantFiles(pluginDir, state) { /** @type {string[]} */ let files = []; await listFileRecursive(pluginDir, files); for (const file of files) { if (!state.exists(file.replace(`${pluginDir}/`, ""))) { fsOperation(file).delete(); } } } ================================================ FILE: src/lib/installState.js ================================================ import fsOperation from "fileSystem"; import Url from "utils/Url"; const INSTALL_STATE_STORAGE = Url.join(DATA_STORAGE, ".install-state"); export default class InstallState { /** @type Record */ store; /** @type Record */ updatedStore; /** * * @param {string} id * @returns */ static async new(id) { try { const state = new InstallState(); state.id = await checksumText(id); state.updatedStore = {}; if (!(await fsOperation(INSTALL_STATE_STORAGE).exists())) { await fsOperation(DATA_STORAGE).createDirectory(".install-state"); } state.storeUrl = Url.join(INSTALL_STATE_STORAGE, state.id); if (await fsOperation(state.storeUrl).exists()) { let raw = "{}"; try { raw = await fsOperation(state.storeUrl).readFile("utf-8"); state.store = JSON.parse(raw); } catch (err) { console.error( "InstallState: Failed to parse state file, deleting:", err, ); // Delete corrupted state file to avoid parse errors such as 'Unexpected end of JSON' state.store = {}; try { await fsOperation(state.storeUrl).delete(); // Recreate a fresh empty file to keep invariant await fsOperation(INSTALL_STATE_STORAGE).createFile(state.id); } catch (writeErr) { console.error( "InstallState: Failed to recreate state file:", writeErr, ); } } const patchedStore = {}; for (const [key, value] of Object.entries(state.store)) { patchedStore[key.toLowerCase()] = value; } state.store = patchedStore; } else { state.store = {}; await fsOperation(INSTALL_STATE_STORAGE).createFile(state.id); } return state; } catch (e) { console.error(e); } } /** * * @param {string} url * @param {ArrayBuffer | string} content * @param {boolean} isString * @returns */ async isUpdated(url, content) { url = url.toLowerCase(); const current = this.store[url]; const update = typeof content === "string" ? await checksumText(content) : await checksum(content); this.updatedStore[url] = update; if (current === update) { return false; } else { return true; } } /** * * @param {string} url * @returns */ exists(url) { if (typeof this.store[url.toLowerCase()] !== "undefined") { return true; } else { return false; } } async save() { this.store = this.updatedStore; await fsOperation(this.storeUrl).writeFile( JSON.stringify(this.updatedStore), ); } async delete(url) { url = url.toLowerCase(); if (await fsOperation(url).exists()) { await fsOperation(url).delete(); } } async clear() { try { this.store = {}; this.updatedStore = {}; // Delete the state file entirely to avoid corrupted/partial JSON issues if (await fsOperation(this.storeUrl).exists()) { try { await fsOperation(this.storeUrl).delete(); } catch (delErr) { console.error( "InstallState: Failed to delete state file during clear:", delErr, ); // As a fallback, overwrite with a valid empty JSON await fsOperation(this.storeUrl).writeFile("{}"); } } } catch (error) { console.error("Failed to clear install state:", error); } } } /** * Derives the checksum of a Buffer * @param {BufferSource} data * @returns the derived checksum */ async function checksum(data) { const hashBuffer = await window.crypto.subtle.digest("SHA-256", data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); return hashHex; } /** * * @param {string} text * @returns */ async function checksumText(text) { return new Promise((resolve, reject) => { cordova.exec( (hash) => resolve(hash), (error) => reject(error), "System", "checksumText", [text], ); }); } ================================================ FILE: src/lib/keyBindings.js ================================================ import * as cmCommands from "@codemirror/commands"; import { defaultKeymap, emacsStyleKeymap, historyKeymap, indentWithTab, standardKeymap, } from "@codemirror/commands"; const MODIFIER_ORDER = ["Ctrl", "Alt", "Shift", "Cmd"]; const KEYMAP_SOURCES = [ ...standardKeymap, ...defaultKeymap, ...historyKeymap, ...emacsStyleKeymap, indentWithTab, ]; const APP_BINDING_CONFIG = [ { name: "focusEditor", description: "Focus editor", key: "Ctrl-1", readOnly: false, }, { name: "findFile", description: "Find a file", key: "Ctrl-P", action: "find-file", }, { name: "closeCurrentTab", description: "Close current tab.", key: "Ctrl-Q", action: "close-current-tab", readOnly: false, }, { name: "closeAllTabs", description: "Close all tabs.", key: "Ctrl-Shift-Q", action: "close-all-tabs", readOnly: false, }, { name: "newFile", description: "Create new file", key: "Ctrl-N", action: "new-file", readOnly: true, }, { name: "openFile", description: "Open a file", key: "Ctrl-O", action: "open-file", readOnly: true, }, { name: "openFolder", description: "Open a folder", key: "Ctrl-Shift-O", action: "open-folder", readOnly: true, }, { name: "saveFile", description: "Save current file", key: "Ctrl-S", action: "save", readOnly: true, editorOnly: true, }, { name: "saveFileAs", description: "Save as current file", key: "Ctrl-Shift-S", action: "save-as", readOnly: true, editorOnly: true, }, { name: "saveAllChanges", description: "Save all changes", key: null, action: "save-all-changes", readOnly: true, }, { name: "nextFile", description: "Open next file tab", key: "Ctrl-Tab", action: "next-file", readOnly: true, }, { name: "prevFile", description: "Open previous file tab", key: "Ctrl-Shift-Tab", action: "prev-file", readOnly: true, }, { name: "showSettingsMenu", description: "Show settings menu", key: "Ctrl-,", readOnly: false, }, { name: "renameFile", description: "Rename current file", key: "F2", action: "rename", readOnly: true, editorOnly: true, }, { name: "run", description: "Run current file", key: "F5", action: "run", readOnly: false, editorOnly: true, }, { name: "openInAppBrowser", description: "Open in-app browser", key: null, readOnly: true, }, { name: "toggleFullscreen", description: "Toggle full screen mode", key: "F11", action: "toggle-fullscreen", readOnly: false, }, { name: "toggleSidebar", description: "Toggle sidebar", key: "Ctrl-B", action: "toggle-sidebar", readOnly: true, }, { name: "toggleMenu", description: "Toggle menu", key: "F3", action: "toggle-menu", readOnly: true, }, { name: "toggleEditMenu", description: "Toggle edit menu", key: "F4", action: "toggle-editmenu", readOnly: true, }, { name: "selectall", description: "Select all", key: "Ctrl-A", readOnly: true, editorOnly: true, }, { name: "gotoline", description: "Go to line", key: "Ctrl-G", readOnly: true, editorOnly: true, }, { name: "find", description: "Find", key: "Ctrl-F", readOnly: true, editorOnly: true, }, { name: "copy", description: "Copy", key: "Ctrl-C", readOnly: true, editorOnly: true, }, { name: "cut", description: "Cut", key: "Ctrl-X", readOnly: false, editorOnly: true, }, { name: "paste", description: "Paste", key: "Ctrl-V", readOnly: false, editorOnly: true, }, { name: "problems", description: "Show problems", key: null, readOnly: true, editorOnly: true, }, { name: "replace", description: "Replace", key: "Ctrl-R", readOnly: false, editorOnly: true, }, { name: "openCommandPalette", description: "Open command palette", key: "Ctrl-Shift-P", readOnly: true, }, { name: "modeSelect", description: "Change language mode", key: "Ctrl-M", readOnly: false, editorOnly: true, }, { name: "toggleQuickTools", description: "Toggle quick tools", key: null, readOnly: true, }, { name: "selectWord", description: "Select current word", key: "Ctrl-D", action: "select-word", readOnly: false, editorOnly: true, }, { name: "openLogFile", description: "Open log file", key: null, action: "open-log-file", readOnly: true, }, { name: "openPluginsPage", description: "Open plugins page", key: null, readOnly: true, }, { name: "openFileExplorer", description: "Open file explorer", key: null, readOnly: true, }, { name: "copyDeviceInfo", description: "Copy device info", key: null, action: "copy-device-info", readOnly: true, }, { name: "changeAppTheme", description: "Change app theme", key: null, action: "change-app-theme", readOnly: true, }, { name: "changeEditorTheme", description: "Change editor theme", key: null, action: "change-editor-theme", readOnly: true, }, { name: "openTerminal", description: "Open terminal", key: "Ctrl-`", action: "new-terminal", readOnly: true, }, { name: "documentSymbols", description: "Go to symbol in document", key: null, readOnly: true, editorOnly: true, }, { name: "duplicateSelection", description: "Duplicate selection", key: "Ctrl-Shift-D", readOnly: false, editorOnly: true, }, { name: "copylinesdown", description: "Copy lines down", key: "Alt-Shift-Down", readOnly: false, editorOnly: true, }, { name: "copylinesup", description: "Copy lines up", key: "Alt-Shift-Up", readOnly: false, editorOnly: true, }, { name: "movelinesdown", description: "Move lines down", key: "Alt-Down", readOnly: false, editorOnly: true, }, { name: "movelinesup", description: "Move lines up", key: "Alt-Up", readOnly: false, editorOnly: true, }, { name: "removeline", description: "Remove line", key: null, readOnly: false, editorOnly: true, }, { name: "insertlineafter", description: "Insert line after", key: null, readOnly: false, editorOnly: true, }, { name: "selectline", description: "Select line", key: null, readOnly: true, editorOnly: true, }, { name: "selectlinesdown", description: "Select lines down", key: null, readOnly: true, editorOnly: true, }, { name: "selectlinesup", description: "Select lines up", key: null, readOnly: true, editorOnly: true, }, { name: "selectlinestart", description: "Select line start", key: "Shift-Home", readOnly: true, editorOnly: true, }, { name: "selectlineend", description: "Select line end", key: "Shift-End", readOnly: true, editorOnly: true, }, { name: "indent", description: "Indent", key: "Tab", readOnly: false, editorOnly: true, }, { name: "outdent", description: "Outdent", key: "Shift-Tab", readOnly: false, editorOnly: true, }, { name: "indentselection", description: "Indent selection", key: null, readOnly: false, editorOnly: true, }, { name: "newline", description: "Insert newline", key: null, readOnly: false, editorOnly: true, }, { name: "joinlines", description: "Join lines", key: null, readOnly: false, editorOnly: true, }, { name: "deletetolinestart", description: "Delete to line start", key: null, readOnly: false, editorOnly: true, }, { name: "deletetolineend", description: "Delete to line end", key: null, readOnly: false, editorOnly: true, }, { name: "togglecomment", description: "Toggle comment", key: "Ctrl-/", readOnly: false, editorOnly: true, }, { name: "comment", description: "Add line comment", key: null, readOnly: false, editorOnly: true, }, { name: "uncomment", description: "Remove line comment", key: null, readOnly: false, editorOnly: true, }, { name: "toggleBlockComment", description: "Toggle block comment", key: "Ctrl-Shift-/", readOnly: false, editorOnly: true, }, { name: "undo", description: "Undo", key: "Ctrl-Z", readOnly: false, editorOnly: true, }, { name: "redo", description: "Redo", key: "Ctrl-Shift-Z|Ctrl-Y", readOnly: false, editorOnly: true, }, { name: "simplifySelection", description: "Simplify selection", key: null, readOnly: true, editorOnly: true, }, ]; const APP_KEY_BINDINGS = buildAppBindings(APP_BINDING_CONFIG); const APP_CUSTOM_COMMANDS = new Set( APP_BINDING_CONFIG.filter((config) => !config.action).map( (config) => config.name, ), ); const FORCE_READ_ONLY = new Set([ "toggleTabFocusMode", "temporarilySetTabFocusMode", ]); const MUTATING_COMMAND_PATTERN = /^(delete|insert|indent|move|copy|split|transpose|toggle|undo|redo|line|block)/i; const CODEMIRROR_COMMAND_NAMES = new Set( Object.entries(cmCommands) .filter(([, value]) => typeof value === "function") .map(([name]) => name), ); const CODEMIRROR_KEY_BINDINGS = buildCodemirrorKeyBindings(APP_KEY_BINDINGS); const keyBindings = Object.fromEntries( Object.entries({ ...CODEMIRROR_KEY_BINDINGS, ...APP_KEY_BINDINGS }) .filter( ([name, binding]) => binding && (binding.action || APP_CUSTOM_COMMANDS.has(name) || CODEMIRROR_COMMAND_NAMES.has(name)), ) .sort((a, b) => a[0].localeCompare(b[0])), ); export default keyBindings; function buildAppBindings(configs) { return Object.fromEntries( configs.map( ({ name, description, key = null, action, readOnly = true, editorOnly, }) => [ name, { description: description ?? humanizeCommandName(name), key, readOnly, ...(editorOnly !== undefined ? { editorOnly } : {}), ...(action ? { action } : {}), }, ], ), ); } function buildCodemirrorKeyBindings(appBindings) { const commandEntries = Object.entries(cmCommands).filter( ([, value]) => typeof value === "function", ); const commandNameByFunction = new Map( commandEntries.map(([name, fn]) => [fn, name]), ); const comboMap = new Map(); for (const binding of KEYMAP_SOURCES) { const baseCombos = new Set(); pushCommandCombo(binding.run, binding.key, "win", baseCombos); pushCommandCombo(binding.run, binding.win, "win", baseCombos); pushCommandCombo(binding.run, binding.linux, "win", baseCombos); pushCommandCombo(binding.run, binding.mac, "mac", baseCombos); if (binding.shift) { const shiftName = commandNameByFunction.get(binding.shift); if (shiftName && !appBindings[shiftName]) { const combos = baseCombos.size ? Array.from(baseCombos) : [ normalizeKey(binding.key, "win"), normalizeKey(binding.win, "win"), normalizeKey(binding.linux, "win"), normalizeKey(binding.mac, "mac"), ].filter(Boolean); for (const combo of combos) { addCommandCombo(comboMap, shiftName, ensureModifier(combo, "Shift")); } } } } const result = {}; for (const [name, combos] of comboMap.entries()) { if (!combos.size || appBindings[name]) continue; result[name] = { description: humanizeCommandName(name), key: Array.from(combos) .sort((a, b) => a.localeCompare(b)) .join("|"), readOnly: inferReadOnly(name), editorOnly: true, }; } return result; function pushCommandCombo(commandFn, key, platform, baseCombos) { if (!commandFn) return; const name = commandNameByFunction.get(commandFn); if (!name || appBindings[name]) return; const normalized = normalizeKey(key, platform); if (!normalized) return; addCommandCombo(comboMap, name, normalized); baseCombos.add(normalized); } } function addCommandCombo(map, name, combo) { if (!combo) return; let entry = map.get(name); if (!entry) { entry = new Set(); map.set(name, entry); } entry.add(combo); } function normalizeKey(key, platform = "win") { if (!key) return null; const replaced = key.replace(/Mod/g, platform === "mac" ? "Cmd" : "Ctrl"); const { modifiers, baseKey } = parseKeyParts(replaced); if (!baseKey) return [...modifiers].join("-") || null; const ordered = MODIFIER_ORDER.filter((mod) => modifiers.has(mod)); return [...ordered, baseKey].join("-"); } function ensureModifier(combo, modifier) { if (!combo) return null; const { modifiers, baseKey } = parseKeyParts(combo); if (!baseKey) return combo; modifiers.add(modifier); const ordered = MODIFIER_ORDER.filter((mod) => modifiers.has(mod)); return [...ordered, baseKey].join("-"); } function parseKeyParts(combo) { const modifiers = new Set(); let baseKey = ""; if (!combo) return { modifiers, baseKey }; for (const rawPart of combo.split("-")) { const part = rawPart.trim(); if (!part) continue; const normalized = part.charAt(0).toUpperCase() + part.slice(1); if (MODIFIER_ORDER.includes(normalized)) { modifiers.add(normalized); } else { baseKey = part; } } return { modifiers, baseKey }; } function humanizeCommandName(name) { return name .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/_/g, " ") .replace(/^./, (char) => char.toUpperCase()); } function inferReadOnly(name) { if (FORCE_READ_ONLY.has(name)) return true; return !MUTATING_COMMAND_PATTERN.test(name); } ================================================ FILE: src/lib/lang.js ================================================ const langMap = { "en-us": { name: "English", async strings() { return await import("../lang/en-us.json"); }, }, "es-sv": { name: "Español", async strings() { return await import("../lang/es-sv.json"); }, }, "fr-fr": { name: "Francais", async strings() { return await import("../lang/fr-fr.json"); }, }, "tl-ph": { name: "Tagalog", async strings() { return await import("../lang/tl-ph.json"); }, }, "de-de": { name: "Deutsch", async strings() { return await import("../lang/de-de.json"); }, }, "id-id": { name: "Indonesian", async strings() { return await import("../lang/id-id.json"); }, }, "uz-uz": { name: "O'zbekcha", async strings() { return await import("../lang/uz-uz.json"); }, }, "ru-ru": { name: "Русский", async strings() { return await import("../lang/ru-ru.json"); }, }, "pl-pl": { name: "Polski", async strings() { return await import("../lang/pl-pl.json"); }, }, "pt-br": { name: "Português", async strings() { return await import("../lang/pt-br.json"); }, }, "pu-in": { name: "ਪੰਜਾਬੀ", async strings() { return await import("../lang/pu-in.json"); }, }, "tr-tr": { name: "Türkçe", async strings() { return await import("../lang/tr-tr.json"); }, }, "uk-ua": { name: "Українська", async strings() { return await import("../lang/uk-ua.json"); }, }, "hi-in": { name: "हिंदी", async strings() { return await import("../lang/hi-in.json"); }, }, "zh-cn": { name: "中文简体", async strings() { return await import("../lang/zh-cn.json"); }, }, "zh-hant": { name: "繁體中文", async strings() { return await import("../lang/zh-hant.json"); }, }, "zh-tw": { name: "繁體中文 (台灣)", async strings() { return await import("../lang/zh-tw.json"); }, }, "ir-fa": { name: "فارسی", async strings() { return await import("../lang/ir-fa.json"); }, }, "ar-ye": { name: "العربية", async strings() { return await import("../lang/ar-ye.json"); }, }, "ja-jp": { name: "日本語", async strings() { return await import("../lang/ja-jp.json"); }, }, "bn-bd": { name: "বাংলা", async strings() { return await import("../lang/bn-bd.json"); }, }, "cs-cz": { name: "Čeština", async strings() { return await import("../lang/cs-cz.json"); }, }, "vi-vn": { name: "Tiếng Việt", async strings() { return await import("../lang/vi-vn.json"); }, }, "be-by": { name: "Беларуская", async strings() { return await import("../lang/be-by.json"); }, }, "hu-hu": { name: "Magyar", async strings() { return await import("../lang/hu-hu.json"); }, }, "ml-in": { name: "മലയാളം", async strings() { return await import("../lang/ml-in.json"); }, }, "mm-unicode": { name: "ဗမာစာ(Unicode)", async strings() { return await import("../lang/mm-unicode.json"); }, }, "mm-zawgyi": { name: "ဗမာစာ(Zawgyi)", async strings() { return await import("../lang/mm-zawgyi.json"); }, }, "ko-kr": { name: "한국어", async strings() { return await import("../lang/ko-kr.json"); }, }, "it-it": { name: "Italiano", async strings() { return await import("../lang/it-it.json"); }, }, "he-il": { name: "Hebrew", async strings() { return await import("../lang/he-il.json"); }, }, }; export default { async set(code) { code = code?.toLowerCase(); const lang = langMap[code] || langMap["en-us"]; const strings = await lang.strings(); window.strings = strings.default; }, list: Object.keys(langMap).map((code) => [code, langMap[code].name]), getName(code) { code = code?.toLowerCase(); code = code in langMap ? code : "en-us"; return langMap[code].name; }, }; ================================================ FILE: src/lib/loadPlugin.js ================================================ import fsOperation from "fileSystem"; import Page from "components/page"; import helpers from "utils/helpers"; import Url from "utils/Url"; import actionStack from "./actionStack"; export default async function loadPlugin(pluginId, justInstalled = false) { const baseUrl = await helpers.toInternalUri(Url.join(PLUGIN_DIR, pluginId)); const cacheFile = Url.join(CACHE_STORAGE, pluginId); // Unmount the old version before loading the new one. // This MUST be done here by the framework, not by the new plugin code itself, // because once the new script loads, it calls acode.setPluginUnmount(id, newDestroy) // which overwrites the old version's destroy callback. At that point the old // destroy — which holds references to the old sidebar app, commands, event // listeners, etc. — is lost and can never be called. Letting the framework // invoke unmountPlugin() first ensures the OLD destroy() runs while it still // exists, so all old-version resources are properly cleaned up. try { acode.unmountPlugin(pluginId); } catch (e) { // unmountPlugin() itself is safe when no callback is registered (it no-ops), // but a plugin's destroy() callback may throw. We catch here so a faulty // cleanup in the old version does not block reloading the new one. console.error(`Error while unmounting plugin "${pluginId}":`, e); } // Remove the old ); $script.onerror = (error) => { reject( new Error( `Failed to load script for plugin ${pluginId}: ${error.message || error}`, ), ); }; $script.onload = async () => { const $page = Page("Plugin"); $page.show = () => { actionStack.push({ id: pluginId, action: $page.hide, }); app.append($page); }; $page.onhide = function () { actionStack.remove(pluginId); }; try { if (!(await fsOperation(cacheFile).exists())) { await fsOperation(CACHE_STORAGE).createFile(pluginId); } await acode.initPlugin(pluginId, baseUrl, $page, { cacheFileUrl: await helpers.toInternalUri(cacheFile), cacheFile: fsOperation(cacheFile), firstInit: justInstalled, ctx: await PluginContext.generate( pluginId, JSON.stringify(pluginJson), ), }); resolve(); } catch (error) { reject(error); } }; document.head.append($script); }); } ================================================ FILE: src/lib/loadPlugins.js ================================================ import fsOperation from "../fileSystem"; import Url from "../utils/Url"; import loadPlugin from "./loadPlugin"; import settings from "./settings"; // theme-related keywords for determining theme plugins const THEME_IDENTIFIERS = new Set([ "theme", "catppuccin", "pine", "githubdark", "radiant", "rdtheme", "ayumirage", "dust", "synthwave", "dragon", "mint", "monokai", "lumina_code", "sweet", "moonlight", "bluloco", "acode.plugin.extra_syntax_highlights", "documentsviewer", ]); export const onPluginLoadCallback = Symbol("onPluginLoadCallback"); export const onPluginsLoadCompleteCallback = Symbol( "onPluginsLoadCompleteCallback", ); export const LOADED_PLUGINS = new Set(); export const BROKEN_PLUGINS = new Map(); export default async function loadPlugins(loadOnlyTheme = false) { const plugins = await fsOperation(PLUGIN_DIR).lsDir(); const results = []; const failedPlugins = []; if (plugins.length > 0) { toast(strings["loading plugins"]); } let pluginsToLoad = []; const currentTheme = settings.value.appTheme; const enabledMap = settings.value.pluginsDisabled || {}; if (loadOnlyTheme) { // Only load theme plugins matching current theme pluginsToLoad = plugins.filter((pluginDir) => { const pluginId = Url.basename(pluginDir.url); // Skip already loaded and plugins that were previously marked broken return ( isThemePlugin(pluginId) && !LOADED_PLUGINS.has(pluginId) && !BROKEN_PLUGINS.has(pluginId) ); }); } else { // Load non-theme plugins that aren't loaded yet and are enabled pluginsToLoad = plugins.filter((pluginDir) => { const pluginId = Url.basename(pluginDir.url); // Skip theme plugins, already loaded, disabled or previously marked broken return ( !isThemePlugin(pluginId) && !LOADED_PLUGINS.has(pluginId) && enabledMap[pluginId] !== true && !BROKEN_PLUGINS.has(pluginId) ); }); } // Load plugins concurrently const LOAD_TIMEOUT = 15000; // ms per plugin const loadPromises = pluginsToLoad.map(async (pluginDir) => { const pluginId = Url.basename(pluginDir.url); if (loadOnlyTheme && currentTheme) { const pluginIdLower = pluginId.toLowerCase(); const currentThemeLower = currentTheme.toLowerCase(); const matchFound = pluginIdLower.includes(currentThemeLower); // Skip if: // 1. No match found with current theme AND // 2. It's not a theme plugin at all if (!matchFound && !isThemePlugin(pluginId)) { return; } } try { // ensure loadPlugin doesn't hang: timeout wrapper await Promise.race([ loadPlugin(pluginId), new Promise((_, rej) => setTimeout(() => rej(new Error("Plugin load timeout")), LOAD_TIMEOUT), ), ]); LOADED_PLUGINS.add(pluginId); acode[onPluginLoadCallback](pluginId); results.push(true); // clear broken mark if present if (BROKEN_PLUGINS.has(pluginId)) { BROKEN_PLUGINS.delete(pluginId); } } catch (error) { console.error(`Error loading plugin ${pluginId}:`, error); // mark plugin as broken to avoid repeated attempts until user intervenes BROKEN_PLUGINS.set(pluginId, { error: String(error.message || error), timestamp: Date.now(), }); failedPlugins.push(pluginId); results.push(false); } }); await Promise.allSettled(loadPromises); acode[onPluginsLoadCompleteCallback](); if (failedPlugins.length > 0) { setTimeout(() => { cleanupFailedPlugins(failedPlugins).catch((error) => { console.error("Failed to cleanup plugins:", error); }); }, 1000); } return results.filter(Boolean).length; } function isThemePlugin(pluginId) { // Convert to lowercase for case-insensitive matching const id = pluginId.toLowerCase(); // Check if any theme identifier is present in the plugin ID return Array.from(THEME_IDENTIFIERS).some((theme) => id.includes(theme)); } async function cleanupFailedPlugins(pluginIds) { for (const pluginId of pluginIds) { try { const pluginDir = Url.join(PLUGIN_DIR, pluginId); if (await fsOperation(pluginDir).exists()) { await fsOperation(pluginDir).delete(); } } catch (error) { console.error(`Failed to cleanup plugin ${pluginId}:`, error); } } } ================================================ FILE: src/lib/logger.js ================================================ import fsOperation from "fileSystem"; import Url from "utils/Url"; import constants from "./constants"; /* /** * Logger class for handling application logging with buffer and file output. * @class */ /** * Creates a new Logger instance. * @constructor * @param {number} [maxBufferSize=1000] - Maximum number of log entries to keep in buffer. * @param {string} [logLevel="info"] - Minimum log level to record ("error", "warn", "info", "debug"). * @param {number} [flushInterval=30000] - Interval in milliseconds for automatic log flushing. */ class Logger { #logBuffer; #maxBufferSize; #logLevel; #logFileName; #flushInterval; #autoFlushInterval; #maxFileSize; constructor( maxBufferSize = 1000, logLevel = "info", flushInterval = 30000, maxFileSize = 10 * 1024 * 1024, ) { this.#logBuffer = new Map(); this.#maxBufferSize = maxBufferSize; this.#logLevel = logLevel; this.#logFileName = constants.LOG_FILE_NAME; this.#flushInterval = flushInterval; this.#maxFileSize = maxFileSize; this.#startAutoFlush(); // Automatically flush logs at intervals this.#setupAppLifecycleHandlers(); // Handle app lifecycle events for safe log saving } /** * Logs a message with the specified log level. * @param {'error' | 'warn' | 'info' | 'debug'} level - The log level. * @param {string} message - The message to be logged. */ log(level, message) { const levels = ["error", "warn", "info", "debug"]; if (levels.indexOf(level) <= levels.indexOf(this.#logLevel)) { let logEntry; // Check if the message is an instance of Error if (message instanceof Error) { logEntry = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message.name}: ${message.message}\nStack trace: ${message.stack}`; } else { logEntry = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`; } // LRU Mechanism for efficient log buffer management if (this.#logBuffer.size >= this.#maxBufferSize) { // Remove oldest entry const oldestKey = this.#logBuffer.keys().next().value; this.#logBuffer.delete(oldestKey); } this.#logBuffer.set(Date.now(), logEntry); // Using timestamp as key } } flushLogs() { if (this.#logBuffer.size > 0) { const logContent = Array.from(this.#logBuffer.values()).join("\n"); this.#writeLogToFile(logContent); this.#logBuffer.clear(); // Clear the buffer after flushing } } #writeLogToFile = async (logContent) => { try { const logFilePath = Url.join(DATA_STORAGE, constants.LOG_FILE_NAME); if (!(await fsOperation(logFilePath).exists())) { await fsOperation(window.DATA_STORAGE).createFile( constants.LOG_FILE_NAME, logContent, ); } else { let existingData = await fsOperation(logFilePath).readFile("utf8"); let newData = existingData + "\n" + logContent; // Check if the new data exceeds the maximum file size if (new Blob([newData]).size > this.#maxFileSize) { const lines = newData.split("\n"); while ( new Blob([lines.join("\n")]).size > this.#maxFileSize && lines.length > 0 ) { lines.shift(); } newData = lines.join("\n"); } await fsOperation(logFilePath).writeFile(newData); } } catch (error) { console.error( "Error in handling fs operation on log file. Error:", error, ); } }; #startAutoFlush = () => { this.#autoFlushInterval = setInterval(() => { this.flushLogs(); }, this.#flushInterval); }; stopAutoFlush() { clearInterval(this.#autoFlushInterval); } #setupAppLifecycleHandlers = () => { document.addEventListener( "pause", () => { this.flushLogs(); // Flush logs when app is paused (background) }, false, ); }; } export default Logger; ================================================ FILE: src/lib/notificationManager.js ================================================ import sidebarApps from "sidebarApps"; import DOMPurify from "dompurify"; // Singleton instance let instance = null; export default class NotificationManager { DEFAULT_ICON = ``; MAX_NOTIFICATIONS = 20; notifications = []; REFRESH_INTERVAL = 60000; // 1 minute refresh interval timeUpdateInterval = null; constructor() { if (instance) { return instance; } this.notifications = []; instance = this; } init() { document.body.appendChild(
          , ); this.renderNotifications(); this.startTimeUpdates(); sidebarApps .get("notification") ?.querySelector(".notifications-container") .addEventListener("click", this.handleClick.bind(this)); } startTimeUpdates() { if (this.timeUpdateInterval) { clearInterval(this.timeUpdateInterval); } this.timeUpdateInterval = setInterval(() => { this.updateNotificationTimes(); }, this.REFRESH_INTERVAL); } updateNotificationTimes() { const container = sidebarApps .get("notification") ?.querySelector(".notifications-container"); if (!container) return; container.querySelectorAll(".notification-time").forEach((timeElement) => { const notificationItem = timeElement.closest(".notification-item"); const id = notificationItem?.dataset.id; if (!id) return; const notification = this.notifications.find( (n) => n.id === Number.parseInt(id), ); if (notification) { timeElement.textContent = this.formatTime(notification.time); } }); } renderNotifications() { const container = sidebarApps .get("notification") ?.querySelector(".notifications-container"); if (!container) return; if (this.notifications.length === 0) { container.innerHTML = `
          ${strings["no_unread_notifications"]}
          `; return; } container.innerHTML = ""; this.notifications.forEach((notification) => { container.appendChild(this.createNotificationElement(notification)); }); } handleClick(e) { const dismissButton = e.target.closest(".action-button"); if (!dismissButton) return; e.stopPropagation(); const notificationElement = dismissButton.closest(".notification-item"); if (!notificationElement) return; const id = notificationElement.dataset.id; if (id) { const index = this.notifications.findIndex( (n) => n.id === Number.parseInt(id), ); if (index > -1) { notificationElement.remove(); this.notifications.splice(index, 1); this.renderNotifications(); } } } createNotificationElement(notification) { const element = (
          ); const safeIcon = this.sanitizeIcon(this.parseIcon(notification.icon)); const safeTitle = this.sanitizeText(notification.title); const safeMessage = this.sanitizeText(notification.message); element.innerHTML = `
          ${safeIcon}
          ${safeTitle} ${this.formatTime(notification.time)}
          ${safeMessage}
          Dismiss
          `; if (notification.action) { element.addEventListener("click", (e) => { if (e.target.closest(".action-button")) { return; } notification.action(notification); }); } return element; } createToastNotification(notification) { const element = (
          ); const safeIcon = this.sanitizeIcon(this.parseIcon(notification.icon)); const safeTitle = this.sanitizeText(notification.title); const safeMessage = this.sanitizeText(notification.message); element.innerHTML = `
          ${safeIcon}
          ${safeTitle}
          ${safeMessage}
          ${notification.autoClose ? "" : ``} `; const closeIcon = element.querySelector(".close-icon"); if (closeIcon) { closeIcon.addEventListener("click", (event) => { event.stopPropagation(); element.remove(); }); } if (notification.action) { element.addEventListener("click", () => notification.action(notification), ); } if (notification.autoClose) { setTimeout(() => { element.classList.add("hiding"); setTimeout(() => element.remove(), 300); }, 5000); } return element; } addNotification(notification) { this.notifications.unshift(notification); // Remove oldest if exceeding limit if (this.notifications.length > this.MAX_NOTIFICATIONS) { this.notifications.pop(); } this.renderNotifications(); // show toast notification document .querySelector(".notification-toast-container") ?.appendChild(this.createToastNotification(notification)); } pushNotification({ title, message, icon, autoClose = true, action = null, type = "info", }) { const notification = { id: Date.now(), title, message, icon, action, autoClose, type, time: new Date(), }; this.addNotification(notification); } parseIcon(icon) { if (typeof icon !== "string" || !icon) return this.DEFAULT_ICON; if (icon.startsWith("`; return ``; } sanitizeText(text) { return DOMPurify.sanitize(String(text ?? ""), { ALLOWED_TAGS: [], ALLOWED_ATTR: [], }); } sanitizeIcon(iconMarkup) { return DOMPurify.sanitize(iconMarkup, { USE_PROFILES: { html: true, svg: true }, ALLOW_DATA_ATTR: false, }); } formatTime(date) { const now = new Date(); const diff = Math.floor((now - date) / 1000); if (diff < 60) return "Just now"; if (diff < 3600) return `${Math.floor(diff / 60)}m`; if (diff < 86400) return `${Math.floor(diff / 3600)}h`; if (diff < 604800) return `${Math.floor(diff / 86400)}d`; return date.toLocaleDateString(); } clearAll() { this.notifications = []; this.renderNotifications(); if (this.timeUpdateInterval) { clearInterval(this.timeUpdateInterval); this.timeUpdateInterval = null; } } } ================================================ FILE: src/lib/openFile.js ================================================ import fsOperation from "fileSystem"; import AudioPlayer from "components/audioPlayer"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import { reopenWithNewEncoding } from "palettes/changeEncoding"; import { decode, detectEncoding } from "utils/encodings"; import helpers from "utils/helpers"; import EditorFile from "./editorFile"; import fileTypeHandler from "./fileTypeHandler"; import recents from "./recents"; import appSettings from "./settings"; /** * @typedef {object} FileOptions * @property {string} text * @property {{ row: number, column: number }} cursorPos * @property {boolean} render * @property {function} onsave * @property {string} encoding * @property {string} mode * @property {string} uri */ /** * Opens a editor file * @param {String & FileOptions} file * @param {FileOptions} options */ export default async function openFile(file, options = {}) { try { let uri = typeof file === "string" ? file : file.uri; if (!uri) return; /**@type {EditorFile} */ const existingFile = editorManager.getFile(uri, "uri"); const { cursorPos, render, onsave, text, mode, encoding } = options; if (existingFile) { // If file is already opened and new text is provided const existingText = existingFile.session.doc.toString() ?? ""; // If file is already opened existingFile.makeActive(); const { editor } = editorManager; if (onsave) { existingFile.onsave = onsave; } if (text && existingText !== text) { // let confirmation = true; // if (existingFile.isUnsaved) { // const message = strings['reopen file'].replace('{file}', existingFile.filename); // confirmation = await confirm(strings.warning, message); // } // if (confirmation) { // } editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: String(text), }, }); } // Move cursor if requested and different try { if (cursorPos) { const cur = editor.getCursorPosition(); if (cur.row !== cursorPos.row || cur.column !== cursorPos.column) { editor.gotoLine(cursorPos.row, cursorPos.column); } } } catch (error) { console.warn( `Failed to move cursor for ${existingFile.filename || existingFile.uri}`, error, ); } if (encoding && existingFile.encoding !== encoding) { reopenWithNewEncoding(encoding); } return; } loader.showTitleLoader(); const settings = appSettings.value; const fs = fsOperation(uri); const fileInfo = await fs.stat(); const name = fileInfo.name || file.filename || uri; const readOnly = fileInfo.canWrite ? false : true; const createEditor = (isUnsaved, text, detectedEncoding) => { new EditorFile(name, { uri, text, cursorPos, isUnsaved, render, onsave, readOnly, encoding: detectedEncoding || encoding, SAFMode: mode, }); }; // Check for registered file handlers const customHandler = fileTypeHandler.getFileHandler(name); if (customHandler) { try { await customHandler.handleFile({ name, uri, stats: fileInfo, readOnly, options: { cursorPos, render, onsave, encoding, mode, createEditor, }, }); return; } catch (error) { console.error(`File handler '${customHandler.id}' failed:`, error); // Continue with default handling if custom handler fails } } if (text) { // If file is not opened and has unsaved text createEditor(true, text); return; } const videoRegex = /\.(mp4|webm|ogg|mov|avi|wmv|flv|mkv|3gp)$/i; const imageRegex = /\.(jpe?g|png|gif|webp|bmp|ico|avif|apng|tiff?)$/i; const audioRegex = /\.(mp3|wav|ogg|m4a|aac|wma|flac|opus|3gp|mid|midi)$/i; if (videoRegex.test(name)) { const objectUrl = await fileToDataUrl(uri); const videoContainer = (
          ); const videoEl = ( ); videoContainer.append(videoEl); new EditorFile(name, { uri, type: "video", tabIcon: "file file_type_video", content: videoContainer, render: true, }); return; } if (imageRegex.test(name)) { const objectUrl = await fileToDataUrl(uri); const imageContainer = (
          ); const imgEl = ( ); let scale = 1; let startX = 0; let startY = 0; let translateX = 0; let translateY = 0; let lastX = 0; let lastY = 0; function getBoundaries() { const containerRect = imageContainer.getBoundingClientRect(); const imgRect = imgEl.getBoundingClientRect(); const maxX = (imgRect.width * scale - containerRect.width) / (2 * scale); const maxY = (imgRect.height * scale - containerRect.height) / (2 * scale); return { maxX: Math.max(0, maxX), maxY: Math.max(0, maxY), minX: -Math.max(0, maxX), minY: -Math.max(0, maxY), }; } function constrainTranslation() { const bounds = getBoundaries(); translateX = Math.min(Math.max(translateX, bounds.minX), bounds.maxX); translateY = Math.min(Math.max(translateY, bounds.minY), bounds.maxY); } // Zoom with mouse wheel imageContainer.addEventListener("wheel", (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; const oldScale = scale; scale = Math.max(0.1, Math.min(5, scale + delta)); // Adjust translation to zoom toward mouse position const rect = imgEl.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const scaleChange = scale / oldScale; translateX = mouseX - (mouseX - translateX) * scaleChange; translateY = mouseY - (mouseY - translateY) * scaleChange; constrainTranslation(); imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; }); // Pan image with mouse drag or touch imageContainer.addEventListener("mousedown", startDrag); imageContainer.addEventListener("touchstart", (e) => { if (e.touches.length === 1) { startDrag(e.touches[0]); } else if (e.touches.length === 2) { const touch1 = e.touches[0]; const touch2 = e.touches[1]; startX = Math.abs(touch1.clientX - touch2.clientX); startY = Math.abs(touch1.clientY - touch2.clientY); } }); function startDrag(e) { lastX = e.clientX; lastY = e.clientY; document.addEventListener("mousemove", onDrag); document.addEventListener("mouseup", stopDrag); document.addEventListener("touchmove", onTouchDrag); document.addEventListener("touchend", stopDrag); } function onDrag(e) { const deltaX = e.clientX - lastX; const deltaY = e.clientY - lastY; translateX += deltaX / scale; translateY += deltaY / scale; lastX = e.clientX; lastY = e.clientY; constrainTranslation(); imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; } function onTouchDrag(e) { if (e.touches.length === 1) { const touch = e.touches[0]; const deltaX = touch.clientX - lastX; const deltaY = touch.clientY - lastY; translateX += deltaX / scale; translateY += deltaY / scale; lastX = touch.clientX; lastY = touch.clientY; constrainTranslation(); imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; } else if (e.touches.length === 2) { e.preventDefault(); const touch1 = e.touches[0]; const touch2 = e.touches[1]; const currentX = Math.abs(touch1.clientX - touch2.clientX); const currentY = Math.abs(touch1.clientY - touch2.clientY); const startDist = Math.sqrt(startX * startX + startY * startY); const currentDist = Math.sqrt( currentX * currentX + currentY * currentY, ); const delta = (currentDist - startDist) / 100; scale = Math.max(0.1, Math.min(5, scale + delta)); constrainTranslation(); imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; startX = currentX; startY = currentY; } } function stopDrag() { document.removeEventListener("mousemove", onDrag); document.removeEventListener("mouseup", stopDrag); document.removeEventListener("touchmove", onTouchDrag); document.removeEventListener("touchend", stopDrag); } imageContainer.append(imgEl); new EditorFile(name, { uri, type: "image", tabIcon: "file file_type_image", content: imageContainer, render: true, }); return; } if (audioRegex.test(name)) { const objectUrl = await fileToDataUrl(uri); const audioContainer = (
          ); const audioPlayer = new AudioPlayer(audioContainer); audioPlayer.loadTrack(objectUrl); const audioTab = new EditorFile(name, { uri, type: "audio", tabIcon: "file file_type_audio", content: audioPlayer.container, render: true, }); audioTab.onclose = () => { audioPlayer.cleanup(); }; return; } // Else open a new file // Checks for valid file if (fileInfo.length * 0.000001 > settings.maxFileSize) { return alert( strings.error.toUpperCase(), strings["file too large"].replace( "{size}", settings.maxFileSize + "MB", ), ); } if (helpers.isBinary(uri)) { const confirmation = await confirm(strings.info, strings["binary file"]); if (!confirmation) return; } const binData = await fs.readFile(); // Determine encoding: if explicit provided use it, otherwise // if settings.defaultFileEncoding === 'auto' then detect; else use the default as-is let detectedEncoding = file.encoding || encoding; if (!detectedEncoding) { const defaultSetting = appSettings.value.defaultFileEncoding; if (defaultSetting === "auto") { try { detectedEncoding = await detectEncoding(binData); if (detectedEncoding === "auto") detectedEncoding = "UTF-8"; } catch (error) { console.warn("Encoding detection failed, using UTF-8:", error); detectedEncoding = "UTF-8"; } } else { detectedEncoding = defaultSetting || "UTF-8"; } } const fileContent = await decode(binData, detectedEncoding); createEditor(false, fileContent, detectedEncoding); if (mode !== "single") recents.addFile(uri); return; } catch (error) { console.error(error); } finally { loader.removeTitleLoader(); } } /** * Converts file to data url * @param {string} file file url */ async function fileToDataUrl(file) { const fs = fsOperation(file); const fileInfo = await fs.stat(); const binData = await fs.readFile(); return URL.createObjectURL(new Blob([binData], { type: fileInfo.mime })); } ================================================ FILE: src/lib/openFolder.js ================================================ import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; import collapsableList from "components/collapsableList"; import FileTree from "components/fileTree"; import Sidebar from "components/sidebar"; import { TerminalManager } from "components/terminal"; import tile from "components/tile"; import toast from "components/toast"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import escapeStringRegexp from "escape-string-regexp"; import FileBrowser from "pages/fileBrowser"; 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 * as FileList from "./fileList"; import openFile from "./openFile"; import recents from "./recents"; import appSettings from "./settings"; const isTermuxSafUri = (value = "") => value.startsWith("content://com.termux.documents/tree/"); const isAcodeTerminalPublicSafUri = (value = "") => value.startsWith("content://com.foxdebug.acode.documents/tree/"); const isTerminalSafUri = (value = "") => isTermuxSafUri(value) || isAcodeTerminalPublicSafUri(value); const getTerminalPaths = () => { const packageName = window.BuildInfo?.packageName || "com.foxdebug.acode"; const dataDir = `/data/user/0/${packageName}`; const alpineRoot = `${dataDir}/files/alpine`; const publicDir = `${dataDir}/files/public`; return { alpineRoot, publicDir, dataDir }; }; const isTerminalAccessiblePath = (url = "") => { if (isAcodeTerminalPublicSafUri(url)) return true; const { alpineRoot, publicDir } = getTerminalPaths(); const cleanUrl = url.replace(/^file:\/\//, ""); if (cleanUrl.startsWith(alpineRoot) || cleanUrl.startsWith(publicDir)) { return true; } return false; }; const convertToProotPath = (url = "") => { const { alpineRoot, publicDir } = getTerminalPaths(); if (isAcodeTerminalPublicSafUri(url)) { try { const { docId } = Uri.parse(url); const cleanDocId = /::/.test(url) ? decodeURIComponent(docId || "") : docId || ""; if (!cleanDocId) return "/public"; if (cleanDocId.startsWith(publicDir)) { return cleanDocId.replace(publicDir, "/public") || "/public"; } if (cleanDocId.startsWith("/public")) { return cleanDocId; } if (cleanDocId.startsWith("public:")) { const relativePath = cleanDocId.slice("public:".length); return relativePath ? Path.join("/public", relativePath) : "/public"; } const relativePath = cleanDocId .replace(/^\/+/, "") .replace(/^public\//, ""); return relativePath ? Path.join("/public", relativePath) : "/public"; } catch (error) { console.warn( `Failed to parse public SAF URI for terminal conversion: ${url}`, ); return "/public"; } } const cleanUrl = url.replace(/^file:\/\//, ""); if (cleanUrl.startsWith(publicDir)) { return cleanUrl.replace(publicDir, "/public"); } if (cleanUrl.startsWith(alpineRoot)) { return cleanUrl.replace(alpineRoot, "") || "/"; } console.warn(`Unrecognized path for terminal conversion: ${url}`); return cleanUrl; }; /** * @typedef {import('../components/collapsableList').Collapsible} Collapsible */ /** * @typedef {object} ClipBoard * @property {string} url * @property {HTMLElement} $el * @property {"cut"|"copy"} action */ /** * @typedef {object} Folder * @property {string} id * @property {string} url * @property {string} title * @property {boolean} listFiles Weather to list all files recursively * @property {boolean} saveState * @property {Collapsible} $node * @property {ClipBoard} clipBoard * @property {function(): void} remove * @property {function(): void} reload * @property {Map} listState */ /**@type {Folder[]} */ export const addedFolder = []; const ACODE_PLUGIN_MANIFEST_FILE = "plugin.json"; /** * Open a folder in the sidebar * @param {string} _path * @param {object} opts * @param {string} opts.name * @param {string} [opts.id] * @param {boolean} [opts.saveState] * @param {boolean} [opts.listFiles] * @param {Map} [opts.listState] */ function openFolder(_path, opts = {}) { if (addedFolder.find((folder) => folder.url === _path)) { return; } const saveState = opts.saveState ?? true; const listState = opts.listState || {}; const title = opts.name; let listFiles = opts.listFiles; if (!title) { throw new Error("Folder name is required"); } const $root = collapsableList(title, "folder", { allCaps: true, ontoggle: () => expandList($root), }); const $text = $root.$title.get(":scope>span.text"); $root.id = "r" + _path.hashCode(); $text.style.overflow = "hidden"; $text.style.whiteSpace = "nowrap"; $text.style.textOverflow = "ellipsis"; $root.$title.dataset.type = "root"; $root.$title.dataset.url = _path; $root.$title.dataset.name = title; $root.$ul.onclick = $root.$ul.oncontextmenu = $root.$title.onclick = $root.$title.oncontextmenu = handleItems; recents.addFolder(_path, opts); sidebarApps.get("files").append($root); const event = { url: _path, name: title, }; const folder = { title, remove, listFiles, saveState, listState, url: _path, $node: $root, id: opts.id, clipBoard: {}, reload() { $root.collapse(); $root.expand(); }, }; editorManager.emit("update", "add-folder"); editorManager.onupdate("add-folder", event); editorManager.emit("add-folder", event); (async () => { if (typeof listFiles !== "boolean") { const protocol = Url.getProtocol(_path).slice(0, -1); const type = /^(content|file)$/.test(protocol) ? "" : ` (${protocol})`; const message = strings["list files"].replace( "{name}", `${title}${type}`, ); listFiles = await confirm(strings.confirm, message, true); } if (listFiles) { FileList.addRoot({ url: _path, name: title }); } folder.listFiles = listFiles; addedFolder.push(folder); })(); if (listState[_path]) { $root.expand(); } function remove(e) { if (e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); } if ($root.parentElement) { $root.remove(); } const index = addedFolder.findIndex((folder) => folder.url === _path); if (index !== -1) addedFolder.splice(index, 1); editorManager.emit("update", "remove-folder"); editorManager.onupdate("remove-folder", event); editorManager.emit("remove-folder", event); } } /** * Expand the list * @param {Collapsible} $list */ async function expandList($list) { const { $ul, $title } = $list; const { url } = $title.dataset; const { saveState, listState, $node } = openFolder.find(url); const startLoading = () => $node.$title.classList.add("loading"); const stopLoading = () => $node.$title.classList.remove("loading"); if (!$ul) return; // Cleanup existing file tree if ($ul._fileTree) { $ul._fileTree.destroy(); $ul._fileTree = null; } $ul.innerHTML = ""; if (saveState) listState[url] = $list.unclasped; if (!$list.unclasped) return; try { startLoading(); const fileTree = new FileTree($ul, { getEntries: (dirUrl) => fsOperation(dirUrl).lsDir(), expandedState: listState, onExpandedChange: (folderUrl, isExpanded) => { if (saveState) listState[folderUrl] = isExpanded; }, onFileClick: (fileUrl) => { handleClick("file", fileUrl); }, onContextMenu: (type, itemUrl, name, $target) => { handleContextmenu(type, itemUrl, name, $target); }, }); await fileTree.load(url); $ul._fileTree = fileTree; } catch (err) { $list.collapse(); if (err?.includes?.("Invalid message length")) { console.error(err); toast("SFTP connection broken. Restart the app"); return; } helpers.error(err); } finally { stopLoading(); } } /** * Gets weather the folder is collapsed or not * @param {HTMLElement} $el * @param {boolean} isFile * @returns */ function collapsed($el, isFile) { if (!$el.isConnected) return true; $el = $el.parentElement; if (!isFile) { $el = $el.parentElement; } return $el.previousElementSibling.collapsed; } /** * Handle click event * @param {Event} e */ function handleItems(e) { const mode = e.type; const $target = e.target; if (!($target instanceof HTMLElement)) return; const type = $target.dataset.type; if (!type) return; const url = $target.dataset.url; const name = $target.dataset.name; if (mode === "click") { handleClick(type, url, name, $target); } else if (mode === "contextmenu") { handleContextmenu(type, url, name, $target); } } /** * Handle contextmenu * @param {"file"|"dir"|"root"} type * @param {string} url * @param {string} name * @param {HTMLElement} $target */ async function handleContextmenu(type, url, name, $target) { if (appSettings.value.vibrateOnTap) { navigator.vibrate(constants.VIBRATION_TIME); } const { clipBoard, $node } = openFolder.find(url); const cancel = `${strings.cancel}${clipBoard ? ` (${strings[clipBoard.action]})` : ""}`; const COPY = ["copy", strings.copy, "copy"]; const CUT = ["cut", strings.cut, "cut"]; const COPY_RELATIVE_PATH = [ "copy-relative-path", strings["copy relative path"], "attach_file", ]; const REMOVE = ["delete", strings.delete, "delete"]; const RENAME = ["rename", strings.rename, "edit"]; const PASTE = ["paste", strings.paste, "paste", !!clipBoard]; const NEW_FILE = ["new file", strings["new file"], "document-add"]; const NEW_FOLDER = ["new folder", strings["new folder"], "folder-add"]; const CANCEL = ["cancel", cancel, "clearclose"]; const OPEN_FOLDER = ["open-folder", strings["open folder"], "folder"]; const INSERT_FILE = ["insert-file", strings["insert file"], "file_copy"]; const CLOSE_FOLDER = ["close", strings["close"], "folder-remove"]; const INSTALL_PLUGIN = [ "install-plugin", strings["install as plugin"] || "Install as Plugin", "extension", ]; let options; if (helpers.isFile(type)) { options = [COPY, CUT, COPY_RELATIVE_PATH, RENAME, REMOVE]; if ( url.toLowerCase().endsWith(".zip") && (await fsOperation( Url.dirname(url) + ACODE_PLUGIN_MANIFEST_FILE, ).exists()) ) { options.push(INSTALL_PLUGIN); } } else if (helpers.isDir(type)) { options = [COPY, CUT, COPY_RELATIVE_PATH, REMOVE, RENAME]; if (clipBoard.url != null) { options.push(PASTE); } options.push(NEW_FILE, NEW_FOLDER, OPEN_FOLDER, INSERT_FILE); if (isTerminalAccessiblePath(url)) { const OPEN_IN_TERMINAL = [ "open-in-terminal", strings["open in terminal"] || "Open in Terminal", "terminal", ]; options.push(OPEN_IN_TERMINAL); } } else if (type === "root") { options = []; if (clipBoard.url != null) { options.push(PASTE); } options.push(NEW_FILE, NEW_FOLDER, INSERT_FILE); if (isTerminalAccessiblePath(url)) { const OPEN_IN_TERMINAL = [ "open-in-terminal", strings["open in terminal"] || "Open in Terminal", "terminal", ]; options.push(OPEN_IN_TERMINAL); } options.push(CLOSE_FOLDER); } if (clipBoard.action) options.push(CANCEL); try { const option = await select(name, options); await execOperation(type, option, url, $target, name); } catch (error) { console.error(error); helpers.error(error); } finally { $node.$title.classList.remove("loading"); } } /** * @param {"dir"|"file"|"root"} type * @param {"copy"|"cut"|"delete"|"rename"|"paste"|"new file"|"new folder"|"cancel"|"open-folder"|"install-plugin"} action * @param {string} url target url * @param {HTMLElement} $target target element * @param {string} name Name of file or folder */ function execOperation(type, action, url, $target, name) { const { clipBoard, $node, remove, url: rootUrl } = openFolder.find(url); const startLoading = () => $node.$title.classList.add("loading"); const stopLoading = () => $node.$title.classList.remove("loading"); switch (action) { case "copy": case "cut": return clipBoardAction(); case "delete": return deleteFile(); case "rename": return renameFile(); case "paste": return paste(); case "new file": case "new folder": return createNew(); case "cancel": return cancelAction(); case "open-folder": return open(); case "insert-file": return insertFile(); case "close": return remove(); case "install-plugin": return installPlugin(); case "open-in-terminal": return openInTerminal(); case "copy-relative-path": return copyRelativePath(); } async function installPlugin() { try { const manifest = JSON.parse( await fsOperation( Url.dirname(url) + ACODE_PLUGIN_MANIFEST_FILE, ).readFile("utf8"), ); const { default: installPlugin } = await import("lib/installPlugin"); await installPlugin(url, manifest.name); toast(strings["success"], 3000); } catch (error) { helpers.error(error); console.error(error); } } async function copyRelativePath() { try { // Validate inputs if (!url) { console.error("File path not available"); return; } if (!rootUrl) { console.error("Root folder not found"); return; } let relativePath; // Try using Url.pathname for protocol-based URLs const rootPath = Url.pathname(rootUrl); const targetPath = Url.pathname(url); if (rootPath && targetPath) { // Both pathnames extracted successfully relativePath = Path.convertToRelative(rootPath, targetPath); } else { // Fallback: Use simple string comparison for URIs where pathname extraction fails const cleanRoot = rootUrl.endsWith("/") ? rootUrl.slice(0, -1) : rootUrl; const cleanTarget = url.endsWith("/") ? url.slice(0, -1) : url; // Check if target URL starts with root URL if (cleanTarget.startsWith(cleanRoot)) { relativePath = cleanTarget.slice(cleanRoot.length + 1); } else { // If not a child path, just use basename relativePath = Url.basename(url); } } if (!relativePath) { console.error("Unable to calculate relative path"); return; } if (cordova.plugins.clipboard) { cordova.plugins.clipboard.copy(relativePath); toast(strings.success || "Relative path copied to clipboard"); } else { console.error("Clipboard not available"); toast("Clipboard not available"); } } catch (error) { console.error("Failed to copy relative path:", error); } } async function openInTerminal() { try { const prootPath = convertToProotPath(url); const terminal = await TerminalManager.createTerminal({ name: `Terminal - ${name}`, render: true, }); if (terminal?.component) { const waitForConnection = (timeoutMs = 5000) => new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { if (terminal.component.isConnected) { resolve(); } else if (Date.now() - startTime > timeoutMs) { reject(new Error("Terminal connection timeout")); } else { setTimeout(check, 50); } }; check(); }); await waitForConnection(); terminal.component.write(`cd ${JSON.stringify(prootPath)}\n`); Sidebar.hide(); } } catch (error) { console.error("Failed to open terminal:", error); const errorMsg = error.message || "Unknown error occurred"; toast(`Failed to open terminal: ${errorMsg}`); } } async function deleteFile() { const msg = strings["delete entry"].replace("{name}", name); const confirmation = await confirm(strings.warning, msg); if (!confirmation) return; startLoading(); if (!(await fsOperation(url).exists())) return; // await fsOperation(url).delete(); recents.removeFile(url); if (helpers.isFile(type)) { await fsOperation(url).delete(); $target.remove(); const file = editorManager.getFile(url, "uri"); if (file) file.uri = null; editorManager.onupdate("delete-file"); editorManager.emit("update", "delete-file"); } else { if (isTerminalSafUri(url)) { const fs = fsOperation(url); const entries = await fs.lsDir(); if (entries.length === 0) { await fs.delete(); } else { const deleteRecursively = async (currentUrl) => { const currentFs = fsOperation(currentUrl); const currentEntries = await currentFs.lsDir(); for (const entry of currentEntries) { if (entry.isDirectory) { await deleteRecursively(entry.url); } else { await fsOperation(entry.url).delete(); } } await currentFs.delete(); }; await deleteRecursively(url); } } else { await fsOperation(url).delete(); } recents.removeFolder(url); helpers.updateUriOfAllActiveFiles(url, null); $target.parentElement.remove(); editorManager.onupdate("delete-folder"); editorManager.emit("update", "delete-folder"); } toast(strings.success); FileList.remove(url); } async function renameFile() { if (isTermuxSafUri(url) && !helpers.isFile(type)) { alert(strings.warning, strings["rename not supported"]); return; } let newName = await prompt(strings.rename, name, "text", { match: constants.FILE_NAME_REGEX, required: true, }); newName = helpers.fixFilename(newName); if (!newName || newName === name) return; startLoading(); const fs = fsOperation(url); let newUrl; if (isTermuxSafUri(url) && helpers.isFile(type)) { // Special handling for Termux SAF content files const newFilePath = Url.join(Url.dirname(url), newName); const content = await fs.readFile(); await fsOperation(Url.dirname(url)).createFile(newName, content); await fs.delete(); newUrl = newFilePath; } else { newUrl = await fs.renameTo(newName); } newName = Url.basename(newUrl); $target.querySelector(":scope>.text").textContent = newName; $target.dataset.url = newUrl; $target.dataset.name = newName; if (helpers.isFile(type)) { $target.querySelector(":scope>span").className = helpers.getIconForFile(newName); let file = editorManager.getFile(url, "uri"); if (file) { file.uri = newUrl; file.filename = newName; } } else { helpers.updateUriOfAllActiveFiles(url, newUrl); //Reloading the folder by collapsing and expanding the folder $target.click(); //collapse $target.click(); //expand } toast(strings.success); FileList.rename(url, newUrl); } async function createNew() { const msg = action === "new file" ? strings["enter file name"] : strings["enter folder name"]; let newName = await prompt(msg, "", "text", { match: constants.FILE_NAME_REGEX, required: true, }); newName = helpers.fixFilename(newName); if (!newName) return; startLoading(); try { const isNestedPath = newName.split("/").filter(Boolean).length > 1; let newUrl; if (action === "new file") { newUrl = await helpers.createFileStructure(url, newName); } else { newUrl = await helpers.createFileStructure(url, newName, false); } if (!newUrl.created) return; if (isNestedPath) { await refreshOpenFolder(url); await FileList.refresh(); toast(strings.success); return; } newName = Url.basename(newUrl.uri); appendEntryToOpenFolder(url, newUrl.uri, newUrl.type); FileList.append(url, newUrl.uri); toast(strings.success); } catch (error) { helpers.error(error); } finally { stopLoading(); } } async function paste() { if (clipBoard.url == null) { alert(strings.warning, "Nothing to paste"); return; } // Prevent pasting a folder into itself or its subdirectories if (helpers.isDir(clipBoard.$el.dataset.type)) { const sourceUrl = Url.parse(clipBoard.url).url; const targetUrl = Url.parse(url).url; // Check if trying to paste folder into itself if (sourceUrl === targetUrl) { alert(strings.warning, "Cannot paste a folder into itself"); return; } // Check if trying to paste folder into one of its subdirectories if ( targetUrl.startsWith(sourceUrl + "/") || targetUrl.startsWith(sourceUrl + "\\") ) { alert(strings.warning, "Cannot paste a folder into its subdirectory"); return; } } let CASE = ""; const $src = clipBoard.$el; const srcType = $src.dataset.type; const IS_FILE = helpers.isFile(srcType); const IS_DIR = helpers.isDir(srcType); const srcCollapsed = collapsed($src, IS_FILE); CASE += IS_FILE ? 1 : 0; CASE += srcCollapsed ? 1 : 0; CASE += $target.collapsed ? 1 : 0; startLoading(); try { const fs = fsOperation(clipBoard.url); const itemName = Url.basename(clipBoard.url); const possibleConflictUrl = Url.join(url, itemName); const doesExist = await fsOperation(possibleConflictUrl).exists(); if (doesExist) { let confirmation = await confirm( strings.warning, strings["already exists"] ? strings["already exists"].replace("{name}", itemName) : `"${itemName}" already exists in this location.`, ); if (!confirmation) return; } let newUrl; if (clipBoard.action === "cut") { // Special handling for SAF folders backed by terminal providers - move manually due to SAF limitations if (isTerminalSafUri(clipBoard.url) && IS_DIR) { const moveRecursively = async (sourceUrl, targetParentUrl) => { const sourceFs = fsOperation(sourceUrl); const sourceName = Url.basename(sourceUrl); const targetUrl = Url.join(targetParentUrl, sourceName); // Create target folder await fsOperation(targetParentUrl).createDirectory(sourceName); // Get all entries in source folder const entries = await sourceFs.lsDir(); // Move all files and folders recursively for (const entry of entries) { if (entry.isDirectory) { await moveRecursively(entry.url, targetUrl); } else { const fileContent = await fsOperation(entry.url).readFile(); const fileName = entry.name || Url.basename(entry.url); await fsOperation(targetUrl).createFile(fileName, fileContent); await fsOperation(entry.url).delete(); } } // Delete the now-empty source folder await sourceFs.delete(); return targetUrl; }; newUrl = await moveRecursively(clipBoard.url, url); } else { newUrl = await fs.moveTo(url); } } else { newUrl = await fs.copyTo(url); } const { name: newName } = await fsOperation(newUrl).stat(); stopLoading(); /** * CASES: * CASE 111: src is file and parent is collapsed where target is also collapsed * CASE 110: src is file and parent is collapsed where target is unclasped * CASE 101: src is file and parent is unclasped where target is collapsed * CASE 100: src is file and parent is unclasped where target is also unclasped * CASE 011: src is directory and parent is collapsed where target is also collapsed * CASE 001: src is directory and parent is unclasped where target is also collapsed * CASE 010: src is directory and parent is collapsed where target is also unclasped * CASE 000: src is directory and parent is unclasped where target is also unclasped */ if (clipBoard.action === "cut") { //move if (IS_FILE) { const file = editorManager.getFile(clipBoard.url, "uri"); if (file) file.uri = newUrl; } else if (IS_DIR) { helpers.updateUriOfAllActiveFiles(clipBoard.url, newUrl); } switch (CASE) { case "111": case "011": break; case "110": appendTile($target, createFileTile(newName, newUrl)); break; case "101": $src.remove(); break; case "100": appendTile($target, createFileTile(newName, newUrl)); $src.remove(); break; case "001": $src.parentElement.remove(); break; case "010": appendList($target, createFolderTile(newName, newUrl)); break; case "000": appendList($target, createFolderTile(newName, newUrl)); $src.parentElement.remove(); break; default: break; } FileList.remove(clipBoard.url); } else { //copy switch (CASE) { case "111": case "101": case "011": case "001": break; case "110": case "100": appendTile($target, createFileTile(newName, newUrl)); break; case "010": case "000": appendList($target, createFolderTile(newName, newUrl)); break; default: break; } } FileList.append(url, newUrl); toast(strings.success); clearClipboard(); } catch (error) { console.error(error); helpers.error(error); } finally { stopLoading(); } } async function insertFile() { startLoading(); try { const file = await FileBrowser("file", strings["insert file"]); const sourceFs = fsOperation(file.url); const data = await sourceFs.readFile(); const sourceStats = await sourceFs.stat(); const insertedFile = await fsOperation(url).createFile( sourceStats.name, data, ); appendTile($target, createFileTile(sourceStats.name, insertedFile)); FileList.append(url, insertedFile); } catch (error) { } finally { stopLoading(); } } async function clipBoardAction() { clipBoard.url = url; clipBoard.action = action; clipBoard.$el = $target; if (action === "cut") $target.classList.add("cut"); else $target.classList.remove("cut"); } async function open() { FileBrowser.openFolder({ url, name, }); } function cancelAction() { clipBoard.$el.classList.remove("cut"); clearClipboard(); } function clearClipboard() { clipBoard.$el = null; clipBoard.url = null; clipBoard.action = null; } } /** * * @param {"file"|"dir"|"root"} type * @param {string} url */ function handleClick(type, uri) { if (!helpers.isFile(type)) return; openFile(uri, { render: true }); Sidebar.hide(); } /** * Insert a file into the list * @param {HTMLElement} $target * @param {HTMLElement} $tile */ function appendTile($target, $tile) { $target = $target.nextElementSibling; const $firstTile = $target.get(":scope>[type=file]"); if ($firstTile) $target.insertBefore($tile, $firstTile); else $target.append($tile); } /** * Insert folder into the list * @param {HTMLElement} $target The target element * @param {HTMLElement} $list The tile to be inserted */ function appendList($target, $list) { $target = $target.nextElementSibling; const $firstList = $target.firstElementChild; if ($firstList) $target.insertBefore($list, $firstList); else $target.append($list); } /** * Get the active file tree for a folder element, if it has been loaded. * @param {HTMLElement} $el * @returns {FileTree|null} */ function getLoadedFileTree($el) { return ( $el?.$ul?._fileTree || $el?.fileTree || $el?.nextElementSibling?._fileTree ); } /** * Update matching expanded folder views with a new entry. * @param {string} parentUrl * @param {string} entryUrl * @param {"file"|"folder"} type */ function appendEntryToOpenFolder(parentUrl, entryUrl, type) { const filesApp = sidebarApps.get("files"); const $els = filesApp.getAll(`[data-url="${parentUrl}"]`); const isDirectory = type === "folder"; const name = Url.basename(entryUrl); Array.from($els).forEach(($el) => { if (!(helpers.isDir($el.dataset.type) || $el.dataset.type === "root")) { return; } if (!$el.unclasped) return; const fileTree = getLoadedFileTree($el); if (fileTree) { fileTree.appendEntry(name, entryUrl, isDirectory); return; } if (isDirectory) { appendList($el, createFolderTile(name, entryUrl)); } else { appendTile($el, createFileTile(name, entryUrl)); } }); } /** * Refresh matching expanded folder views. * @param {string} folderUrl */ async function refreshOpenFolder(folderUrl) { const filesApp = sidebarApps.get("files"); const $els = filesApp.getAll(`[data-url="${folderUrl}"]`); await Promise.all( Array.from($els).map(async ($el) => { if (!(helpers.isDir($el.dataset.type) || $el.dataset.type === "root")) { return; } const fileTree = getLoadedFileTree($el); if (fileTree) { await fileTree.refresh(); } }), ); } /** * Create a folder tile * @param {string} name * @param {string} url * @returns {HTMLElement} */ function createFolderTile(name, url) { const $list = collapsableList(name, "folder", { ontoggle: () => expandList($list), }); const { $title } = $list; $title.dataset.url = url; $title.dataset.name = name; $title.dataset.type = "dir"; return $list; } /** * Create a file tile * @param {string} name * @param {string} url * @returns {HTMLElement} */ function createFileTile(name, url) { const $tile = tile({ lead: , text: name, }); $tile.dataset.url = url; $tile.dataset.name = name; $tile.dataset.type = "file"; return $tile; } /** * Add file or folder to the list if expanded * @param {string} url Url of file or folder to add * @param {'file'|'folder'} type is file or folder */ openFolder.add = async (url, type) => { const { url: parent } = await fsOperation(Url.dirname(url)).stat(); FileList.append(parent, url); appendEntryToOpenFolder(parent, url, type); }; openFolder.renameItem = (oldFile, newFile, newFilename) => { FileList.rename(oldFile, newFile); helpers.updateUriOfAllActiveFiles(oldFile, newFile); const filesApp = sidebarApps.get("files"); const $els = filesApp.getAll(`[data-url="${oldFile}"]`); Array.from($els).forEach(($el) => { if ($el.dataset.type === "dir") { $el = $el.$title; setTimeout(() => { $el.collapse(); $el.expand(); }, 0); } else { $el.querySelector(":scope>span").className = helpers.getIconForFile(newFilename); } $el.dataset.url = newFile; $el.dataset.name = newFilename; $el.querySelector(":scope>.text").textContent = newFilename; }); }; openFolder.removeItem = (url) => { FileList.remove(url); const folder = addedFolder.find(({ url: fUrl }) => url === fUrl); if (folder) { folder.remove(); return; } const filesApp = sidebarApps.get("files"); const $el = filesApp.getAll(`[data-url="${url}"]`); Array.from($el).forEach(($el) => { const type = $el.dataset.type; if (helpers.isFile(type)) { $el.remove(); } else { $el.parentElement.remove(); } }); }; openFolder.removeFolders = (url) => { ({ url } = Url.parse(url)); const regex = new RegExp("^" + escapeStringRegexp(url)); addedFolder.forEach((folder) => { if (regex.test(folder.url)) { folder.remove(); } }); }; /** * Find the folder that contains the url * @param {String} url * @returns {Folder} */ openFolder.find = (url) => { const found = addedFolder.find((folder) => folder.url === url); if (found) return found; return addedFolder.find((folder) => { const { url: furl } = Url.parse(folder.url); const regex = new RegExp("^" + escapeStringRegexp(furl)); return regex.test(url); }); }; export default openFolder; ================================================ FILE: src/lib/polyfill.js ================================================ // polyfill for prepend (function (arr) { arr.forEach(function (item) { if (item.hasOwnProperty("prepend")) { return; } Object.defineProperty(item, "prepend", { configurable: true, enumerable: true, writable: true, value: function prepend() { var argArr = Array.prototype.slice.call(arguments), docFrag = document.createDocumentFragment(); argArr.forEach(function (argItem) { var node = argItem instanceof Node ? argItem : document.createTextNode(String(argItem)); docFrag.appendChild(node); }); this.insertBefore(docFrag, this.firstChild); }, }); }); })([Element.prototype, Document.prototype, DocumentFragment.prototype]); // polyfill for closest (function (arr) { arr.forEach(function (item) { if (item.hasOwnProperty("closest")) { return; } Object.defineProperty(item, "closest", { configurable: true, enumerable: true, writable: true, value: function closest(s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s), i, el = this; do { i = matches.length; while (--i >= 0 && matches.item(i) !== el) {} } while (i < 0 && (el = el.parentElement)); return el; }, }); }); })([Element.prototype]); // polyfill for replaceWith (function (arr) { arr.forEach(function (item) { if (item.hasOwnProperty("replaceWith")) { return; } Object.defineProperty(item, "replaceWith", { configurable: true, enumerable: true, writable: true, value: function replaceWith() { var parent = this.parentNode, i = arguments.length, currentNode; if (!parent) return; if (!i) // if there are no arguments parent.removeChild(this); while (i--) { // i-- decrements i and returns the value of i before the decrement currentNode = arguments[i]; if (typeof currentNode !== "object") { currentNode = this.ownerDocument.createTextNode(currentNode); } else if (currentNode.parentNode) { currentNode.parentNode.removeChild(currentNode); } // the value of "i" below is after the decrement if (!i) // if currentNode is the first argument (currentNode === arguments[0]) parent.replaceChild(currentNode, this); // if currentNode isn't the first else parent.insertBefore(this.previousSibling, currentNode); } }, }); }); })([Element.prototype, CharacterData.prototype, DocumentType.prototype]); // polyfill for toggleAttribute (function (arr) { arr.forEach(function (item) { if (item.hasOwnProperty("toggleAttribute")) { return; } Object.defineProperty(item, "toggleAttribute", { configurable: true, enumerable: true, writable: true, value: function toggleAttribute() { var attr = arguments[0]; if (this.hasAttribute(attr)) { this.removeAttribute(attr); } else { this.setAttribute(attr, arguments[1] || ""); } }, }); }); })([Element.prototype]); // polyfill for performance.now (function () { if ("performance" in window === false) { window.performance = {}; } Date.now = Date.now || function () { // thanks IE8 return new Date().getTime(); }; if ("now" in window.performance === false) { var nowOffset = Date.now(); if (performance.timing && performance.timing.navigationStart) { nowOffset = performance.timing.navigationStart; } window.performance.now = function now() { return Date.now() - nowOffset; }; } })(); ================================================ FILE: src/lib/prettierFormatter.js ================================================ import fsOperation from "fileSystem"; import { parse } from "acorn"; import toast from "components/toast"; import appSettings from "lib/settings"; import prettierPluginBabel from "prettier/plugins/babel"; import prettierPluginEstree from "prettier/plugins/estree"; import prettierPluginGraphql from "prettier/plugins/graphql"; import prettierPluginHtml from "prettier/plugins/html"; import prettierPluginMarkdown from "prettier/plugins/markdown"; import prettierPluginPostcss from "prettier/plugins/postcss"; import prettierPluginTypescript from "prettier/plugins/typescript"; import prettierPluginYaml from "prettier/plugins/yaml"; import prettier from "prettier/standalone"; import helpers from "utils/helpers"; import Url from "utils/Url"; const PRETTIER_ID = "prettier"; const PRETTIER_NAME = "Prettier"; const CONFIG_FILENAMES = [ ".prettierrc", ".prettierrc.json", ".prettierrc.json5", ".prettierrc.js", ".prettierrc.cjs", ".prettierrc.mjs", ".prettierrc.config.cjs", ".prettierrc.config.mjs", ".prettier.config.js", ".prettier.config.cjs", ".prettier.config.mjs", "prettier.config.json", "prettier.config.js", "prettier.config.cjs", "prettier.config.mjs", ]; const PRETTIER_PLUGINS = [ prettierPluginEstree, prettierPluginBabel, prettierPluginHtml, prettierPluginMarkdown, prettierPluginPostcss, prettierPluginTypescript, prettierPluginYaml, prettierPluginGraphql, ]; /** * Supported parser mapping keyed by CodeMirror mode name * @type {Record} */ const MODE_TO_PARSER = { angular: "angular", gfm: "markdown", css: "css", graphql: "graphql", html: "html", json: "json", json5: "json", jsx: "babel", less: "less", markdown: "markdown", md: "markdown", mdx: "mdx", scss: "scss", styled_jsx: "babel", typescript: "typescript", tsx: "typescript", jsonc: "json", yaml: "yaml", yml: "yaml", vue: "vue", javascript: "babel", }; const SUPPORTED_EXTENSIONS = [ "js", "cjs", "mjs", "jsx", "ts", "tsx", "json", "json5", "css", "scss", "less", "html", "htm", "vue", "md", "markdown", "mdx", "yaml", "yml", "graphql", "gql", ]; /** * Register Prettier formatter with Acode instance */ export function registerPrettierFormatter() { if (!window?.acode) return; const alreadyRegistered = acode.formatters.some( ({ id }) => id === PRETTIER_ID, ); if (alreadyRegistered) return; acode.registerFormatter( PRETTIER_ID, SUPPORTED_EXTENSIONS, () => formatActiveFileWithPrettier(), PRETTIER_NAME, ); } async function formatActiveFileWithPrettier() { const file = editorManager?.activeFile; const editor = editorManager?.editor; if (!file || file.type !== "editor" || !editor) return false; const modeName = (file.currentMode || "text").toLowerCase(); const parser = getParserForMode(modeName); if (!parser) { toast("Prettier does not support this file type yet"); return false; } const doc = editor.state.doc; const source = doc.toString(); const filepath = file.uri || file.filename || ""; try { const config = await resolvePrettierConfig(file); const formatted = await prettier.format(source, { ...config, parser, plugins: PRETTIER_PLUGINS, filepath, overrideEditorconfig: true, }); if (formatted === source) return true; editor.dispatch({ changes: { from: 0, to: doc.length, insert: formatted, }, }); return true; } catch (error) { const message = error instanceof Error ? error.message : String(error); toast(message); return false; } } function getParserForMode(modeName) { if (MODE_TO_PARSER[modeName]) return MODE_TO_PARSER[modeName]; if (modeName.includes("javascript")) return "babel"; if (modeName.includes("typescript")) return "typescript"; return null; } async function resolvePrettierConfig(file) { const overrides = appSettings?.value?.prettier || {}; const projectConfig = await loadProjectConfig(file); const result = { ...overrides, ...(projectConfig || {}) }; if (file?.eol && result.endOfLine == null) { result.endOfLine = file.eol === "windows" ? "crlf" : "lf"; } if (result.useTabs == null) { result.useTabs = !appSettings?.value?.softTab; } if ( result.tabWidth == null && typeof appSettings?.value?.tabSize === "number" ) { result.tabWidth = appSettings.value.tabSize; } return result; } async function loadProjectConfig(file) { const uri = file?.uri; if (!uri) return null; const projectRoot = findProjectRoot(uri); const directories = collectCandidateDirectories(uri, projectRoot); for (const directory of directories) { const config = await readConfigFromDirectory(directory); if (config) return config; } return null; } function findProjectRoot(uri) { const folders = Array.isArray(globalThis.addedFolder) ? globalThis.addedFolder : []; const target = normalizePath(uri); let match = null; let matchLength = -1; for (const folder of folders) { const folderUrl = folder?.url; if (!folderUrl) continue; const normalized = normalizePath(folderUrl); if (!normalized) continue; if (target === normalized || target.startsWith(`${normalized}/`)) { if (normalized.length > matchLength) { match = folderUrl; matchLength = normalized.length; } } } return match; } function collectCandidateDirectories(fileUri, projectRoot) { const directories = []; const visited = new Set(); let currentDir = safeDirname(fileUri); while (currentDir) { const normalized = normalizePath(currentDir); if (visited.has(normalized)) break; directories.push(currentDir); visited.add(normalized); if (projectRoot && pathsAreSame(currentDir, projectRoot)) break; const parent = safeDirname(currentDir); if (!parent || parent === currentDir) break; currentDir = parent; } if ( projectRoot && !directories.some((dir) => pathsAreSame(dir, projectRoot)) ) { directories.push(projectRoot); } return directories; } function safeDirname(path) { try { return Url.dirname(path); } catch (_) { return null; } } async function readConfigFromDirectory(directory) { if (!directory) return null; for (const name of CONFIG_FILENAMES) { const config = await loadConfigFile(directory, name); if (config) return config; } return loadPrettierFromPackageJson(directory); } async function loadConfigFile(directory, basename) { try { const filePath = Url.join(directory, basename); const fs = fsOperation(filePath); if (!(await fs.exists())) return null; const text = await fs.readFile("utf8"); switch (basename) { case ".prettierrc": case ".prettierrc.json": case ".prettierrc.json5": case "prettier.config.json": return parseJsonLike(text); case ".prettierrc.js": case ".prettier.config.js": case "prettier.config.js": return parseJsConfig(directory, text, filePath); case ".prettierrc.mjs": case ".prettierrc.config.mjs": case ".prettier.config.mjs": case "prettier.config.mjs": return parseJsConfig(directory, text, filePath); case ".prettierrc.cjs": case ".prettierrc.config.cjs": case ".prettier.config.cjs": case "prettier.config.cjs": return parseJsConfig(directory, text, filePath); default: return null; } } catch (_) { return null; } } async function loadPrettierFromPackageJson(directory) { try { const pkgPath = Url.join(directory, "package.json"); const fs = fsOperation(pkgPath); if (!(await fs.exists())) return null; const pkg = await fs.readFile("json"); const config = pkg?.prettier; if (config && typeof config === "object") return config; } catch (_) { return null; } return null; } function parseJsonLike(text) { const trimmed = text?.trim(); if (!trimmed) return null; const parsed = helpers.parseJSON(trimmed); if (parsed) return parsed; try { return parseSafeExpression(trimmed); } catch (_) { return null; } } function parseJsConfig(directory, source, absolutePath) { if (!source) return null; void directory; void absolutePath; try { return extractConfigFromProgram(source); } catch (_) { return null; } } function parseProgram(source) { try { return parse(source, { ecmaVersion: "latest", sourceType: "module", allowHashBang: true, }); } catch (_) { return parse(source, { ecmaVersion: "latest", sourceType: "script", allowHashBang: true, }); } } function extractConfigFromProgram(source) { const ast = parseProgram(source); const scope = new Map(); for (const statement of ast.body) { const declared = readVariableDeclaration(statement, scope); if (declared) { for (const [name, value] of declared) { scope.set(name, value); } continue; } const exported = readCommonJsExport(statement, scope); if (exported !== undefined) return exported; const esmExported = readEsmExport(statement, scope); if (esmExported !== undefined) return esmExported; } return null; } function parseSafeExpression(text) { const wrapped = `(${text})`; const ast = parse(wrapped, { ecmaVersion: "latest", sourceType: "module", allowHashBang: true, }); const statement = ast.body[0]; if (statement?.type !== "ExpressionStatement") return null; return evaluateNode(statement.expression, new Map()); } function readVariableDeclaration(statement, scope) { if (statement?.type !== "VariableDeclaration") return null; const values = new Map(); const lookupScope = new Map(scope); for (const decl of statement.declarations || []) { if (!decl || decl.type !== "VariableDeclarator") continue; if (decl.id?.type !== "Identifier") continue; if (!decl.init) continue; try { const value = evaluateNode(decl.init, lookupScope); values.set(decl.id.name, value); lookupScope.set(decl.id.name, value); } catch (_) { // Ignore unsupported declarations } } return values.size ? values : null; } function readCommonJsExport(statement, scope) { if (statement?.type !== "ExpressionStatement") return undefined; const expr = statement.expression; if (expr?.type !== "AssignmentExpression" || expr.operator !== "=") { return undefined; } if (!isModuleExports(expr.left)) return undefined; return evaluateNode(expr.right, scope); } function readEsmExport(statement, scope) { if (statement?.type !== "ExportDefaultDeclaration") return undefined; return evaluateNode(statement.declaration, scope); } function isModuleExports(node) { return ( node?.type === "MemberExpression" && !node.computed && node.object?.type === "Identifier" && node.object.name === "module" && node.property?.type === "Identifier" && node.property.name === "exports" ); } function evaluateNode(node, scope) { if (!node) return null; switch (node.type) { case "ObjectExpression": return evaluateObjectExpression(node, scope); case "ArrayExpression": return node.elements.map((entry) => evaluateNode(entry, scope)); case "Literal": return node.value; case "TemplateLiteral": if (node.expressions.length) { throw new Error("Template expressions are not supported"); } return node.quasis.map((part) => part.value.cooked ?? "").join(""); case "Identifier": if (scope.has(node.name)) return scope.get(node.name); if (node.name === "undefined") return undefined; throw new Error(`Unsupported identifier: ${node.name}`); case "UnaryExpression": return evaluateUnaryExpression(node, scope); default: throw new Error(`Unsupported node type: ${node.type}`); } } function evaluateObjectExpression(node, scope) { const output = {}; for (const property of node.properties || []) { if (!property || property.type !== "Property") { throw new Error("Unsupported object property"); } if (property.kind !== "init" || property.method || property.shorthand) { throw new Error("Unsupported object property kind"); } const key = property.computed ? evaluateNode(property.key, scope) : getPropertyKey(property.key); const normalizedKey = typeof key === "string" || typeof key === "number" ? String(key) : null; if (!normalizedKey) { throw new Error("Unsupported object key"); } output[normalizedKey] = evaluateNode(property.value, scope); } return output; } function getPropertyKey(node) { if (node?.type === "Identifier") return node.name; if (node?.type === "Literal") return node.value; throw new Error("Unsupported property key"); } function evaluateUnaryExpression(node, scope) { const value = evaluateNode(node.argument, scope); switch (node.operator) { case "+": return +value; case "-": return -value; case "!": return !value; default: throw new Error(`Unsupported unary operator: ${node.operator}`); } } function normalizePath(path) { let result = String(path || "").replace(/\\/g, "/"); while (result.length > 1 && result.endsWith("/")) { const prefix = result.slice(0, -1); if (/^[a-z]+:\/{0,2}$/i.test(prefix)) break; result = prefix; } return result; } function pathsAreSame(a, b) { if (!a || !b) return false; return normalizePath(a) === normalizePath(b); } ================================================ FILE: src/lib/projects.js ================================================ const projects = { html() { acode.addIcon( "html-project-icon", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAABIUExURUdwTORPJuRPJuNOJeRPJuNQJ+RPJuNOJuNPJuROJeRPJuNOJuRPJuRQJONPJuNPJeVQI+NPJeROJuNPJuZPJ+NOJuRPJuNPJkmKsooAAAAXdFJOUwA6h5uxKGh/60/VE8BBll8izqXdDHT3jnqTYwAAAQRJREFUGBl9wY22azAURtGFhMS/Vvu9/5veHeGMMrhzAvoPkqBHgWTRo4XE6ZEjqfSoImn0qCGpZQYuBpmaJMpMXESZSFLIfLioZQoSLzMCzYmMJ+lkXsBbVx0bmR546YosSGqBUheBbJEUuFgkLWROpuMsSHJklYznTKYiK2WaHwWsMiXZRxceZpkP2SQzGO1mKGQmsigTwWvXQZSJZIVMDZ12K9QyBdks0wBDuUjvVw00MjNZJ1OxmWc2o0zHLkhynl9OUuDQyoS+jGx8PfZfSS2HXrvg6unVatdzcLrlOIy6NXIog26Ekj9+qlqdtNXkOSua/qvNt28Kbq1xfL/HuPLjH4f8MW+juHZUAAAAAElFTkSuQmCC", ); return { async files() { return { "index.html": '\n\n\n \n \n \n \n <%name%>\n\n\n\t

          <%name%>

          \n\n', "css/index.css": "", "js/index.js": "", }; }, icon: "html-project-icon", }; }, }; export default { list() { return Object.keys(projects).map((project) => ({ name: project, icon: projects[project]().icon, })); }, get(project) { return projects[project]?.(); }, /** * * @param {string} project Project name * @param {()=>Promise>} files Async function that returns a map of files * @param {string} iconSrc Icon source (data url) */ set(project, files, iconSrc) { const icon = `${project}-project-icon`; acode.addIcon(`${project}-project-icon`, iconSrc); projects[project] = () => ({ files, icon }); }, delete(project) { delete projects[project]; }, }; ================================================ FILE: src/lib/recents.js ================================================ import select from "dialogs/select"; import escapeStringRegexp from "escape-string-regexp"; import helpers from "utils/helpers"; import Url from "utils/Url"; const recents = { /** * @returns {Array} */ get files() { const files = helpers.parseJSON(localStorage.recentFiles); return Array.isArray(files) ? files : []; }, /** * @returns {{url: String, opts: Map}[]} */ get folders() { const folders = helpers.parseJSON(localStorage.recentFolders); return Array.isArray(folders) ? folders : []; }, set files(list) { if (Array.isArray(list)) localStorage.recentFiles = JSON.stringify(list); }, set folders(list) { if (Array.isArray(list)) localStorage.recentFolders = JSON.stringify(list); }, MAX: 10, /** * * @param {string} file */ addFile(file) { let files = this.files; if (files.length >= this.MAX) files.pop(); files = files.filter((i) => i !== file); files.unshift(file); this.files = files; }, addFolder(url, opts) { if (url.slice(-1) === "/") { url = url.slice(0, -1); } let folders = this.folders; if (folders.length >= this.MAX) folders.pop(); folders = folders.filter((i) => i.url !== url); folders.unshift({ url, opts, }); this.folders = folders; }, removeFolder(url) { ({ url } = Url.parse(url)); this.folders = this.folders.filter((folder) => { return !new RegExp("^" + escapeStringRegexp(folder.url)).test(url); }); }, removeFile(url) { ({ url } = Url.parse(url)); this.files = this.files.filter((file) => { return !new RegExp("^" + escapeStringRegexp(url)).test(file); }); }, clear() { this.files = []; this.folders = []; }, /** * * @param {Array>} [extra] * @param {"file"|"dir"|"all"} [type] * @param {string} [title] * @returns {Promise} */ select(extra, type = "all", title = strings["open recent"]) { const all = []; const MAX = 20; const shortName = (name) => { name = helpers.getVirtualPath(name); if (name.length > MAX) { return "..." + name.substr(-MAX - 3); } return name; }; if (type === "dir" || type === "all") { let dirs = this.folders; for (let dir of dirs) { const { url } = dir; const dirValue = { type: "dir", val: dir, }; const tailElement = tag("span", { className: "icon clearclose", dataset: { action: "clear", }, }); all.push({ value: dirValue, text: shortName(url), icon: "folder", tailElement: tailElement, ontailclick: (e) => { const $item = e.currentTarget.closest(".tile"); if ($item) $item.remove(); this.removeFolder(dir.url); }, }); } } if (type === "file" || type === "all") { let files = this.files; for (let file of files) { if (!file) continue; const name = shortName(Url.parse(file).url); const fileValue = { type: "file", val: file, }; const tailElement = tag("span", { className: "icon clearclose", dataset: { action: "clear", }, }); all.push({ value: fileValue, text: name, icon: helpers.getIconForFile(name), tailElement: tailElement, ontailclick: (e) => { const $item = e.currentTarget.closest(".tile"); if ($item) $item.remove(); this.removeFile(file); }, }); } } if (type === "all") all.push(["clear", strings.clear, "icon clearclose"]); if (extra) { extra = extra.map((item) => { item[1] = shortName(item[1]); return item; }); all.push(...extra); } return select(title, all, { textTransform: false, }); }, }; export default recents; ================================================ FILE: src/lib/remoteStorage.js ================================================ import fsOperation from "fileSystem"; import Ftp from "fileSystem/ftp"; import Sftp from "fileSystem/sftp"; import loader from "dialogs/loader"; import multiPrompt from "dialogs/multiPrompt"; import URLParse from "url-parse"; import helpers from "utils/helpers"; import Url from "utils/Url"; export default { /** * * @param {...any} args [username, password, hostname, port, ftps, active, name] */ async addFtp(...args) { let stopConnection = false; const { username, // password, hostname, port, ftps, active, alias, } = await prompt(...args); const security = ftps ? "ftps" : "ftp"; const mode = active ? "active" : "passive"; const ftp = Ftp(hostname, username, password, port, security, mode); try { loader.create(strings["add ftp"], strings["connecting..."], { timeout: 10000, callback() { stopConnection = true; }, }); const [home] = await Promise.all([ftp.getWorkingDirectory(), loadAd()]); if (stopConnection) { stopConnection = false; return; } const url = Url.formate({ protocol: "ftp:", username, password, hostname, port, path: "/", query: { mode, security, }, }); const res = { url, alias, name: alias, type: "ftp", home: null, }; if (home !== "/") { res.home = home; } loader.destroy(); await helpers.showInterstitialIfReady(); return res; } catch (err) { if (stopConnection) { stopConnection = false; return; } loader.destroy(); await helpers.error(err); return await this.addFtp( username, password, hostname, alias, port, security, mode, ); } function prompt(username, password, hostname, alias, port, security, mode) { port = port || 21; security = security || "ftp"; mode = mode || "passive"; return multiPrompt(strings["add ftp"], [ { id: "alias", placeholder: strings.name, type: "text", value: alias ? alias : "", required: true, }, { id: "username", placeholder: `${strings.username} (${strings.optional})`, type: "text", value: username, }, { id: "hostname", placeholder: strings.hostname, type: "text", required: true, value: hostname, }, { id: "password", placeholder: `${strings.password} (${strings.optional})`, type: "password", value: password, }, [ `${strings["security type"]}: `, { id: "ftp", placeholder: "FTP", name: "type", type: "radio", value: security === "ftp" ? true : false, }, { id: "ftps", placeholder: "FTPS", name: "type", type: "radio", value: security === "ftps" ? true : false, }, ], [ `${strings["connection mode"]}: `, { id: "active", placeholder: "Active", name: "mode", type: "radio", value: mode === "active" ? true : false, }, { id: "passive", placeholder: "Passive", name: "mode", type: "radio", value: mode === "passive" ? true : false, }, ], { id: "port", placeholder: `${strings.port} (${strings.optional})`, type: "number", value: port, }, ]); } }, /** * @param {...any} args [hostname, username, keyFile, password, passphrase, port, name] */ async addSftp(...args) { let stopConnection = false; const { hostname, username, keyFile, password, passPhrase, port, alias, usePassword, } = await prompt(...args); const authType = usePassword ? "password" : "keyFile"; loader.create(strings["add sftp"], strings["connecting..."], { timeout: 10000, callback() { stopConnection = true; }, }); const connection = Sftp(hostname, Number.parseInt(port), username, { password, keyFile, passPhrase, }); try { const [home] = await Promise.all([connection.pwd(), loadAd()]); if (stopConnection) { stopConnection = false; return; } let localKeyFile = ""; if (keyFile) { let fs = fsOperation(keyFile); const text = await fs.readFile("utf8"); //Original key file sometimes gives permission error //To solve permission error const filename = keyFile.hashCode(); localKeyFile = Url.join(DATA_STORAGE, filename); fs = fsOperation(localKeyFile); const exists = await fs.exists(); if (exists) { await fs.writeFile(text); } else { let fs = fsOperation(DATA_STORAGE); await fs.createFile(filename, text); } } const url = Url.formate({ protocol: "sftp:", hostname, username, password, port, path: "/", query: { keyFile: localKeyFile, passPhrase, }, }); loader.destroy(); await helpers.showInterstitialIfReady(); return { alias, name: alias, url, type: "sftp", home, }; } catch (err) { if (stopConnection) { stopConnection = false; return; } loader.destroy(); await helpers.error(err); return await this.addSftp( hostname, username, keyFile, password, passPhrase, port, alias, authType, ); } function prompt( hostname, username, keyFile, password, passPhrase, port, alias, authType = "password", ) { port = port || 22; const MODE_PASS = authType === "password"; const inputs = [ { id: "alias", placeholder: strings.name, type: "text", value: alias ? alias : "", required: true, }, { id: "username", placeholder: `${strings.username} (${strings.optional})`, type: "text", value: username, }, { id: "hostname", placeholder: strings.hostname, type: "text", required: true, value: hostname, }, [ "Authentication type: ", { id: "usePassword", placeholder: strings.password, name: "authType", type: "radio", value: MODE_PASS, onchange() { if (!!this.value) { this.prompt.$body.get("#password").hidden = false; this.prompt.$body.get("#keyFile").hidden = true; this.prompt.$body.get("#passPhrase").hidden = true; } }, }, { id: "useKeyFile", placeholder: strings["key file"], name: "authType", type: "radio", value: !MODE_PASS, onchange() { if (!!this.value) { const $password = this.prompt.$body.get("#password"); $password.hidden = true; $password.value = ""; this.prompt.$body.get("#keyFile").hidden = false; this.prompt.$body.get("#passPhrase").hidden = false; } }, }, ], { id: "password", placeholder: strings.password, name: "password", type: "password", value: password, hidden: !MODE_PASS, }, { id: "keyFile", placeholder: strings["select key file"], name: "keyFile", hidden: MODE_PASS, value: keyFile, type: "text", onclick() { sdcard.openDocumentFile((res) => { this.value = res.uri; }); }, }, { id: "passPhrase", placeholder: `${strings.passphrase} (${strings.optional})`, name: "passPhrase", type: "password", hidden: MODE_PASS, value: passPhrase, }, { id: "port", placeholder: `${strings.port} (${strings.optional})`, type: "number", value: port, }, ]; return multiPrompt(strings["add sftp"], inputs); } }, edit({ name, storageType, url }) { let { username, password, hostname, port, query } = URLParse(url, true); if (username) { username = decodeURIComponent(username); } if (password) { password = decodeURIComponent(password); } if (storageType === "ftp") { let { security, mode } = query; if (security) { security = decodeURIComponent(security); } if (mode) { mode = decodeURIComponent(mode); } return this.addFtp( username, password, hostname, name, port, security, mode, ); } if (storageType === "sftp") { let { passPhrase, keyFile } = query; if (passPhrase) { passPhrase = decodeURIComponent(passPhrase); } if (keyFile) { keyFile = decodeURIComponent(keyFile); } return this.addSftp( hostname, username, keyFile, password, passPhrase, port, name, password ? "password" : "key", ); } return null; }, }; async function loadAd() { if (!helpers.canShowAds()) return; try { if (!(await window.iad?.isLoaded())) { toast(strings.loading); await window.iad.load(); } } catch (error) { console.warn("Failed to load interstitial ad.", error); } } ================================================ FILE: src/lib/removeAds.js ================================================ import purchaseListener from "handlers/purchase"; import { hideAd } from "./startAd.js"; /** * Remove ads after purchase * @returns {Promise} */ export default function removeAds() { return new Promise((resolve, reject) => { iap.getProducts(["acode_pro_new"], (products) => { const [product] = products; iap.setPurchaseUpdatedListener(...purchaseListener(onpurchase, reject)); iap.purchase( product.productId, (code) => { // ignore }, (err) => { alert(strings.error, err); }, ); }); function onpurchase() { resolve(null); hideAd(true); localStorage.setItem("acode_pro", "true"); window.IS_FREE_VERSION = false; toast(strings["thank you :)"]); } }); } ================================================ FILE: src/lib/restoreFiles.js ================================================ import EditorFile from "./editorFile"; /** * * @param {import('./editorFile').FileOptions[]} files * @param {(count: number)=>void} callback */ export default async function restoreFiles(files) { let rendered = false; await Promise.all( files.map(async (file, i) => { rendered ||= !!file.render; if (i === files.length - 1 && !rendered) { file.render = true; } const { filename, render = false } = file; const options = { ...file, render, emitUpdate: false, }; new EditorFile(filename, options); }), ); } ================================================ FILE: src/lib/restoreTheme.js ================================================ import themes from "theme/list"; import Color from "utils/color"; import appSettings from "./settings"; let count = 0; /** * Restores the theme or darkens the status bar and navigation bar * Used when dialogs are opened which has mask that darkens the background * @param {boolean} darken Whether to darken the status bar and navigation bar * @returns */ export default function restoreTheme(darken = false) { if (!count && !darken) return; count += darken ? 1 : -1; if (darken !== !!count) return; if (darken && document.body.classList.contains("loading")) return; let themeName = DOES_SUPPORT_THEME ? appSettings.value.appTheme : "default"; let theme = themes.get(themeName); if (theme?.version !== "free" && IS_FREE_VERSION) { themeName = "default"; theme = themes.get(themeName); appSettings.value.appTheme = themeName; appSettings.update(); } if ( !theme.darkenedPrimaryColor || theme.darkenedPrimaryColor === theme.primaryColor ) { theme.darkenPrimaryColor(); } const color = darken ? theme.darkenedPrimaryColor : theme.primaryColor; const hexColor = Color(color).hex.toString(); system.setUiTheme(hexColor, theme.toJSON("hex")); } ================================================ FILE: src/lib/run.js ================================================ import fsOperation from "fileSystem"; import tutorial from "components/tutorial"; import alert from "dialogs/alert"; import box from "dialogs/box"; import markdownIt from "markdown-it"; import anchor from "markdown-it-anchor"; import MarkdownItGitHubAlerts from "markdown-it-github-alerts"; import mimeType from "mime-types"; import mustache from "mustache"; import openMarkdownPreview from "pages/markdownPreview"; import browser from "plugins/browser"; 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 EditorFile from "./editorFile"; import openFolder from "./openFolder"; import appSettings from "./settings"; /**@type {Server} */ let webServer; /** * Starts the server and run the active file in browser * @param {Boolean} isConsole * @param {"inapp"|"browser"} target * @param {Boolean} runFile */ async function run( isConsole = false, target = appSettings.value.previewMode, runFile = false, ) { /** @type {EditorFile} */ const activeFile = isConsole ? null : editorManager.activeFile; if (!isConsole && Url.extname(activeFile?.filename || "") === ".md") { if (!(await activeFile?.canRun())) return; await openMarkdownPreview(activeFile); return; } if (!isConsole && !runFile) { const { serverPort, previewPort, previewMode, disableCache, host } = appSettings.value; if (serverPort !== previewPort) { const src = `http://${host}:${previewPort}`; if (previewMode === "browser") { system.openInBrowser(src); return; } browser.open(src); return; } } if (!isConsole && !(await activeFile?.canRun())) return; if (!isConsole && !localStorage.__init_runPreview) { localStorage.__init_runPreview = true; tutorial("run-preview", strings["preview info"]); } const uuid = helpers.uuid(); let isLoading = false; let filename, pathName, extension; let port = appSettings.value.serverPort; let EXECUTING_SCRIPT = uuid + "_script.js"; const MIMETYPE_HTML = mimeType.lookup("html"); const CONSOLE_SCRIPT = uuid + "_console.js"; const MARKDOWN_STYLE = uuid + "_md.css"; const queue = []; if (activeFile) { filename = activeFile.filename; pathName = activeFile.location; extension = Url.extname(filename); if (!pathName && activeFile.uri) { pathName = Url.dirname(activeFile.uri); } } if (runFile && extension === "svg") { try { const fs = fsOperation(activeFile.uri); const res = await fs.readFile(); const blob = new Blob([new Uint8Array(res)], { type: mimeType.lookup(extension), }); box(filename, ``); } catch (err) { helpers.error(err); } return; } if (!runFile && filename !== "index.html" && pathName) { const folder = openFolder.find(activeFile.uri); if (folder) { const { url } = folder; const fs = fsOperation(Url.join(url, "index.html")); try { if (await fs.exists()) { filename = "index.html"; extension = "html"; pathName = url; start(); return; } next(); return; } catch (err) { helpers.error(err); return; } } } next(); function next() { if (extension === ".js" || isConsole) startConsole(); else start(); } function startConsole() { runConsole(); start(); } function runConsole() { if (!isConsole) EXECUTING_SCRIPT = activeFile.filename; isConsole = true; target = "inapp"; filename = "console.html"; pathName = `${ASSETS_DIRECTORY}www/`; port = constants.CONSOLE_PORT; } function start() { if (target === "browser") { system.isPowerSaveMode((res) => { if (res) { alert(strings.info, strings["powersave mode warning"]); } else { startServer(); } }, startServer); } else { startServer(); } } function startServer() { webServer?.stop(); webServer = CreateServer(port, openBrowser, onError); webServer.setOnRequestHandler(handleRequest); function onError(err) { if (err === "Server already running") { openBrowser(); } else { ++port; start(); } } } /** * Requests handler * @param {object} req * @param {string} req.requestId * @param {string} req.path */ function handleRequest(req) { const reqId = req.requestId; let reqPath = req.path.substring(1); if (!reqPath || reqPath.endsWith("/")) { reqPath += "index.html"; } const ext = Url.extname(reqPath); let url = null; switch (reqPath) { case CONSOLE_SCRIPT: if ( isConsole || appSettings.value.console === appSettings.CONSOLE_LEGACY ) { url = `${ASSETS_DIRECTORY}/build/console.js`; } else { url = `${DATA_STORAGE}/eruda.js`; } sendFileContent(url, reqId, "application/javascript"); break; case EXECUTING_SCRIPT: { const text = activeFile?.session?.doc?.toString() || ""; sendText(text, reqId, "application/javascript"); break; } case MARKDOWN_STYLE: url = appSettings.value.markdownStyle; if (url) sendFileContent(url, reqId, "text/css"); else sendText("img {max-width: 100%;}", reqId, "text/css"); break; default: sendByExt(); break; } async function sendByExt() { if (isConsole) { if (reqPath === "console.html") { sendText( mustache.render($_console, { CONSOLE_SCRIPT, EXECUTING_SCRIPT, }), reqId, MIMETYPE_HTML, ); return; } if (reqPath === "favicon.ico") { sendIco(ASSETS_DIRECTORY, reqId); return; } } if (activeFile.mode === "single") { if (filename === reqPath) { sendText( activeFile.session?.doc?.toString(), reqId, mimeType.lookup(filename), ); } else { error(reqId); } return; } let url = activeFile.uri; let file = activeFile.SAFMode === "single" ? activeFile : null; if (pathName) { url = Url.join(pathName, reqPath); file = editorManager.getFile(url, "uri"); } else if (!activeFile.uri) { file = activeFile; } // Handle extensionless URLs (e.g., "about" -> "about.html" or "about/index.html") if (!ext && pathName) { // Try exact match first for extensionless files (LICENSE, README, etc.) const exactUrl = Url.join(pathName, reqPath); const exactFs = fsOperation(exactUrl); if (await exactFs.exists()) { sendFile(exactUrl, reqId); return; } // Try path.html const htmlUrl = Url.join(pathName, reqPath + ".html"); const htmlFile = editorManager.getFile(htmlUrl, "uri"); if (htmlFile?.loaded && htmlFile.isUnsaved) { sendHTML(htmlFile.session?.doc?.toString(), reqId); return; } const htmlFs = fsOperation(htmlUrl); if (await htmlFs.exists()) { sendFileContent(htmlUrl, reqId, MIMETYPE_HTML); return; } // Try path/index.html const indexUrl = Url.join(pathName, reqPath, "index.html"); const indexFs = fsOperation(indexUrl); if (await indexFs.exists()) { sendFileContent(indexUrl, reqId, MIMETYPE_HTML); return; } error(reqId); return; } switch (ext) { case ".htm": case ".html": if (file && file.loaded && file.isUnsaved) { sendHTML(file.session?.doc?.toString(), reqId); } else { sendFileContent(url, reqId, MIMETYPE_HTML); } break; case ".md": if (file) { const html = markdownIt({ html: true }) .use(MarkdownItGitHubAlerts) .use(anchor, { slugify: (s) => s .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-"), }) .render(file.session?.doc?.toString()); const doc = mustache.render($_markdown, { html, filename, MARKDOWN_STYLE, }); sendText(doc, reqId, MIMETYPE_HTML); } break; default: if (file && file.loaded && file.isUnsaved) { sendText( file.session?.doc?.toString(), reqId, mimeType.lookup(file.filename), ); } else if (url) { if (reqPath === "favicon.ico") { sendIco(ASSETS_DIRECTORY, reqId); } else { sendFile(url, reqId); } } else { error(reqId); } break; } } } /** * Sends 404 error * @param {string} id */ function error(id) { webServer?.send(id, { status: 404, body: "File not found!", }); } /** * Sends favicon * @param {string} assets * @param {string} reqId */ function sendIco(assets, reqId) { const ico = Url.join(assets, "res/logo/favicon.ico"); sendFile(ico, reqId); } /** * Sends HTML file * @param {string} text * @param {string} id */ function sendHTML(text, id) { const js = ` `; text = text.replace(/><\/script>/g, ' crossorigin="anonymous">'); const part = text.split(""); if (part.length === 2) { text = `${part[0]}${js}${part[1]}`; } else if (//i.test(text)) { text = text.replace("", `${js}`); } else { text = `${js}` + text; } sendText(text, id); } /** * Sends file * @param {string} path * @param {string} id * @returns */ async function sendFile(path, id) { if (isLoading) { queue.push(() => { sendFile(path, id); }); return; } isLoading = true; const protocol = Url.getProtocol(path); const ext = Url.extname(path); const mimetype = mimeType.lookup(ext); if (/s?ftp:/.test(protocol)) { const cacheFile = Url.join( CACHE_STORAGE, protocol.slice(0, -1) + path.hashCode(), ); const fs = fsOperation(path); try { await fs.readFile(); // Because reading the remote file will create cache file path = cacheFile; } catch (err) { error(id); isLoading = false; return; } } else if (protocol === "content:") { path = await new Promise((resolve, reject) => { sdcard.formatUri(path, resolve, reject); }); } else if (!/^file:/.test(protocol)) { const fileContent = await fsOperation(path).readFile(); const tempFileName = path.hashCode(); const tempFile = Url.join(CACHE_STORAGE, tempFileName); if (!(await fsOperation(tempFile).exists())) { await fsOperation(CACHE_STORAGE).createFile(tempFileName, fileContent); } else { await fsOperation(tempFile).writeFile(fileContent); } path = tempFile; } webServer?.send(id, { status: 200, path, headers: { "Content-Type": mimetype, }, }); isLoading = false; const action = queue.splice(-1, 1)[0]; if (typeof action === "function") action(); } /** * Sends file content * @param {string} url * @param {string} id * @param {string} mime * @param {(txt: string) => string} processText * @returns */ async function sendFileContent(url, id, mime, processText) { const fs = fsOperation(url); if (!(await fs.exists())) { error(id); return; } let text = await fs.readFile(appSettings.value.defaultFileEncoding); text = processText ? processText(text) : text; if (mime === MIMETYPE_HTML) { sendHTML(text, id); } else { sendText(text, id, mime); } } /** * Sends text * @param {string} text * @param {string} id * @param {string} mimeType * @param {(txt: string) => string} processText */ function sendText(text, id, mimeType, processText) { webServer?.send(id, { status: 200, body: processText ? processText(text) : text, headers: { "Content-Type": mimeType || "text/html", }, }); } /** * Opens the preview in browser */ function openBrowser() { console.count("openBrowser"); const src = `http://localhost:${port}/${filename}`; if (target === "browser") { system.openInBrowser(src); return; } browser.open(src, isConsole); } } export default run; ================================================ FILE: src/lib/saveFile.js ================================================ import fsOperation from "fileSystem"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; 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 EditorFile from "./editorFile"; import openFolder from "./openFolder"; import appSettings from "./settings"; let saveTimeout; const SELECT_FOLDER = "select-folder"; /** * Saves a file to it's location, if file is new, it will ask for location * @param {EditorFile} file * @param {boolean} [isSaveAs] */ async function saveFile(file, isSaveAs = false) { // If file is loading, return if (file.loading) return; /** * If set, new file needs to be created * @type {string} */ let newUrl; /** * File operation object * @type {fsOperation} */ let fileOnDevice; /** * File name, can be changed by user * @type {string} */ let { filename } = file; /** * If file is new * @type {boolean} */ let isNewFile = false; /** * Encoding of file * @type {string} */ const { encoding } = file; /** * File data * @type {string} */ const data = file.session ? file.session.doc.toString() : ""; /** * File tab bar text element, used to show saving status * @type {HTMLElement} */ const $text = file.tab.querySelector("span.text"); if (!file.uri) { isNewFile = true; } else { isSaveAs = isSaveAs ?? file.readOnly; } if (isSaveAs || isNewFile) { const option = await recents.select( [[SELECT_FOLDER, strings["select folder"], "folder"]], // options "dir", // type strings["select folder"], // title ); if (option === SELECT_FOLDER) { newUrl = await selectFolder(); } else { newUrl = option.val.url; } if (isSaveAs) { filename = await getfilename(newUrl, file.filename); } else { filename = await check(newUrl, file.filename); } // in case if user cancels the dialog if (!filename) return; } if (filename !== file.filename) { file.filename = filename; } $text.textContent = strings.saving + "..."; file.isSaving = true; try { if (isSaveAs || newUrl) { // if save as or new file const fileUri = Url.join(newUrl, file.filename); fileOnDevice = fsOperation(fileUri); if (!(await fileOnDevice.exists())) { await fsOperation(newUrl).createFile(file.filename); } const openedFile = editorManager.getFile(fileUri, "uri"); if (openedFile) openedFile.uri = null; file.uri = fileUri; recents.addFile(fileUri); const folder = openFolder.find(newUrl); if (folder) folder.reload(); } if (!fileOnDevice) { fileOnDevice = fsOperation(file.uri); } if (appSettings.value.formatOnSave) { editorManager.activeFile.markChanged = false; acode.exec("format", false); } await fileOnDevice.writeFile(data, encoding); if (file.location) { recents.addFolder(file.location); } clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { file.isSaving = false; file.isUnsaved = false; if (newUrl) recents.addFile(file.uri); editorManager.onupdate("save-file"); editorManager.emit("update", "save-file"); editorManager.emit("save-file", file); resetText(); }, editorManager.TIMEOUT_VALUE + 100); } catch (err) { helpers.error(err); } resetText(); function resetText() { setTimeout(() => { $text.textContent = file.filename; }, editorManager.TIMEOUT_VALUE); } async function selectFolder() { const dir = await FileBrowser( "folder", strings[`save file${isSaveAs ? " as" : ""}`], ); return dir.url; } async function getfilename(url, name) { let filename = await prompt( strings["enter file name"], name || "", strings["new file"], { match: constants.FILE_NAME_REGEX, required: true, }, ); filename = helpers.fixFilename(filename); if (!filename) return null; return await check(url, filename); } async function check(url, filename) { const pathname = Url.join(url, filename); const fs = fsOperation(pathname); if (!(await fs.exists())) return filename; const action = await select(strings["file already exists"], [ ["overwrite", strings.overwrite], ["newname", strings["enter file name"]], ]); if (action === "newname") { filename = await getfilename(url, filename); } return filename; } } export default saveFile; ================================================ FILE: src/lib/saveState.js ================================================ import { getAllFolds, getScrollPosition, getSelection } from "cm/editorUtils"; import constants from "./constants"; import { addedFolder } from "./openFolder"; import appSettings from "./settings"; export default () => { if (!window.editorManager) return; const filesToSave = []; const folders = []; const { editor, files, activeFile } = editorManager; const { value: settings } = appSettings; files.forEach((file) => { if (file.type !== "editor") return; if (file.id === constants.DEFAULT_FILE_SESSION) return; if (file.SAFMode === "single") return; // Selection per file: // - Active file uses live EditorView selection // - Inactive files use their persisted EditorState selection let cursorPos; if (activeFile?.id === file.id) { cursorPos = getSelection(editor); } else { const sel = file.session?.selection; if (sel) { cursorPos = { ranges: sel.ranges.map((r) => ({ from: r.from, to: r.to })), mainIndex: sel.mainIndex ?? 0, }; } else { cursorPos = null; } } // Scroll per file: // - Active file uses live scroll from EditorView // - Inactive files use lastScrollTop/Left captured on tab switch let scrollTop, scrollLeft; if (activeFile?.id === file.id) { const sp = getScrollPosition(editor); scrollTop = sp.scrollTop; scrollLeft = sp.scrollLeft; } else { scrollTop = typeof file.lastScrollTop === "number" ? file.lastScrollTop : 0; scrollLeft = typeof file.lastScrollLeft === "number" ? file.lastScrollLeft : 0; } const fileJson = { id: file.id, uri: file.uri, type: file.type, filename: file.filename, pinned: file.pinned, isUnsaved: file.isUnsaved, readOnly: file.readOnly, SAFMode: file.SAFMode, deletedFile: file.deletedFile, cursorPos, scrollTop, scrollLeft, editable: file.editable, encoding: file.encoding, render: activeFile?.id === file.id, folds: getAllFolds(file.session), }; if (settings.rememberFiles || fileJson.isUnsaved) filesToSave.push(fileJson); }); if (settings.rememberFolders) { addedFolder.forEach((folder) => { const { url, saveState, title, listState, listFiles } = folder; folders.push({ url, opts: { saveState, name: title, listState, listFiles, }, }); }); } localStorage.files = JSON.stringify(filesToSave); localStorage.folders = JSON.stringify(folders); }; ================================================ FILE: src/lib/searchHistory.js ================================================ /** * Search and Replace History Manager * Manages search/replace history using localStorage */ const HISTORY_KEY = "acode.searchreplace.history"; const MAX_HISTORY_ITEMS = 20; class SearchHistory { constructor() { this.history = this.loadHistory(HISTORY_KEY); this.searchIndex = -1; // Current position in history for search input this.replaceIndex = -1; // Current position in history for replace input this.tempSearchValue = ""; // Temporary storage for current search input this.tempReplaceValue = ""; // Temporary storage for current replace input } /** * Load history from localStorage * @param {string} key Storage key * @returns {Array} History items */ loadHistory(key) { try { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : []; } catch (error) { console.warn("Failed to load search history:", error); return []; } } /** * Save history to localStorage */ saveHistory() { try { localStorage.setItem(HISTORY_KEY, JSON.stringify(this.history)); } catch (error) { console.warn("Failed to save search history:", error); } } /** * Add item to history * @param {string} item Item to add */ addToHistory(item) { if (!item || typeof item !== "string" || item.trim().length === 0) { return; } const trimmedItem = item.trim(); // Remove existing item if present this.history = this.history.filter((h) => h !== trimmedItem); // Add to beginning this.history.unshift(trimmedItem); // Limit history size this.history = this.history.slice(0, MAX_HISTORY_ITEMS); this.saveHistory(); } /** * Get history * @returns {Array} History items */ getHistory() { return [...this.history]; } /** * Clear all history */ clearHistory() { this.history = []; this.saveHistory(); } /** * Navigate up in search history (terminal-like) * @param {string} currentValue Current input value * @returns {string} Previous history item or current value */ navigateSearchUp(currentValue) { if (this.history.length === 0) return currentValue; // Store current value if we're at the beginning if (this.searchIndex === -1) { this.tempSearchValue = currentValue; this.searchIndex = this.history.length - 1; } else if (this.searchIndex > 0) { this.searchIndex--; } return this.history[this.searchIndex] || currentValue; } /** * Navigate down in search history (terminal-like) * @param {string} currentValue Current input value * @returns {string} Next history item or original value */ navigateSearchDown(currentValue) { if (this.history.length === 0 || this.searchIndex === -1) { return currentValue; } this.searchIndex++; // If we've gone past the end, return to original value if (this.searchIndex >= this.history.length) { this.searchIndex = -1; return this.tempSearchValue; } return this.history[this.searchIndex]; } /** * Navigate up in replace history (terminal-like) * @param {string} currentValue Current input value * @returns {string} Previous history item or current value */ navigateReplaceUp(currentValue) { if (this.history.length === 0) return currentValue; // Store current value if we're at the beginning if (this.replaceIndex === -1) { this.tempReplaceValue = currentValue; this.replaceIndex = this.history.length - 1; } else if (this.replaceIndex > 0) { this.replaceIndex--; } return this.history[this.replaceIndex] || currentValue; } /** * Navigate down in replace history (terminal-like) * @param {string} currentValue Current input value * @returns {string} Next history item or original value */ navigateReplaceDown(currentValue) { if (this.history.length === 0 || this.replaceIndex === -1) { return currentValue; } this.replaceIndex++; // If we've gone past the end, return to original value if (this.replaceIndex >= this.history.length) { this.replaceIndex = -1; return this.tempReplaceValue; } return this.history[this.replaceIndex]; } /** * Reset search history navigation */ resetSearchNavigation() { this.searchIndex = -1; this.tempSearchValue = ""; } /** * Reset replace history navigation */ resetReplaceNavigation() { this.replaceIndex = -1; this.tempReplaceValue = ""; } /** * Reset all navigation state */ resetAllNavigation() { this.resetSearchNavigation(); this.resetReplaceNavigation(); } } export default new SearchHistory(); ================================================ FILE: src/lib/secureAdRewardState.js ================================================ function execSystem(action, args = []) { return new Promise((resolve, reject) => { if (!window.cordova?.exec) { reject(new Error("Cordova exec is unavailable.")); return; } cordova.exec(resolve, reject, "System", action, args); }); } export default { async getStatus() { try { const raw = await execSystem("getRewardStatus"); if (!raw) return null; return typeof raw === "string" ? JSON.parse(raw) : raw; } catch (error) { console.warn("Failed to load secure rewarded ad status.", error); return null; } }, async redeem(offerId) { try { const raw = await execSystem("redeemReward", [offerId]); if (!raw) return null; return typeof raw === "string" ? JSON.parse(raw) : raw; } catch (error) { console.warn("Failed to redeem rewarded ad offer.", error); throw error; } }, }; ================================================ FILE: src/lib/selectionMenu.js ================================================ const exec = (command) => { const { editor } = editorManager; editor.execCommand(command); if (command === "selectall") { editor.scrollToRow(Number.POSITIVE_INFINITY); editor.setSelection(true); editor.setMenu(true); } editor.focus(); }; const showCodeActions = async () => { const { editor } = editorManager; if (!editor) return; try { const { showCodeActionsMenu, supportsCodeActions } = await import("cm/lsp"); if (supportsCodeActions(editor)) { await showCodeActionsMenu(editor); } } catch (error) { console.warn("[SelectionMenu] Code actions not available:", error); } }; const items = []; export default function selectionMenu() { return [ item( () => exec("copy"), , "selected", true, ), item(() => exec("cut"), , "selected"), item(() => exec("paste"), , "all"), item( () => exec("selectall"), , "all", true, ), item( (color) => acode.exec("insert-color", color), , "all", ), item( () => showCodeActions(), , "all", true, ), ...items, ]; } /** * * @param {function} onclick function to be called when the item is clicked * @param {string | HTMLElement} text content of the item * @param {'selected'|'all'} mode mode supported by the item * @param {boolean} readOnly whether to show the item in readOnly mode */ selectionMenu.add = (onclick, text, mode, readOnly) => { items.push(item(onclick, text, mode, readOnly)); }; selectionMenu.exec = (command) => { exec(command); }; function item(onclick, text, mode = "all", readOnly = false) { return { onclick, text, mode, readOnly }; } ================================================ FILE: src/lib/settings.js ================================================ import fsOperation from "fileSystem"; import ThemeBuilder from "theme/builder"; 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 lang from "./lang"; import { isDeviceDarkTheme } from "./systemConfiguration"; /** * @typedef {object} fileBrowserSettings * @property {string} showHiddenFiles * @property {string} sortByName */ /** * @typedef {object} searchAndFindSettings * @property {boolean} wrap * @property {boolean} caseSensitive * @property {boolean} regExp */ class Settings { #customTheme = new ThemeBuilder("Custom").toJSON(); #defaultSettings; #oldSettings; #initialized = false; #on = { update: [], "update:after": [], reset: [], }; #searchSettings = { caseSensitive: false, regExp: false, wholeWord: false, }; #fileBrowserSettings = { showHiddenFiles: false, sortByName: true, }; #excludeFolders = [ "**/node_modules/**", "**/bower_components/**", "**/jspm_packages/**", "**/.npm/**", "**/flow-typed/**", "**/vendor/**", "**/composer/**", "**/venv/**", "**/.virtualenv/**", "**/__pycache__/**", "**/.pytest_cache/**", "**/.eggs/**", "**/*.egg-info/**", "**/.git/**", "**/.svn/**", "**/.hg/**", "**/.vscode/**", "**/.idea/**", "**/.vs/**", "**/.project/**", "**/.settings/**", "**/.classpath/**", "**/dist/**", "**/build/**", "**/out/**", "**/target/**", "**/bin/**", "**/obj/**", "**/coverage/**", "**/.nyc_output/**", "**/htmlcov/**", "**/temp/**", "**/tmp/**", "**/.cache/**", "**/logs/**", "**/.sass-cache/**", "**/.DS_Store/**", "**/Thumbs.db/**", ]; #IS_TABLET = innerWidth > 768; QUICKTOOLS_ROWS = 2; QUICKTOOLS_GROUP_CAPACITY = 8; QUICKTOOLS_GROUPS = 2; #QUICKTOOLS_SIZE = this.QUICKTOOLS_GROUP_CAPACITY * // items per group this.QUICKTOOLS_GROUPS * // number of groups this.QUICKTOOLS_ROWS; // number of rows QUICKTOOLS_TRIGGER_MODE_TOUCH = "touch"; QUICKTOOLS_TRIGGER_MODE_CLICK = "click"; OPEN_FILE_LIST_POS_HEADER = "header"; OPEN_FILE_LIST_POS_SIDEBAR = "sidebar"; OPEN_FILE_LIST_POS_BOTTOM = "bottom"; KEYBOARD_MODE_NO_SUGGESTIONS = "NO_SUGGESTIONS"; KEYBOARD_MODE_NO_SUGGESTIONS_AGGRESSIVE = "NO_SUGGESTIONS_AGGRESSIVE"; KEYBOARD_MODE_NORMAL = "NORMAL"; CONSOLE_ERUDA = "eruda"; CONSOLE_LEGACY = "legacy"; PREVIEW_MODE_INAPP = "inapp"; PREVIEW_MODE_BROWSER = "browser"; /**@type {{[key: string]: import('components/settingsPage').SettingsPage}} */ uiSettings = {}; constructor() { this.#defaultSettings = { animation: "system", appTheme: "dark", autosave: 0, fileBrowser: this.#fileBrowserSettings, formatter: {}, prettier: {}, maxFileSize: 12, serverPort: constants.SERVER_PORT, previewPort: constants.PREVIEW_PORT, showConsoleToggler: true, previewMode: this.PREVIEW_MODE_INAPP, disableCache: false, useCurrentFileForPreview: false, host: "localhost", search: this.#searchSettings, lang: "en-us", fontSize: "12px", editorTheme: "one_dark", textWrap: true, softTab: true, tabSize: 2, retryRemoteFsAfterFail: true, linenumbers: true, formatOnSave: false, fadeFoldWidgets: false, autoCorrect: true, openFileListPos: this.OPEN_FILE_LIST_POS_HEADER, quickTools: this.#IS_TABLET ? 0 : 1, quickToolsTriggerMode: this.QUICKTOOLS_TRIGGER_MODE_TOUCH, appFont: "", editorFont: "Roboto Mono", vibrateOnTap: true, fullscreen: false, floatingButton: !this.#IS_TABLET, liveAutoCompletion: true, showPrintMargin: false, printMargin: 80, scrollbarSize: 20, showSpaces: false, confirmOnExit: true, lineHeight: 2, leftMargin: 50, checkFiles: true, checkForAppUpdates: false, desktopMode: false, console: this.CONSOLE_LEGACY, keyboardMode: this.KEYBOARD_MODE_NO_SUGGESTIONS_AGGRESSIVE, rememberFiles: true, rememberFolders: true, diagonalScrolling: false, reverseScrolling: false, scrollSpeed: constants.SCROLL_SPEED_NORMAL, customTheme: this.#customTheme, relativeLineNumbers: false, elasticTabstops: false, rtlText: false, hardWrap: false, useTextareaForIME: false, touchMoveThreshold: Math.round((1 / devicePixelRatio) * 10) / 20, quicktoolsItems: [...Array(this.#QUICKTOOLS_SIZE).keys()], excludeFolders: this.#excludeFolders, defaultFileEncoding: "UTF-8", inlineAutoCompletion: true, colorPreview: true, maxRetryCount: 3, showRetryToast: false, showSideButtons: true, showSponsorSidebarApp: true, showAnnotations: false, lintGutter: true, indentGuides: true, rainbowBrackets: true, pluginsDisabled: {}, // pluginId: true/false lsp: { servers: {}, }, developerMode: false, shiftClickSelection: false, }; this.value = structuredClone(this.#defaultSettings); } async init() { if (this.#initialized) return; this.settingsFile = Url.join(DATA_STORAGE, "settings.json"); this.#defaultSettings.appTheme = "system"; this.#defaultSettings.editorTheme = getSystemEditorTheme( isDeviceDarkTheme(), ); this.#initialized = true; const fs = fsOperation(this.settingsFile); if (!(await fs.exists())) { await this.#save(); this.value = structuredClone(this.#defaultSettings); this.#oldSettings = structuredClone(this.#defaultSettings); this.value.lang = navigator.language || "en-us"; return; } const settings = helpers.parseJSON(await fs.readFile("utf8")); if (settings) { // make sure that all the settings are present Object.keys(this.#defaultSettings).forEach((setting) => { const value = settings[setting]; if ( value === undefined || typeof value !== typeof this.#defaultSettings[setting] ) { settings[setting] = this.#defaultSettings[setting]; } }); this.value = structuredClone(settings); this.#oldSettings = structuredClone(settings); try { themes.update(ThemeBuilder.fromJSON(this.value.customTheme)); } catch (error) { themes.update(new ThemeBuilder("Custom").toJSON()); } // Ensure pluginsDisabled exists if (!this.value.pluginsDisabled) this.value.pluginsDisabled = {}; return; } await this.reset(); } async #save() { const fs = fsOperation(this.settingsFile); const settingsText = JSON.stringify(this.value, undefined, 4); if (!(await fs.exists())) { const dirFs = fsOperation(DATA_STORAGE); await dirFs.createFile("settings.json"); } await fs.writeFile(settingsText); this.#oldSettings = structuredClone(this.value); } /** * * @param {Object} [settings] - if provided, the settings will be updated * @param {Boolean} [showToast] - if false, the toast will not be shown * default is true * @param {Boolean} [saveFile] - if false, the settings will not be saved to the file, * default is true */ async update(settings, showToast = true, saveFile = true) { if (typeof settings === "boolean") { showToast = settings; settings = undefined; } const onupdate = [...this.#on.update]; const onupdateAfter = [...this.#on["update:after"]]; if (settings) { Object.keys(settings).forEach((key) => { if (key in this.value) this.value[key] = settings[key]; }); } const changedSettings = this.#getChangedKeys(); changedSettings.forEach((setting) => { this.#applySettings(setting); const listeners = this.#on[`update:${setting}`]; if (Array.isArray(listeners)) { onupdate.push(...listeners); } onupdate.forEach((listener) => listener(this.value[setting])); }); if (saveFile) await this.#save(); if (showToast) toast(strings["settings saved"]); changedSettings.forEach((setting) => { const listeners = this.#on[`update:${setting}:after`]; if (Array.isArray(listeners)) { onupdateAfter.push(...listeners); } onupdateAfter.forEach((listener) => listener(this.value[setting])); }); } async reset(setting) { if (setting) { if (setting in this.#defaultSettings) { this.value[setting] = this.#defaultSettings[setting]; await this.update(); } else { return false; } } else { this.value = this.#defaultSettings; await this.update(false); } this.#on.reset.forEach((onreset) => onreset(this.value)); } /** * Adds a listener for the given event * @param {'update:' | 'update::after' | 'reset'} event * @param {function():void} callback */ on(event, callback) { if (!this.#on[event]) this.#on[event] = []; this.#on[event].push(callback); } /** * Removes the given callback from the given event * @param {'update' | 'reset'} event * @param {function():void} callback */ off(event, callback) { if (!this.#on[event]) this.#on[event] = []; this.#on[event].splice(this.#on[event].indexOf(callback), 1); } /** * Gets a setting with the given key * @param {String} key * @returns */ get(key) { return this.value[key]; } /** * Returns changed settings * @returns {Array} */ #getChangedKeys() { if (!this.#oldSettings) return []; const keys = []; Object.keys(this.#oldSettings).forEach((key) => { const value = this.#oldSettings[key]; if (typeof value === "object") { if (!areEqual(value, this.value[key])) keys.push(key); return; } if (value !== this.value[key]) keys.push(key); }); return keys; } #applySettings(setting) { switch (setting) { case "animation": this.applyAnimationSetting(); break; case "lang": this.applyLangSetting(); break; default: break; } } async applyAnimationSetting() { let value = this.value.animation; if (value === "system") { const res = await new Promise((resolve, reject) => { system.getGlobalSetting("animator_duration_scale", resolve, reject); }); if (res) value = "yes"; else value = "no"; } if (value === "yes") { app.classList.remove("no-animation"); } else if (value === "no") { app.classList.add("no-animation"); } } async applyLangSetting() { const value = this.value.lang; lang.set(value); } } /** * Checks whether given objects are equal or not * @param {Object} obj1 * @param {Object} obj2 * @returns */ function areEqual(obj1, obj2) { if (obj1 === obj2) return true; if (obj1 == null || obj2 == null) return false; if (obj1.constructor !== obj2.constructor) return false; for (let key in obj1) { if (!obj2.hasOwnProperty(key)) return false; if (obj1[key] === obj2[key]) continue; if (typeof obj1[key] !== "object") return false; if (!areEqual(obj1[key], obj2[key])) return false; } return true; } export default new Settings(); ================================================ FILE: src/lib/showFileInfo.js ================================================ import fsOperation from "fileSystem"; import box from "dialogs/box"; import { filesize } from "filesize"; import mustache from "mustache"; import helpers from "utils/helpers"; import Url from "utils/Url"; import $_fileInfo from "views/file-info.hbs"; import settings from "./settings"; /** * Shows file info * @param {String} [url] */ export default async function showFileInfo(url) { if (!url) url = editorManager.activeFile.uri; app.classList.add("title-loading"); try { const fs = fsOperation(url); const stats = await fs.stat(); let { name, lastModified, length, type } = stats; length = filesize(length); lastModified = new Date(lastModified).toLocaleString(); const protocol = Url.getProtocol(url); const fileType = type.toLowerCase(); const options = { name: name.slice(0, name.length - Url.extname(name).length), extension: Url.extname(name), lastModified, length, type, lang: strings, showUri: helpers.getVirtualPath(url), isEditor: fileType === "text/plain" || editorManager.activeFile.type === "editor", }; if (editorManager.activeFile.type === "editor") { const value = await fs.readFile(settings.value.defaultFileEncoding); options.lineCount = value.split(/\n+/).length; options.wordCount = value.split(/\s+|\n+/).length; if (/s?ftp:/.test(protocol)) { options.shareUri = Url.join(CACHE_STORAGE, name); const fs = fsOperation(options.shareUri); if (await fs.exists()) { await fs.delete(); } await fsOperation(CACHE_STORAGE).createFile(name, value); } } box("", mustache.render($_fileInfo, options), true).onclick((e) => { const $target = e.target; if ($target instanceof HTMLElement) { const action = $target.getAttribute("action"); if (action === "copy") { cordova.plugins.clipboard.copy($target.textContent); toast(strings["copied to clipboard"]); } } }); } catch (err) { helpers.error(err); } app.classList.remove("title-loading"); } ================================================ FILE: src/lib/startAd.js ================================================ import tag from "html-tag-js"; let adUnitIdBanner = "ca-app-pub-5911839694379275/9157899592"; // Production let adUnitIdInterstitial = "ca-app-pub-5911839694379275/9570937608"; // Production let adUnitIdRewarded = "ca-app-pub-5911839694379275/1633667633"; // Production let initialized = false; export default async function startAd() { if (!IS_FREE_VERSION || !admob) return; if (!initialized) { initialized = true; if (BuildInfo.type === "debug") { adUnitIdBanner = "ca-app-pub-3940256099942544/6300978111"; // Test adUnitIdInterstitial = "ca-app-pub-3940256099942544/1033173712"; // Test adUnitIdRewarded = "ca-app-pub-3940256099942544/5224354917"; // Test } } const consentStatus = await consent.getConsentStatus(); if (consentStatus === consent.ConsentStatus.Required) { await consent.requestInfoUpdate(); } const formStatus = await consent.getFormStatus(); if (formStatus === consent.FormStatus.Available) { const form = await consent.loadForm(); form.show(); } await admob.start(); const currentHour = new Date().getHours(); //currentHour >= 22: Covers 10:00 PM to 11:59 PM. //currentHour < 4: Covers 12:00 AM to 3:59 AM. const isQuietHours = currentHour >= 22 || currentHour < 4; await admob.configure({ appMuted: isQuietHours, appVolume: isQuietHours ? 0.0 : 1.0, }); const banner = new admob.BannerAd({ adUnitId: adUnitIdBanner, position: "bottom", }); const interstitial = new admob.InterstitialAd({ adUnitId: adUnitIdInterstitial, }); interstitial.load(); interstitial.on("dismiss", () => { interstitial.load(); }); window.ad = banner; window.iad = interstitial; window.adRewardedUnitId = adUnitIdRewarded; } /** * Hides the ad * @param {Boolean} [force=false] */ export function hideAd(force = false) { const { ad } = window; if (ad?.active) { const $pages = tag.getAll(".page-replacement"); const hide = $pages.length === 1; if (force || hide) { ad.active = false; ad.hide(); } } } ================================================ FILE: src/lib/systemConfiguration.js ================================================ export const HARDKEYBOARDHIDDEN_NO = 1; export const HARDKEYBOARDHIDDEN_YES = 2; export const HARDKEYBOARDHIDDEN_UNDEFINED = 0; export const KEYBOARDHIDDEN_NO = 1; export const KEYBOARDHIDDEN_YES = 2; export const KEYBOARDHIDDEN_UNDEFINED = 0; export const KEYBOARD_12KEY = 3; export const KEYBOARD_QWERTY = 2; export const KEYBOARD_UNDEFINED = 0; export const KEYBOARD_NOKEYS = 1; export const NAVIGATIONHIDDEN_NO = 1; export const NAVIGATIONHIDDEN_YES = 2; export const NAVIGATIONHIDDEN_UNDEFINED = 0; export const NAVIGATION_DPAD = 2; export const NAVIGATION_TRACKBALL = 3; export const NAVIGATION_WHEEL = 4; export const NAVIGATION_UNDEFINED = 0; export const ORIENTATION_LANDSCAPE = 2; export const ORIENTATION_PORTRAIT = 1; export const ORIENTATION_SQUARE = 3; export const ORIENTATION_UNDEFINED = 0; export const TOUCHSCREEN_FINGER = 3; export const TOUCHSCREEN_NOTOUCH = 1; export const TOUCHSCREEN_STYLUS = 2; export const TOUCHSCREEN_UNDEFINED = 0; /** * @typedef {Object} SystemConfiguration * @property {number} hardKeyboardHidden * @property {number} navigationHidden * @property {number} keyboardHidden * @property {number} keyboardHeight * @property {number} orientation * @property {number} navigation * @property {number} fontScale * @property {number} keyboard * @property {string} locale */ /** * Get the system configuration * @returns {Promise} */ export function getSystemConfiguration() { return new Promise((resolve, reject) => { cordova.exec(resolve, reject, "System", "get-configuration", []); }); } export function isDeviceDarkTheme() { return window.matchMedia("(prefers-color-scheme: dark)").matches; } ================================================ FILE: src/main.js ================================================ import "core-js/stable"; import "html-tag-js/dist/polyfill"; import "./main.scss"; import "res/icons/style.css"; import "res/file-icons/style.css"; import "styles/overrideAceStyle.scss"; import "styles/wideScreen.scss"; import "lib/polyfill"; import "cm/supportedModes"; import "components/WebComponents"; import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; import ajax from "@deadlyjack/ajax"; import { setKeyBindings } from "cm/commandRegistry"; import { getModeForPath, getModes, getModesByName, initModes, } from "cm/modelist"; import Contextmenu from "components/contextmenu"; import { hasConnectedServers } from "components/lspInfoDialog"; import Sidebar from "components/sidebar"; import { TerminalManager } from "components/terminal"; import tile from "components/tile"; import toast from "components/toast"; import tutorial from "components/tutorial"; import confirm from "dialogs/confirm"; import intentHandler, { processPendingIntents } from "handlers/intent"; import keyboardHandler, { keydownState } from "handlers/keyboard"; import quickToolsInit from "handlers/quickToolsInit"; import windowResize from "handlers/windowResize"; import Acode from "lib/acode"; import actionStack from "lib/actionStack"; import adRewards from "lib/adRewards"; import applySettings from "lib/applySettings"; import checkFiles from "lib/checkFiles"; import checkPluginsUpdate from "lib/checkPluginsUpdate"; import EditorFile from "lib/editorFile"; import EditorManager from "lib/editorManager"; import { initFileList } from "lib/fileList"; import lang from "lib/lang"; import loadPlugins from "lib/loadPlugins"; import Logger from "lib/logger"; import NotificationManager from "lib/notificationManager"; import openFolder, { addedFolder } from "lib/openFolder"; import { registerPrettierFormatter } from "lib/prettierFormatter"; import restoreFiles from "lib/restoreFiles"; import settings from "lib/settings"; import startAd from "lib/startAd"; import mustache from "mustache"; import plugins from "pages/plugins"; import openWelcomeTab from "pages/welcome"; import otherSettings from "settings/appSettings"; import themes from "theme/list"; import { initHighlighting } from "utils/codeHighlight"; import { getEncoding, initEncodings } from "utils/encodings"; import helpers from "utils/helpers"; import loadPolyFill from "utils/polyfill"; import Url from "utils/Url"; import $_fileMenu from "views/file-menu.hbs"; import $_menu from "views/menu.hbs"; import auth, { loginEvents } from "./lib/auth"; const previousVersionCode = Number.parseInt(localStorage.versionCode, 10); window.onload = Main; const logger = new Logger(); function createAceModelistCompatModule() { const toAceMode = (mode) => { const resolved = mode || getModeForPath(""); if (!resolved) return null; const name = resolved.name || "text"; const rawMode = String(resolved.mode || name); const modePath = rawMode.startsWith("ace/mode/") ? rawMode : `ace/mode/${rawMode}`; return { ...resolved, name, caption: resolved.caption || name, mode: modePath, }; }; return { get modes() { return getModes() .map((mode) => toAceMode(mode)) .filter(Boolean); }, get modesByName() { const source = getModesByName(); const result = {}; Object.keys(source).forEach((name) => { result[name] = toAceMode(source[name]); }); return result; }, getModeForPath(path) { return toAceMode(getModeForPath(String(path || ""))); }, }; } function ensureAceCompatApi() { const ace = window.ace || {}; const modelistModule = createAceModelistCompatModule(); const originalRequire = typeof ace.require === "function" ? ace.require.bind(ace) : null; ace.require = (moduleId) => { if (moduleId === "ace/ext/modelist" || moduleId === "ace/ext/modelist.js") { return modelistModule; } return originalRequire?.(moduleId); }; window.ace = ace; } async function Main() { const oldPreventDefault = TouchEvent.prototype.preventDefault; ajax.response = (xhr) => { return xhr.response; }; loadPolyFill.apply(window); TouchEvent.prototype.preventDefault = function () { if (this.cancelable) { oldPreventDefault.bind(this)(); } }; window.addEventListener("resize", windowResize); document.addEventListener("pause", pauseHandler); document.addEventListener("resume", resumeHandler); document.addEventListener("keydown", keyboardHandler); document.addEventListener("deviceready", onDeviceReady); document.addEventListener("backbutton", backButtonHandler); document.addEventListener("menubutton", menuButtonHandler); } async function onDeviceReady() { await initEncodings(); // important to load encodings before anything else const isFreePackage = /(free)$/.test(BuildInfo.packageName); const oldResolveURL = window.resolveLocalFileSystemURL; const { externalCacheDirectory, // externalDataDirectory, cacheDirectory, dataDirectory, } = cordova.file; window.app = document.body; window.root = tag.get("#root"); window.addedFolder = addedFolder; window.editorManager = null; window.toast = toast; window.ASSETS_DIRECTORY = Url.join(cordova.file.applicationDirectory, "www"); window.DATA_STORAGE = externalDataDirectory || dataDirectory; 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); // 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"}`; window.log("error", errorMsg); }); // Capture unhandled promise rejections window.addEventListener("unhandledrejection", (event) => { window.log( "error", `Unhandled rejection: ${event.reason ? event.reason.message : "Unknown reason"}\nStack: ${event.reason ? event.reason.stack : "No stack available"}`, ); }); startAd(); try { await helpers.promisify(iap.startConnection).catch((e) => { window.log("error", "connection error"); window.log("error", e); }); if (localStorage.acode_pro === "true") { window.IS_FREE_VERSION = false; } if (navigator.onLine) { const purchases = await helpers.promisify(iap.getPurchases); const isPro = purchases.find((p) => p.productIds.includes("acode_pro_new"), ); if (isPro) { window.IS_FREE_VERSION = false; } else { window.IS_FREE_VERSION = isFreePackage; } } } catch (error) { window.log("error", "Purchase error"); window.log("error", error); } try { window.ANDROID_SDK_INT = await new Promise((resolve, reject) => system.getAndroidVersion(resolve, reject), ); } catch (error) { window.ANDROID_SDK_INT = Number.parseInt(device.version); } window.DOES_SUPPORT_THEME = (() => { const $testEl = (
          ); document.body.append($testEl); const client = $testEl.getBoundingClientRect(); $testEl.remove(); if (client.height === 0) return false; return true; })(); window.acode = new Acode(); await adRewards.init(); ensureAceCompatApi(); system.requestPermission("android.permission.READ_EXTERNAL_STORAGE"); system.requestPermission("android.permission.WRITE_EXTERNAL_STORAGE"); system.requestPermission("android.permission.POST_NOTIFICATIONS"); const { versionCode } = BuildInfo; if (previousVersionCode !== versionCode) { system.clearCache(); } if (!(await fsOperation(PLUGIN_DIR).exists())) { await fsOperation(DATA_STORAGE).createDirectory("plugins"); } localStorage.versionCode = versionCode; try { await setDebugInfo(); } catch (e) { console.error(e); } acode.setLoadingMessage("Loading settings..."); window.resolveLocalFileSystemURL = function (url, ...args) { oldResolveURL.call(this, Url.safe(url), ...args); }; setTimeout(async () => { if (document.body.classList.contains("loading")) { window.log("warn", "App is taking unexpectedly long time!"); document.body.setAttribute( "data-small-msg", "This is taking unexpectedly long time!", ); } }, 1000 * 10); acode.setLoadingMessage("Loading settings..."); await settings.init(); themes.init(); initHighlighting(); registerPrettierFormatter(); acode.setLoadingMessage("Loading language..."); await lang.set(settings.value.lang); if (settings.value.developerMode) { try { const devTools = (await import("lib/devTools")).default; await devTools.init(false); } catch (error) { console.error("Failed to initialize developer tools", error); } } try { await loadApp(); } catch (error) { window.log("error", error); toast(`Error: ${error.message}`); } finally { setTimeout(async () => { document.body.removeAttribute("data-small-msg"); app.classList.remove("loading", "splash"); // load plugins try { await loadPlugins(); // Ensure at least one sidebar app is active after all plugins are loaded // This handles cases where the stored section was from an uninstalled plugin sidebarApps.ensureActiveApp(); // Re-emit events for active file after plugins are loaded const { activeFile } = editorManager; if (activeFile?.uri) { // Re-emit file-loaded event editorManager.emit("file-loaded", activeFile); // Re-emit switch-file event editorManager.emit("switch-file", activeFile); } } catch (error) { window.log("error", "Failed to load plugins!"); window.log("error", error); toast("Failed to load plugins!"); } applySettings.afterRender(); // Check login status before emitting events try { const isLoggedIn = await auth.isLoggedIn(); if (isLoggedIn) { loginEvents.emit(); } } catch (error) { console.error("Error checking login status:", error); toast("Error checking login status"); } }, 500); } await promptUpdateCheckConsent(); // Check for app updates if (settings.value.checkForAppUpdates && navigator.onLine) { cordova.plugin.http.sendRequest( "https://api.github.com/repos/Acode-Foundation/Acode/releases/latest", { method: "GET", responseType: "json", }, (response) => { const release = response.data; // assuming version is in format v1.2.3 const latestVersion = release.tag_name .replace("v", "") .split(".") .map(Number); const currentVersion = BuildInfo.version.split(".").map(Number); const hasUpdate = latestVersion.some( (num, i) => num > currentVersion[i], ); if (hasUpdate) { acode.pushNotification( "Update Available", `Acode ${release.tag_name} is now available! Click here to checkout.`, { icon: "update", type: "warning", action: () => { system.openInBrowser(release.html_url); }, }, ); } }, (err) => { window.log("error", "Failed to check for updates"); window.log("error", err); }, ); } checkPluginsUpdate() .then((updates) => { if (!updates.length) return; acode.pushNotification( "Plugin Updates", `${updates.length} plugin${updates.length > 1 ? "s" : ""} ${updates.length > 1 ? "have" : "has"} new version${updates.length > 1 ? "s" : ""} available.`, { icon: "extension", action: () => { plugins(updates); }, }, ); }) .catch(console.error); } async function setDebugInfo() { const { version, versionCode } = BuildInfo; const userAgent = navigator.userAgent; const language = navigator.language; // Extract Android version const androidMatch = userAgent.match(/Android\s([0-9.]+)/); const androidVersion = androidMatch ? androidMatch[1] : "Unknown"; // Extract Chrome/WebView version const chromeMatch = userAgent.match(/Chrome\/([0-9.]+)/); const webviewVersion = chromeMatch ? chromeMatch[1] : "Unknown"; const info = [ `App: v${version} (${versionCode})`, `Android: ${androidVersion}`, `WebView: ${webviewVersion}`, `Language: ${language}`, ].join("\n"); document.body.setAttribute("data-version", info); } async function promptUpdateCheckConsent() { try { if (Boolean(localStorage.getItem("checkForUpdatesPrompted"))) return; if (settings.value.checkForAppUpdates) { localStorage.setItem("checkForUpdatesPrompted", "true"); return; } const message = strings["prompt update check consent message"]; const shouldEnable = await confirm(strings?.confirm, message); localStorage.setItem("checkForUpdatesPrompted", "true"); if (shouldEnable) { await settings.update({ checkForAppUpdates: true }, false); } } catch (error) { console.error("Failed to prompt for update check consent", error); } } async function loadApp() { let $mainMenu; let $fileMenu; const $editMenuToggler = ( ); const $navToggler = ( ); const $menuToggler = ( ); const $header = tile({ type: "header", text: "Acode", lead: $navToggler, tail: $menuToggler, }); const $main =
          ; const $sidebar = ; const $runBtn = ( acode.exec("run")} oncontextmenu={() => acode.exec("run-file")} /> ); const $floatingNavToggler = ( acode.exec("toggle-sidebar")} /> ); const $headerToggler = ( ); const folders = helpers.parseJSON(localStorage.folders); const files = helpers.parseJSON(localStorage.files) || []; const editorManager = await EditorManager($header, $main); const setMainMenu = () => { if ($mainMenu) { $mainMenu.removeEventListener("click", handleMenu); $mainMenu.destroy(); } const { openFileListPos, fullscreen } = settings.value; if (openFileListPos === settings.OPEN_FILE_LIST_POS_BOTTOM && fullscreen) { $mainMenu = createMainMenu({ bottom: "6px", toggler: $menuToggler }); } else { $mainMenu = createMainMenu({ top: "6px", toggler: $menuToggler }); } $mainMenu.addEventListener("click", handleMenu); }; const setFileMenu = () => { if ($fileMenu) { $fileMenu.removeEventListener("click", handleMenu); $fileMenu.destroy(); } const { openFileListPos, fullscreen } = settings.value; if (openFileListPos === settings.OPEN_FILE_LIST_POS_BOTTOM && fullscreen) { $fileMenu = createFileMenu({ bottom: "6px", toggler: $editMenuToggler }); } else { $fileMenu = createFileMenu({ top: "6px", toggler: $editMenuToggler }); } $fileMenu.addEventListener("click", handleMenu); }; acode.$headerToggler = $headerToggler; window.actionStack = actionStack.windowCopy(); window.editorManager = editorManager; setMainMenu(settings.value.openFileListPos); setFileMenu(settings.value.openFileListPos); actionStack.onCloseApp = () => acode.exec("save-state"); $headerToggler.onclick = function () { root.classList.toggle("show-header"); this.classList.toggle("keyboard_arrow_left"); this.classList.toggle("keyboard_arrow_right"); }; //#region rendering applySettings.beforeRender(); root.appendOuter($header, $main, $floatingNavToggler, $headerToggler); //#endregion //#region Add event listeners initModes(); quickToolsInit(); sidebarApps.init($sidebar); await sidebarApps.loadApps(); editorManager.onupdate = onEditorUpdate; root.on("show", mainPageOnShow); app.addEventListener("click", onClickApp); editorManager.on("rename-file", onFileUpdate); editorManager.on("switch-file", onFileUpdate); editorManager.on("file-loaded", onFileUpdate); navigator.app.overrideButton("menubutton", true); system.setIntentHandler(intentHandler, intentHandler.onError); system.getCordovaIntent(intentHandler, intentHandler.onError); setTimeout(showTutorials, 1000); settings.on("update:openFileListPos", () => { setMainMenu(); setFileMenu(); }); settings.on("update:fullscreen", () => { setMainMenu(); setFileMenu(); }); $sidebar.onshow = () => { const activeFile = editorManager.activeFile; if (activeFile) editorManager.editor.contentDOM.blur(); }; sdcard.watchFile(KEYBINDING_FILE, async () => { await setKeyBindings(editorManager.editor); toast(strings["key bindings updated"]); }); //#endregion const notificationManager = new NotificationManager(); notificationManager.init(); window.log("info", "Started app and its services..."); // Show welcome tab on first launch, otherwise create default file const isFirstLaunch = Number.isNaN(previousVersionCode); if (isFirstLaunch) { openWelcomeTab(); } else { new EditorFile(); } // load theme plugins try { await loadPlugins(true); } catch (error) { window.log("error", "Failed to load theme plugins!"); window.log("error", error); toast("Failed to load theme plugins!"); } acode.setLoadingMessage("Loading folders..."); if (Array.isArray(folders)) { for (const folder of folders) { folder.opts.listFiles = !!folder.opts.listFiles; openFolder(folder.url, folder.opts); } } if (Array.isArray(files) && files.length) { try { await restoreFiles(files); } catch (error) { window.log("error", "File loading failed!"); window.log("error", error); toast("File loading failed!"); } finally { // Mark restoration complete even after a partial failure so // switch-file persistence and queued intents are not blocked. sessionStorage.setItem("isfilesRestored", true); } // Process any pending intents that were queued before files were restored await processPendingIntents(); } else { // Even when no files need to be restored, mark as restored and process pending intents sessionStorage.setItem("isfilesRestored", true); await processPendingIntents(); onEditorUpdate(undefined, false); } initFileList(); TerminalManager.restorePersistedSessions().catch((error) => { console.error("Terminal restoration failed:", error); }); /** * * @param {MouseEvent} e */ function handleMenu(e) { const $target = e.target; const action = $target.getAttribute("action"); const value = $target.getAttribute("value") || undefined; if (!action) return; if ($mainMenu.contains($target)) $mainMenu.hide(); if ($fileMenu.contains($target)) $fileMenu.hide(); acode.exec(action, value); } function onEditorUpdate(mode, saveState = true) { const { activeFile } = editorManager; // if (!$editMenuToggler.isConnected) { // $header.insertBefore($editMenuToggler, $header.lastChild); // } if (activeFile?.type === "page" || activeFile?.type === "terminal") { $editMenuToggler.remove(); } else { if (!$editMenuToggler.isConnected) { $header.insertBefore($editMenuToggler, $header.lastChild); } } if (mode === "switch-file") { if (settings.value.rememberFiles && activeFile) { localStorage.setItem("lastfile", activeFile.id); } if (saveState && sessionStorage.getItem("isfilesRestored") === "true") { acode.exec("save-state"); } return; } if (saveState) acode.exec("save-state"); } async function onFileUpdate() { try { const { serverPort, previewPort } = settings.value; let canRun = false; if (serverPort !== previewPort) { canRun = true; } else { const { activeFile } = editorManager; canRun = await activeFile?.canRun(); } if (canRun) { $header.insertBefore($runBtn, $header.lastChild); } else { $runBtn.remove(); } } catch (error) { $runBtn.removeAttribute("run-file"); $runBtn.remove(); } } } function onClickApp(e) { let el = e.target; if (el instanceof HTMLAnchorElement || checkIfInsideAnchor()) { e.preventDefault(); e.stopPropagation(); system.openInBrowser(el.href); } function checkIfInsideAnchor() { const allAs = [...document.body.getAll("a")]; for (const a of allAs) { if (a.contains(el)) { el = a; return true; } } return false; } } function mainPageOnShow() { const { editor } = editorManager; // TODO : Codemirror //editor.resize(true); } function createMainMenu({ top, bottom, toggler }) { return Contextmenu({ right: "6px", top, bottom, toggler, transformOrigin: top ? "top right" : "bottom right", innerHTML: () => { return mustache.render($_menu, strings); }, }); } function createFileMenu({ top, bottom, toggler }) { const $menu = Contextmenu({ top, bottom, toggler, transformOrigin: top ? "top right" : "bottom right", innerHTML: () => { const file = window.editorManager.activeFile; if (file.type === "page") { return ""; } if (file.loading) { $menu.classList.add("disabled"); } else { $menu.classList.remove("disabled"); } const { label: encoding } = getEncoding(file.encoding); const isEditorFile = file.type === "editor"; const cmEditor = window.editorManager?.editor; const hasSelection = !!cmEditor && !cmEditor.state.selection.main.empty; return mustache.render($_fileMenu, { ...strings, toggle_pin_tab_text: file.pinned ? strings["unpin tab"] || "Unpin tab" : strings["pin tab"] || "Pin tab", toggle_pin_tab_icon: file.pinned ? "icon pin-off" : "icon pin", // Use CodeMirror mode stored on EditorFile (set in setMode) file_mode: isEditorFile ? file.currentMode || "" : "", file_encoding: isEditorFile ? encoding : "", file_read_only: !file.editable, file_on_disk: !!file.uri, file_eol: isEditorFile ? file.eol : "", copy_text: isEditorFile ? hasSelection : false, is_editor: isEditorFile, has_lsp_servers: isEditorFile && hasConnectedServers(), }); }, }); return $menu; } function showTutorials() { if (window.innerWidth > 750) { tutorial("quicktools-tutorials", (hide) => { const onclick = () => { otherSettings(); hide(); }; return (

          Quicktools has been disabled because it seems like you are on a bigger screen and probably using a keyboard. To enable it,{" "} click here {" "} or press Ctrl + Shift + P and search for{" "} quicktools.

          ); }); } } function backButtonHandler() { if (keydownState.esc) { keydownState.esc = false; return; } actionStack.pop(); } function menuButtonHandler() { const { acode } = window; acode?.exec("toggle-sidebar"); } function pauseHandler() { const { acode } = window; acode?.exec("save-state"); } function resumeHandler() { adRewards.handleResume(); if (!settings.value.checkFiles) return; checkFiles(); } ================================================ FILE: src/main.scss ================================================ @use "./styles/page.scss"; @use "./styles/list.scss"; @use "./styles/mixins.scss"; @use "./styles/keyframes.scss"; @use "./styles/fileInfo.scss"; @use "./styles/markdown.scss"; @use "./styles/codemirror.scss"; :root { --scrollbar-width: 4px; --app-font-family: "Roboto", sans-serif; } * { margin: 0; padding: 0; &:focus { outline: none; } } html { overflow: auto; } html, body { width: 100%; height: 100%; font-size: 14px; } body { user-select: none; font-family: var(--app-font-family); -webkit-tap-highlight-color: transparent; background-color: #9999ff; background-color: var(--primary-color); color: #252525; color: var(--secondary-text-color); &.no-animation * { animation: none !important; transition: none !important; box-shadow: none !important; } &:not(.loading).title-loading { &.title-loading-hide { &::after { background-image: none; transform: translateX(-50%) translateY(-100%) scale3d(0.5, 0.5, 1); opacity: 0; animation: hide-loader 100ms ease-in 1; } } &::after { content: ""; background-color: #3333ff; background-color: var(--primary-color); border-radius: 50%; position: fixed; height: 40px; width: 40px; top: 6px; left: 50%; transform: translateX(-50%); background-image: url(res/tail-spin.svg); background-repeat: no-repeat; background-position: center; background-size: 30px; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2); box-shadow: 0 0 4px 0 var(--box-shadow-color); border: solid 1px transparent; border: solid 1px var(--popup-border-color); animation: appear 100ms ease-out 1; box-sizing: border-box; z-index: 999; } } .main { position: relative; } } a { color: #615efd; color: var(--link-text-color); } .open-file-list { position: relative; height: 30px; width: 100%; background-color: #9999ff; background-color: var(--primary-color); overflow-x: auto !important; overflow-y: hidden !important; display: flex; flex-direction: row !important; color: white; color: var(--primary-text-color); z-index: 5; li.tile { $width: 120px; height: 100%; overflow: hidden; font-size: 0.8em; align-items: center; margin: 0; padding: 0; color: inherit; min-width: $width; min-width: var(--file-tab-width); max-width: $width; max-width: var(--file-tab-width); .text { display: inline-block; white-space: nowrap; max-width: $width; max-width: var(--file-tab-width); overflow: hidden; text-overflow: ellipsis; margin: 0; padding: 0; color: inherit; } &.notice { &::before { content: "•"; color: #ffda0c; font-size: 1.5em; margin-left: 2.5px; text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5); } } &.active { border-top: solid 2px gold; background-color: rgba(0, 0, 0, 0.2); } .file, .icon { height: 24px; width: 24px; font-size: 1em; background-size: 22px; background-position: center; color: inherit; } } } a.icon { pointer-events: all !important; color: white; &:focus, &:active { border: none; outline: none; } } .no-scroll { &::-webkit-scrollbar { width: 0px; height: 0px; } } .list, .prompt, .scroll { &::-webkit-scrollbar { width: var(--scrollbar-width); height: var(--scrollbar-width); } &::-webkit-scrollbar-track { background: transparent; } &::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.333); background: var(--scrollbar-color); border-radius: calc(var(--scrollbar-width) / 2); } } .icon { user-select: none; display: flex; align-items: center; justify-content: center; border-radius: 50%; text-decoration: none; text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background-position: center; background-size: 24px; background-repeat: no-repeat; &.hidden { display: none !important; } &.color { display: flex; &::before { content: ""; height: 16px; width: 16px; border: solid 1px #a90000; border: solid 1px var(--active-color); background-color: currentColor; color: inherit !important; } &.dark { color: #252525; } &.light { color: #ffffff; } } &.notice { @include mixins.icon-badge; } &.angularjs::before { content: "\e92f"; color: #dd0031; } &.html::before { content: "\e939"; color: #e34f26; } &.disabled { opacity: 0.6; pointer-events: none; } &.dull { opacity: 0.6; } &:focus { border: rgba(0, 0, 0, 0.1); } &:not(.floating):active { transition: all 100ms ease; background-color: rgba(0, 0, 0, 0.2) !important; background-color: var(--active-icon-color) !important; } &.active { background-color: rgba(0, 0, 0, 0.2) !important; background-color: var(--active-icon-color) !important; } &.no-icon { max-width: 5px; margin-right: 5px; border-radius: 0; } &.letters::before { content: attr(data-letters); text-transform: uppercase; font-size: 0.6em; font-weight: bolder; } } .mask { position: fixed; left: 0; top: 0; display: block; height: 100vh; width: 100vw; background-color: black; opacity: 0; } footer { &.button-container, .button-container { overflow-x: auto; .section { max-width: 100%; min-width: 100%; .icon.active { @include mixins.active-icon; } } background-color: #9999ff; background-color: var(--primary-color); color: white; color: var(--primary-text-color); } } .section, .button-container { display: flex; min-height: 40px; background-color: inherit; color: inherit; user-select: none; width: 100%; &.primary { button { color: white !important; color: var(--button-text-color) !important; background-color: #39f !important; background-color: var(--button-background-color) !important; box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); box-shadow: 0 0 4px var(--box-shadow-color); border-radius: 4px; &:active { background-color: #2c8ef0 !important; background-color: var(--button-active-color) !important; box-shadow: 0 0 2px rgba(0, 0, 0, 0.4); box-shadow: inset 0 0 2px var(--box-shadow-color); } } } &.disabled { pointer-events: none; .icon, input, button { opacity: 0.6; } } >button { flex: 1; display: inline-flex; align-items: center; justify-content: center; border: none; text-transform: uppercase; background-color: inherit; color: inherit; * { pointer-events: none; } &.disabled { pointer-events: none; opacity: 0.6; } &:active { transition: all 100ms ease; box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2); box-shadow: inset 0 0 4px var(--box-shadow-color); } &:disabled { opacity: 0.6; } } textarea, input { flex: 2; height: auto; color: inherit; border-bottom: 1px solid currentColor; margin: 5px; background-color: inherit; &::placeholder { color: rgba(255, 255, 255, 0.6); } } .icon { color: inherit; font-size: 1.3em; } .search, .save { font-size: 1em; } } input { height: 40px; outline: none; border: none; background-color: inherit; border-bottom: solid 1px #252525; border-bottom: solid 1px var(--secondary-text-color); padding: 0; box-sizing: border-box; color: #252525; color: var(--secondary-text-color); caret-color: currentColor; text-indent: 10px; &:focus { border-bottom-color: #a90000 !important; border-bottom-color: var(--active-color) !important; } } input, textarea { &::placeholder { color: inherit; opacity: 0.8; } } .search-status { flex: 1; display: flex; color: white; color: var(--primary-text-color); align-items: center; justify-content: center; span:not(:nth-child(2)) { margin: 0 5px; color: white; color: var(--primary-text-color); } } .cursor-menu { position: absolute; top: 0; left: 0; height: 40px; background-color: #ffffff; background-color: var(--secondary-color); display: flex; border-radius: 4px; box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); box-shadow: 0 0 8px var(--box-shadow-color); border: none; border: solid 1px var(--popup-border-color); color: #252525; color: var(--secondary-text-color); transform-origin: left center; z-index: 4; >span, >div { display: inline-flex; align-items: center; justify-content: center; height: 100%; font-size: 0.9em; min-width: 50px; color: inherit; user-select: none; white-space: nowrap; &.disabled { opacity: 0.6; pointer-events: none; } } } .file { display: flex; align-items: center; justify-content: center; background-repeat: no-repeat; background-position: 6px center; background-size: 18px; width: 30px; height: 30px; } .hr { display: flex; align-items: center; margin: auto auto 15px auto; &::after, &::before { content: ""; height: 1px; width: 60px; background-color: #252525; background-color: var(--secondary-text-color); margin: auto 15px; opacity: 0.5; } } .d-none { display: none !important; } .floating.icon { position: fixed; height: 50px; width: 50px; font-size: 1.6rem; border: 1px solid; background-color: #9999ff; background-color: var(--primary-color); top: 10px; right: 10px; opacity: 0.2; box-sizing: border-box; color: white; color: var(--primary-text-color); transition: all 300ms ease; box-shadow: -5px 5px 20px 0px rgba(0, 0, 0, 0.5); &:active { transition: all 100ms ease; box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.5); } &.hide { opacity: 0 !important; } } button { cursor: pointer; &.floating.icon { z-index: 1; opacity: 1; &:disabled { opacity: 0.2; } } } #social-links { position: relative; height: 60px; font-size: 1.2em; width: 100%; text-align: center; &::after { display: block; width: 100%; content: attr(title); text-align: center; font-size: 0.5em; text-transform: none; } a { display: inline-flex; min-height: 40px; min-width: 40px; text-shadow: 0 0 1px white; &.github { color: black; } } } #header-toggler { display: none; top: 10px; right: 10px; z-index: 1; height: 40px; width: 40px; } #sidebar-toggler { display: none; top: 10px; left: 10px; z-index: 1; height: 40px; width: 40px; } #quicktools-toggler { top: auto; bottom: 10px; right: 10px; z-index: 1; } .sake { animation: sake 3s ease-out infinite; } .flex-center { display: flex; align-items: center; justify-content: center; } .link { text-decoration: underline; } .w-resize { cursor: w-resize; } .note { margin: 20px 0; .note-title { background-color: rgba(0, 0, 0, 0.2); display: flex; align-items: center; justify-content: center; height: 30px; text-transform: uppercase; .icon { margin: 0 10px; } } p { padding: 10px; box-sizing: border-box; opacity: 0.8; font-size: 0.9rem; } } input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-decoration { -webkit-appearance: none; } .tab-page-container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; } .tab-page-content { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow: auto; } .notification-toast-container { position: absolute; bottom: 20px; left: 10px; right: 10px; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; z-index: 1000; pointer-events: none; >* { pointer-events: auto; } .notification-toast { padding: 12px; border-radius: 6px; background: var(--secondary-color); min-width: 200px; max-width: min(400px, calc(100vw - 40px)); display: flex; gap: 12px; align-items: flex-start; box-shadow: 0 4px 12px var(--box-shadow-color); animation: toastSlideIn 0.3s ease-out; transition: all 0.3s ease; border: 1px solid var(--border-color); word-break: break-word; white-space: normal; box-sizing: border-box; &.hiding { transform: translateX(120%); opacity: 0; } .close-icon { cursor: pointer; font-size: 14px; color: var(--secondary-text-color); margin-left: auto; &:hover { color: var(--button-background-color); } } .notification-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--primary-text-color); } .notification-content { flex: 1; min-width: 0; .notification-title { font-size: 13px; font-weight: 500; margin-bottom: 4px; color: var(--primary-text-color); display: flex; justify-content: space-between; align-items: center; } .notification-message { font-size: 12px; color: var(--secondary-text-color); line-height: 1.4; overflow-wrap: break-word; hyphens: auto; } } &.success { .notification-icon { color: #48c158; } } &.warning { .notification-icon { color: var(--danger-text-color); } } &.error { .notification-icon { color: var(--error-text-color); } } &.info { .notification-icon { color: var(--primary-text-color); } } } @media (max-width: 768px) { align-items: stretch; .notification-toast { min-width: auto; max-width: 100%; } } } @keyframes toastSlideIn { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } ================================================ FILE: src/pages/about/about.js ================================================ import "./about.scss"; 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 { hideAd } from "lib/startAd"; import helpers from "utils/helpers"; export default function AboutInclude() { const $page = Page(strings.about.capitalize()); const webviewVersionName = Reactive("N/A"); const webviewPackageName = Reactive("N/A"); $page.classList.add("about-us"); $page.body = (

          Acode editor

          Version {BuildInfo.version} ({BuildInfo.versionCode})
          ); system.getWebviewInfo((res) => { webviewPackageName.value = res?.packageName || "N/A"; webviewVersionName.value = res?.versionName || "N/A"; }); actionStack.push({ id: "about", action: $page.hide, }); $page.onhide = function () { actionStack.remove("about"); hideAd(); }; app.append($page); helpers.showAd(); } ================================================ FILE: src/pages/about/about.scss ================================================ #about-page { padding: 16px; display: flex; flex-direction: column; align-items: center; gap: 24px; overflow-y: auto; .version-info { text-align: center; margin-bottom: 32px; .version-title { font-size: 24px; font-weight: 600; margin-bottom: 4px; } .version-number { color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); font-size: 14px; } } .info-section { width: 100%; max-width: 400px; min-height: fit-content; height: auto; background: color-mix(in srgb, var(--popup-background-color) 20%, transparent); border-radius: 12px; overflow: visible; margin-bottom: 16px; display: flex; flex-direction: column; .info-item { display: flex; align-items: center; padding: 16px; color: var(--secondary-text-color); text-decoration: none; border-bottom: 1px solid var(--border-color); transition: background 0.2s ease; &:last-child { border-bottom: none; } &:hover { background: color-mix(in srgb, var(--popup-background-color) 40%, transparent); } .info-item-icon { width: 24px; height: 24px; margin-right: 12px; display: flex; align-items: center; font-size: 24px; } .info-item-text { flex: 1; font-size: 15px; .info-item-subtext { color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); font-size: 13px; margin-top: 2px; } } } } .social-links { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; width: 100%; max-width: 400px; .social-link { display: inline-flex; align-items: center; padding: 12px; background: color-mix(in srgb, var(--popup-background-color) 20%, transparent); border-radius: 12px; color: var(--secondary-text-color); text-decoration: none; transition: all 0.2s ease; &:hover { background: color-mix(in srgb, var(--popup-background-color) 40%, transparent); transform: translateY(-1px); } .social-icon { width: 24px; height: 24px; margin-right: 12px; font-size: 24px; display: flex; align-items: center; } } } .icon { height: 100%; width: 100%; } .foxbiz { background-image: url(./foxbiz.svg); } .discord { background-image: url(./discord.svg); } } ================================================ FILE: src/pages/about/index.js ================================================ //jshint ignore:start function About() { import(/* webpackChunkName: "about" */ "./about").then((res) => { res.default(); }); } export default About; ================================================ FILE: src/pages/adRewards/adRewards.scss ================================================ #ad-rewards-page { padding: 16px; max-width: 600px; margin: 0 auto; color: var(--primary-text-color); .reward-hero { margin-bottom: 20px; .hero-copy { margin-bottom: 16px; .eyebrow { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 6px; background: color-mix(in srgb, var(--active-color) 15%, transparent); color: var(--active-color); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; } h1 { margin: 10px 0 6px; font-size: 1.2rem; font-weight: 700; line-height: 1.3; } p { margin: 0; font-size: 0.85rem; color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); line-height: 1.5; } } .reward-status { padding: 12px 14px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; background: color-mix(in srgb, var(--primary-color) 10%, transparent); border-radius: 10px; border: 1px solid var(--border-color); transition: all 0.2s ease; &.is-active { border-color: color-mix(in srgb, var(--active-color) 50%, transparent); background: color-mix(in srgb, var(--active-color) 10%, transparent); } .status-label { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: color-mix(in srgb, var(--primary-text-color) 55%, transparent); } .status-value { font-size: 0.9rem; font-weight: 700; flex: 1; min-width: 0; } .status-note { font-size: 0.78rem; color: color-mix(in srgb, var(--primary-text-color) 55%, transparent); width: 100%; } .status-subnote { font-size: 0.74rem; color: color-mix(in srgb, var(--primary-text-color) 50%, transparent); width: 100%; } } } .reward-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 20px; } .reward-offer { background: color-mix(in srgb, var(--popup-background-color) 20%, transparent); border-radius: 12px; border: 1px solid var(--border-color); padding: 14px; display: flex; flex-direction: column; gap: 8px; transition: border-color 0.2s ease; &.is-focus { border-color: color-mix(in srgb, var(--link-text-color) 50%, transparent); } &.is-upgrade { border-color: color-mix(in srgb, var(--button-background-color) 40%, transparent); } .offer-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; } .offer-kicker { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: color-mix(in srgb, var(--primary-text-color) 55%, transparent); } h2 { margin: 3px 0 0; font-size: 0.88rem; font-weight: 600; } .offer-duration { padding: 3px 8px; border-radius: 6px; background: var(--primary-color); color: var(--primary-text-color); font-size: 0.68rem; font-weight: 600; white-space: nowrap; align-self: flex-start; } p { margin: 0; color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); line-height: 1.45; font-size: 0.8rem; flex: 1; } .offer-limit { font-size: 0.74rem; line-height: 1.4; color: color-mix(in srgb, var(--primary-text-color) 55%, transparent); } } .offer-action { appearance: none; border: 0; border-radius: 8px; padding: 9px 12px; font: inherit; font-size: 0.82rem; font-weight: 600; background: var(--button-background-color); color: var(--button-text-color); cursor: pointer; transition: all 0.2s ease; &:active { transform: translateY(1px); opacity: 0.9; } &:disabled { opacity: 0.45; cursor: not-allowed; } &.secondary { background: var(--primary-color); color: var(--primary-text-color); } } .reward-notes { display: grid; gap: 10px; .note-card { padding: 12px 14px; border-left: 3px solid var(--active-color); border-radius: 0 8px 8px 0; background: color-mix(in srgb, var(--primary-color) 8%, transparent); h3 { margin: 0 0 4px; font-size: 0.82rem; font-weight: 600; } p { margin: 0; line-height: 1.45; font-size: 0.78rem; color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); } } } } @media (max-width: 400px) { #ad-rewards-page .reward-grid { grid-template-columns: 1fr; } } ================================================ FILE: src/pages/adRewards/index.js ================================================ import "./adRewards.scss"; import Page from "components/page"; import loader from "dialogs/loader"; import actionStack from "lib/actionStack"; import adRewards from "lib/adRewards"; import removeAds from "lib/removeAds"; import { hideAd } from "lib/startAd"; import helpers from "utils/helpers"; let $rewardPage = null; export default function openAdRewardsPage() { if ($rewardPage) { $rewardPage.show?.(); return $rewardPage; } const $page = Page("Ad-free passes"); function render() { const rewardState = adRewards.getState(); const rewardedSupported = adRewards.isRewardedSupported(); const unavailableReason = adRewards.getRewardedUnavailableReason(); const offers = adRewards.getOffers(); const isBusy = adRewards.isWatchingReward(); const redemptionStatus = adRewards.canRedeemNow(); const rewardDisabledReason = !rewardedSupported ? unavailableReason : !redemptionStatus.ok ? redemptionStatus.reason : ""; $page.body = (
          Rewarded ads

          Trade a short ad break for focused coding time.

          Unlock temporary ad-free time without leaving the free version. When your pass expires, Acode will show a toast and add a notification in-app.

          {rewardState.isActive ? "Ad-free active" : "No active pass"}
          {rewardState.isActive ? adRewards.getRemainingLabel() : "Watch a rewarded ad to start a pass"}
          {rewardState.isActive ? `Expires ${adRewards.getExpiryLabel()}` : "Passes stack on top of any active rewarded time."}
          {rewardState.redemptionsToday}/{rewardState.maxRedemptionsPerDay}{" "} rewards used today
          {offers.map((offer) => (
          {offer.adsRequired} rewarded ad {offer.adsRequired > 1 ? "s" : ""}

          {offer.title}

          {offer.durationLabel}

          {offer.description}

          {rewardDisabledReason || `${rewardState.remainingRedemptions} of ${rewardState.maxRedemptionsPerDay} rewards left today`}
          ))}
          Permanent option

          Remove ads for good

          One purchase

          If you use Acode daily, Pro still gives the cleanest experience.

          How it works

          Rewarded passes hide your banners and interstitials until the timer ends. If you already have time left, new rewards extend the expiry.

          Limits

          You can redeem up to {rewardState.maxRedemptionsPerDay} rewards per day, and your active ad-free pass is capped at 10 hours.

          ); } async function purchaseRemoveAds() { try { loader.showTitleLoader(); await removeAds(); $page.hide(); } catch (error) { helpers.error(error); } finally { loader.removeTitleLoader(); } } async function watchOffer(offerId) { try { render(); await adRewards.watchOffer(offerId); } catch (error) { helpers.error(error); } finally { render(); } } const unsubscribe = adRewards.onChange(() => { if ($page.isConnected) { render(); } }); $page.onhide = () => { unsubscribe(); actionStack.remove("ad-rewards"); helpers.showAd(); $rewardPage = null; }; actionStack.push({ id: "ad-rewards", action: $page.hide, }); hideAd(true); render(); app.append($page); $rewardPage = $page; return $page; } ================================================ FILE: src/pages/changelog/changelog.js ================================================ import "./style.scss"; import fsOperation from "fileSystem"; import Contextmenu from "components/contextmenu"; import Page from "components/page"; import toast from "components/toast"; import DOMPurify from "dompurify"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; import { hideAd } from "lib/startAd"; import markdownIt from "markdown-it"; import markdownItFootnote from "markdown-it-footnote"; import markdownItTaskLists from "markdown-it-task-lists"; import helpers from "utils/helpers"; export default async function Changelog() { const GITHUB_API_URL = "https://api.github.com/repos/Acode-Foundation/Acode/releases"; const CHANGELOG_FILE_URL = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/CHANGELOG.md"; const currentVersion = BuildInfo.version; let selectedVersion = currentVersion; let selectedStatus = "current"; const versionIndicatorRef = Ref(); const versionTextRef = Ref(); const body = Ref(); const versionSelector = (
          {selectedVersion}
          ); const $page = Page(strings["changelog"], { tail: versionSelector, }); const versionSelectorMenu = Contextmenu({ top: "36px", right: "5px", toggler: versionSelector, transformOrigin: "top right", onclick: menuClickHandler, innerHTML: () => { return `
        • Current Version (${currentVersion})
        • Latest Release
        • Beta Version
        • Full Changelog
        • `; }, }); const changelogMd = await import("../../../CHANGELOG.md"); toast("Loading changelog..."); loadVersionChangelog(); body.onref = () => renderChangelog(changelogMd.default); $page.body =
          ; app.append($page); helpers.showAd(); $page.onhide = function () { actionStack.remove("changelog"); hideAd(); }; actionStack.push({ id: "changelog", action: $page.hide, }); async function loadLatestRelease() { try { const releases = await fsOperation(`${GITHUB_API_URL}/latest`).readFile( "json", ); selectedVersion = releases.tag_name.replace("v", ""); selectedStatus = "latest"; updateVersionSelector(); return renderChangelog(releases.body); } catch (error) { toast("Failed to load latest release notes"); renderChangelog(changelogMd.default); } } async function loadBetaRelease() { try { const releases = await fsOperation(GITHUB_API_URL).readFile("json"); const betaRelease = releases.find((r) => r.prerelease); if (!betaRelease) { body.content =
          No beta release found
          ; return; } selectedVersion = betaRelease.tag_name.replace("v", ""); selectedStatus = "prerelease"; updateVersionSelector(); return renderChangelog(betaRelease.body); } catch (error) { toast("Failed to load beta release notes"); renderChangelog(changelogMd.default); } } async function loadFullChangelog() { try { const changeLogText = await fsOperation(CHANGELOG_FILE_URL).readFile("utf8"); const cleanedText = changeLogText.replace(/^#\s*Change\s*Log\s*\n*/i, ""); selectedVersion = "Changelogs.md"; selectedStatus = "current"; updateVersionSelector(); return renderChangelog(cleanedText); } catch (error) { toast("Failed to load full changelog"); renderChangelog(changelogMd.default); } } async function loadVersionChangelog() { try { const releases = await fsOperation(GITHUB_API_URL).readFile("json"); const currentRelease = releases.find( (r) => r.tag_name.replace("v", "") === currentVersion, ); selectedVersion = currentVersion; selectedStatus = "current"; updateVersionSelector(); if (currentRelease) { return renderChangelog(currentRelease.body); } else { return loadLatestRelease(); } } catch (error) { toast("Failed to load version changelog"); renderChangelog(changelogMd.default); } } function renderChangelog(text) { const md = markdownIt({ html: true, linkify: true }); const REPO_URL = "https://github.com/Acode-Foundation/Acode"; let processedText = text // Convert full PR URLs to #number format with links preserved in markdown .replace( /https:\/\/github\.com\/Acode-Foundation\/Acode\/pull\/(\d+)/g, `[#$1](${REPO_URL}/pull/$1)`, ) // Convert existing #number references to links if they aren't already .replace(/(? { const Changelog = res.default; Changelog(); }); } export default plugin; ================================================ FILE: src/pages/changelog/style.scss ================================================ #changelog { max-width: 800px; margin: auto; overflow: auto; padding: 0 1rem; } .changelog-version-selector { display: flex; align-items: center; gap: 8px; background-color: var(--popup-background-color); border: none; border-radius: 8px; padding: 8px 16px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; margin-right: 6px; &:hover { background-color: var(--secondary-color); } } .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } .status-latest { background-color: #10b981; } .status-prerelease { background-color: var(--danger-color); } .status-current { background-color: var(--active-icon-color); } .loading { display: flex; justify-content: center; align-items: center; height: 100%; color: var(--primary-text-color); font-size: 1.2em; animation: pulse 1.5s ease-in-out infinite; } @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } ================================================ FILE: src/pages/customTheme/customTheme.js ================================================ import "./customTheme.scss"; import Page from "components/page"; import color from "dialogs/color"; import confirm from "dialogs/confirm"; import select from "dialogs/select"; import actionStack from "lib/actionStack"; import settings from "lib/settings"; import { hideAd } from "lib/startAd"; import ThemeBuilder from "theme/builder"; import themes from "theme/list"; import { isValidColor } from "utils/color/regex"; import helpers from "utils/helpers"; export default function CustomThemeInclude() { const theme = themes.get("custom"); const $page = Page(`${strings["custom"]} ${strings["theme"]}`.capitalize()); $page.header.append( , , ); render(); app.append($page); helpers.showAd(); actionStack.push({ id: "custom-theme", action: $page.hide, }); $page.onhide = () => { actionStack.remove("custom-theme"); hideAd(); }; $page.addEventListener("click", handleClick); /** * Handle click event * @param {MouseEvent | TouchEvent} e */ async function handleClick(e) { const $target = e.target; if ($target instanceof HTMLElement) { const action = $target.getAttribute("action"); if (action === "set-theme") { try { theme.type = await select(strings["theme type"], [ ["light", strings["light"]], ["dark", strings["dark"]], ]); applyTheme(); } catch (error) { console.warn("Unable to update custom theme type.", error); } return; } if (action === "reset-theme") { const confirmation = await confirm( strings["info"].toUpperCase(), strings["reset warning"], ); if (!confirmation) return; settings.reset("customTheme"); themes.update(ThemeBuilder.fromJSON(settings.value.customTheme)); applyTheme(); render(); } } } function applyTheme() { setTimeout(() => { themes.apply("custom"); }, 300); } function render() { const pascalToNormal = (str) => str.replace(/([A-Z])/g, " $1").toLowerCase(); const customTheme = themes.get("custom"); $page.body = (
          {Object.keys(customTheme.toJSON()) .filter((key) => isValidColor(customTheme[key])) .map((key) => (
          { const newColor = await color(customTheme[key]); customTheme[key] = newColor; e.target.get(".icon").style.color = newColor; }} >
          {pascalToNormal(key)}
          ))}
          ); } } ================================================ FILE: src/pages/customTheme/customTheme.scss ================================================ #custom-theme { overflow: auto; .icon.color::before { content: ''; height: 30px; width: 30px; border: solid 1px rgb(255, 255, 255); border-radius: 50%; box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.2); background-color: currentColor; } } ================================================ FILE: src/pages/customTheme/index.js ================================================ export default async function CustomTheme(...args) { const customTheme = ( await import(/* webpackChunkName: "customTheme" */ "./customTheme") ).default; customTheme(); } ================================================ FILE: src/pages/fileBrowser/add-menu-home.hbs ================================================
        • {{add path}}
        • {{add ftp}}
        • {{add sftp}}
        • ================================================ FILE: src/pages/fileBrowser/add-menu.hbs ================================================
        • {{new file}}
        • {{new folder}}
        • {{new project}}
        • {{import project zip}}
        • ================================================ FILE: src/pages/fileBrowser/fileBrowser.hbs ================================================
          ⓘ {{info}}
          ================================================ FILE: src/pages/fileBrowser/fileBrowser.js ================================================ import "./fileBrowser.scss"; import fsOperation from "fileSystem"; import externalFs from "fileSystem/externalFs"; import Checkbox from "components/checkbox"; import Contextmenu from "components/contextmenu"; import Page from "components/page"; import searchBar from "components/searchbar"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import prompt from "dialogs/prompt"; 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 openFolder from "lib/openFolder"; import projects from "lib/projects"; import recents from "lib/recents"; import remoteStorage from "lib/remoteStorage"; import appSettings from "lib/settings"; import { hideAd } from "lib/startAd"; import mimeTypes from "mime-types"; import mustache from "mustache"; import filesSettings from "settings/filesSettings"; import URLParse from "url-parse"; import helpers from "utils/helpers"; import Url from "utils/Url"; import _addMenu from "./add-menu.hbs"; import _addMenuHome from "./add-menu-home.hbs"; import _template from "./fileBrowser.hbs"; import _list from "./list.hbs"; import util from "./util"; /** * @typedef {{url: String, name: String}} Location */ /** * @typedef Storage * @property {String} name * @property {String} uuid * @property {String} url * @property {'dir'} type * @property {'permission'|'ftp'|'sftp'|'sd'} storageType */ /** * * @param {import('.').BrowseMode} [mode='file'] * @param {string} [info] * @param {boolean} [doesOpenLast] * @returns {Promise} */ function FileBrowserInclude(mode, info, doesOpenLast = true) { mode = mode || "file"; const IS_FOLDER_MODE = ["folder", "both"].includes(mode); const IS_FILE_MODE = ["file", "both"].includes(mode); const storedState = helpers.parseJSON(localStorage.fileBrowserState) || []; /**@type {Array} */ const state = []; /**@type {Array} */ const allStorages = []; let storageList = helpers.parseJSON(localStorage.storageList); if (!Array.isArray(storageList)) storageList = []; let isSelectionMode = false; let selectedItems = new Set(); if (!info) { if (mode !== "both") { info = IS_FOLDER_MODE ? strings["open folder"] : strings["open file"]; } else { info = strings["file browser"]; } } return new Promise((resolve, reject) => { //#region Declaration const $menuToggler = ( ); const $selectionMenuToggler = ( ); const $addMenuToggler = ( ); const $selectionModeToggler = ( ); const $search = ; const $lead = ; const $page = Page(strings["file browser"].capitalize(), { lead: $lead, }); let hideSearchBar = () => {}; const $content = helpers.parseHTML( mustache.render(_template, { type: mode, info, }), ); const $navigation = $content.get(".navigation"); const menuOption = { top: "8px", right: "8px", toggler: $menuToggler, transformOrigin: "top right", }; const $fbMenu = Contextmenu({ innerHTML: () => { return `
        • ${strings.settings.capitalize(0)}
        • ${currentDir.url === "/" ? `
        • ${strings["reset connections"].capitalize(0)}
        • ` : ""}
        • ${strings.reload.capitalize(0)}
        • `; }, ...menuOption, }); const $selectionMenu = Contextmenu({ innerHTML: () => { return `
        • ${strings.compress.capitalize(0)}
        • ${strings.delete.capitalize(0)}
        • `; }, ...((menuOption.toggler = $selectionMenuToggler) && menuOption), }); const $addMenu = Contextmenu({ innerHTML: () => { if (currentDir.url === "/") { return mustache.render(_addMenuHome, { ...strings, }); } else { return mustache.render(_addMenu, strings); } }, ...((menuOption.toggler = $addMenuToggler) && menuOption), }); $selectionMenuToggler.style.display = "none"; const progress = {}; let cachedDir = {}; let currentDir = { url: null, name: null, list: [], scroll: 0, }; /** * @type {HTMLButtonElement} */ let $openFolder; //#endregion actionStack.setMark(); $lead.onclick = close; $content.addEventListener("click", handleClick); $content.addEventListener("contextmenu", handleContextMenu, true); $page.body = $content; $page.header.append( $search, $selectionModeToggler, $addMenuToggler, $menuToggler, $selectionMenuToggler, ); if (IS_FOLDER_MODE) { $openFolder = tag("button", { className: "floating icon check", style: { bottom: "10px", top: "auto", }, disabled: true, onclick() { $page.hide(); resolve({ type: "folder", ...currentDir, }); }, }); $page.append($openFolder); } app.append($page); helpers.showAd(); actionStack.push({ id: "filebrowser", action: close, }); $selectionModeToggler.onclick = function () { isSelectionMode = !isSelectionMode; toggleSelectionMode(isSelectionMode); }; $fbMenu.onclick = function (e) { $fbMenu.hide(); const action = e.target.getAttribute("action"); if (action === "settings") { filesSettings().show(); const onshow = () => { $page.off("show", onshow); reload(); }; $page.on("show", onshow); return; } if (action === "reload") { const { url } = currentDir; if (url in cachedDir) delete cachedDir[url]; reload(); return; } if (action === "refresh") { ftp.disconnect( () => {}, () => {}, ); sftp.close( () => {}, () => {}, ); toast(strings.success); return; } }; $addMenu.onclick = async (e) => { $addMenu.hide(); const $target = e.target; const action = $target.getAttribute("action"); const value = $target.getAttribute("value"); if (!action) return; switch (action) { case "create": { try { const newUrl = await create(value); if (!newUrl) break; const type = value === "file" ? "file" : "folder"; openFolder.add(newUrl, type); reload(); } catch (error) { window.log("error", error); helpers.error(error); } break; } case "import-project-zip": { let zipFile = await new Promise((resolve, reject) => { sdcard.openDocumentFile( (res) => { resolve(res.uri); }, (err) => { reject(err); }, "application/zip", ); }); if (!zipFile) break; const loadingLoader = loader.create( strings["loading"], "Importing zip file...", { timeout: 10000 }, ); try { const zipContent = await fsOperation(zipFile).readFile(); const zip = await JSZip.loadAsync(zipContent); const targetDir = currentDir.url; const targetFs = fsOperation(targetDir); // Create folder with zip name const zipName = Url.basename(zipFile).replace(/\.zip$/, ""); const extractDir = Url.join(targetDir, zipName); await targetFs.createDirectory(zipName); const files = Object.keys(zip.files); const total = files.length; let current = 0; for (const filePath of files) { const file = zip.files[filePath]; current++; loadingLoader.setMessage( `Extracting ${filePath} (${Math.round((current / total) * 100)}%)`, ); if (file.dir) { await fsOperation(extractDir).createDirectory(filePath); } else { const content = await file.async("arraybuffer"); await fsOperation(extractDir).createFile(filePath, content); } } loadingLoader.destroy(); toast(strings.success); reload(); } catch (err) { loadingLoader.destroy(); helpers.error(err); } break; } case "add-path": addStorage(); break; case "addFtp": case "addSftp": { const storage = await remoteStorage[action](); updateStorage(storage); break; } default: break; } }; $selectionMenu.onclick = async (e) => { $selectionMenu.hide(); const $target = e.target; const action = $target.getAttribute("action"); if (!action) return; switch (action) { case "compress": if (currentDir.url === "/") { break; } const zip = new JSZip(); let loadingLoader = loader.create( strings["loading"], "Compressing files", { timeout: 3000, }, ); try { for (const url of selectedItems) { const fs = fsOperation(url); const stats = await fs.stat(); const isDir = stats.isDirectory; if (isDir) { const addDirToZip = async (dirUrl, zipFolder) => { const entries = await fsOperation(dirUrl).lsDir(); for (const entry of entries) { const percent = ( ((entries.length - entries.indexOf(entry)) / entries.length) * 100 ).toFixed(0); loadingLoader.setMessage( `Compressing ${entry.name.length > 20 ? entry.name.substring(0, 20) + "..." : entry.name} (${percent}%)`, ); if (entry.isDirectory) { const newZipFolder = zipFolder.folder(entry.name); await addDirToZip(entry.url, newZipFolder); } else { const content = await fsOperation(entry.url).readFile(); zipFolder.file(entry.name, content, { binary: true }); } } }; await addDirToZip(url, zip.folder(Url.basename(url))); } else { const content = await fs.readFile(); zip.file(Url.basename(url), content); } } const zipContent = await zip.generateAsync({ type: "arraybuffer", }); const zipName = "archive_" + Date.now() + ".zip"; const zipPath = Url.join(currentDir.url, zipName); const shortPath = currentDir.url.length > 40 ? currentDir.url.substring(0, 37) + "..." : currentDir.url; loadingLoader.setMessage(`Saving ${zipName} to ${shortPath}`); await fsOperation(currentDir.url).createFile(zipName, zipContent); loadingLoader.destroy(); toast(strings.success); isSelectionMode = !isSelectionMode; toggleSelectionMode(isSelectionMode); reload(); } catch (err) { loadingLoader.destroy(); toast(strings.error); console.error(err); } break; case "delete": { if (currentDir.url === "/") { break; } // Show confirmation dialog const confirmMessage = selectedItems.size === 1 ? strings["delete entry"].replace( "{name}", Array.from(selectedItems)[0].split("/").pop(), ) : strings["delete entries"].replace( "{count}", selectedItems.size, ); const confirmation = await confirm(strings.warning, confirmMessage); if (!confirmation) break; const loadingDialog = loader.create( strings.loading, strings["deleting items"].replace("{count}", selectedItems.size), { timeout: 3000 }, ); try { for (const url of selectedItems) { if ((await fsOperation(url).stat()).isDirectory) { if (url.startsWith("content://com.termux.documents/tree/")) { const fs = fsOperation(url); const entries = await fs.lsDir(); if (entries.length === 0) { await fs.delete(); } else { const deleteRecursively = async (currentUrl) => { const currentFs = fsOperation(currentUrl); const currentEntries = await currentFs.lsDir(); for (const entry of currentEntries) { if (entry.isDirectory) { await deleteRecursively(entry.url); } else { await fsOperation(entry.url).delete(); } } await currentFs.delete(); }; await deleteRecursively(url); } } else { await fsOperation(url).delete(); } helpers.updateUriOfAllActiveFiles(url); recents.removeFolder(url); } else { const fs = fsOperation(url); await fs.delete(); const openedFile = editorManager.getFile(url, "uri"); if (openedFile) openedFile.uri = null; } recents.removeFile(url); openFolder.removeItem(url); delete cachedDir[url]; } toast(strings.success); reload(); isSelectionMode = false; toggleSelectionMode(false); } catch (err) { loadingDialog.destroy(); helpers.error(err); } finally { loadingDialog.destroy(); } break; } default: break; } }; $search.onclick = function () { const $list = $content.get("#list"); if ($list) searchBar($list, (hide) => (hideSearchBar = hide)); }; $page.onhide = function () { hideSearchBar(); hideAd(); actionStack.clearFromMark(); actionStack.remove("filebrowser"); $content.removeEventListener("click", handleClick); $content.removeEventListener("contextmenu", handleContextMenu); document.removeEventListener("resume", reload); }; if (doesOpenLast && storedState.length) { loadStates(storedState); return; } navigate("/", "/"); function close() { const err = new Error("User cancelled"); Object.defineProperty(err, "code", { value: 0, }); reject(err); $page.hide(); } function updateSelectionCount($count) { if ($count) { $count.textContent = `${selectedItems.size} items selected`; } } function toggleSelectionMode(active) { const $list = $content.get("#list"); if (active) { $list.classList.add("selection-mode"); const $header = tag("div", { className: "selection-header", }); const selectAllCheckbox = Checkbox("", false); const $count = tag("span", { className: "text selection-count", textContent: "0 items selected", }); // Handle select all functionality selectAllCheckbox.onclick = () => { const checked = selectAllCheckbox.checked; const items = $list.querySelectorAll(".tile:not(.selection-header)"); items.forEach((item) => { const checkbox = item.querySelector(".input-checkbox"); if (checkbox) { checkbox.checked = checked; const url = item.querySelector("data-url").textContent; if (checked) { selectedItems.add(url); } else { selectedItems.delete(url); } } }); updateSelectionCount($count); }; $header.append(selectAllCheckbox, $count); $list.insertBefore($header, $list.firstChild); // Add checkboxes to list items $list .querySelectorAll(".tile:not(.selection-header)") .forEach((item) => { const checkbox = Checkbox("", false); checkbox.onclick = () => { const url = item.querySelector("data-url").textContent; if (checkbox.checked) { selectedItems.add(url); } else { selectedItems.delete(url); } updateSelectionCount($count); }; item.prepend(checkbox); }); $addMenuToggler.style.display = "none"; $menuToggler.style.display = "none"; $selectionMenuToggler.style.display = ""; // Disable floating button in selection mode if ($openFolder) { $openFolder.disabled = true; } } else { $list.classList.remove("selection-mode"); $list.querySelector(".selection-header")?.remove(); $list.querySelectorAll(".input-checkbox").forEach((cb) => cb.remove()); selectedItems.clear(); $addMenuToggler.style.display = ""; $menuToggler.style.display = ""; $selectionMenuToggler.style.display = "none"; // Re-enable floating button when exiting selection mode if ($openFolder) { $openFolder.disabled = false; } } } /** * Called when any file folder is clicked * @param {MouseEvent} e * @param {"contextmenu"} [isContextMenu] */ function handleClick(e, isContextMenu) { /** * @type {HTMLElement} */ const $el = e.target; if (isSelectionMode) { const checkbox = $el.closest(".tile")?.querySelector(".input-checkbox"); if (checkbox && !$el.closest(".selection-header")) { checkbox.checked = !checkbox.checked; const url = $el .closest(".tile") .querySelector("data-url").textContent; if (checkbox.checked) { selectedItems.add(url); } else { selectedItems.delete(url); } const $count = $content.querySelector(".selection-count"); updateSelectionCount($count); } return; } let action = $el.getAttribute("action") || $el.dataset.action; if (!action) return; let url = $el.dataset.url; let name = $el.dataset.name || $el.getAttribute("name"); const idOpenDoc = $el.hasAttribute("open-doc"); const uuid = $el.getAttribute("uuid"); const type = $el.getAttribute("type"); const storageType = $el.getAttribute("storageType"); const home = $el.getAttribute("home"); const isDir = ["dir", "directory", "folder"].includes(type); if (!url) { const $url = $el.get("data-url"); if ($url) { url = $url.textContent; } } if (storageType === "notification") { switch (uuid) { case "addstorage": addStorage(); break; default: break; } return; } if (!url && action === "open" && isDir && !idOpenDoc && !isContextMenu) { loader.hide(); util.addPath(name, uuid).then((res) => { const storage = allStorages.find((storage) => storage.uuid === uuid); storage.url = res.uri; storage.name = res.name; name = res.name; updateStorage(storage, false); url = res.uri; folder(); }); return; } if (isContextMenu) action = "contextmenu"; else if (idOpenDoc) action = "open-doc"; switch (action) { case "navigation": folder(); break; case "contextmenu": contextMenuHandler(); break; case "open": if (isDir) folder(); else if (!$el.hasAttribute("disabled")) file(); break; case "open-doc": openDoc(); break; } function folder() { if (home) { navigateToHome(); return; } navigate(url, name); } function navigateToHome() { const navigationArray = []; const dirs = home.split("/"); const { url: parsedUrl, query } = Url.parse(url); let path = ""; for (let dir of dirs) { path = Url.join(path, dir); navigationArray.push({ url: `${Url.join(parsedUrl, path, "")}${query}`, name: dir || name, }); } loadStates(navigationArray); } function file() { $page.hide(); resolve({ type: "file", url, name, }); } async function getShareableUri(fileUrl) { if (!fileUrl) return null; try { const fs = fsOperation(fileUrl); if (/^s?ftp:/.test(fileUrl)) { return fs.localName; } const stat = await fs.stat(); return stat?.url || null; } catch (error) { return null; } } async function contextMenuHandler() { if (appSettings.value.vibrateOnTap) { navigator.vibrate(constants.VIBRATION_TIME); } if ($el.getAttribute("open-doc") === "true") return; const deleteText = currentDir.url === "/" ? strings.remove : strings.delete; const options = [ ["delete", deleteText, "delete"], ["rename", strings.rename, "text_format"], ]; if (/s?ftp/.test(storageType)) { options.push(["edit", strings.edit, "edit"]); } if (helpers.isFile(type)) { options.push(["info", strings.info, "info"]); options.push(["open_with", strings["open with"], "open_in_browser"]); } if (currentDir.url !== "/" && url) { options.push(["copyuri", strings["copy uri"], "copy"]); } const option = await select(strings["select"], options); switch (option) { case "delete": { let deleteFunction = removeFile; let message = strings["delete entry"].replace("{name}", name); if (uuid) { deleteFunction = removeStorage; message = strings["remove entry"].replace("{name}", name); } const confirmation = await confirm(strings.warning, message); if (!confirmation) break; deleteFunction(); break; } case "rename": { let newname = await prompt(strings.rename, name, "text", { match: constants.FILE_NAME_REGEX, }); newname = helpers.fixFilename(newname); if (!newname || newname === name) break; if (uuid) renameStorage(newname); else renameFile(newname); break; } case "edit": { const storage = await remoteStorage.edit( storageList.find((storage) => storage.uuid === uuid), ); if (!storage) break; storage.uuid = uuid; updateStorage(storage); break; } case "info": acode.exec("file-info", url); break; case "copyuri": navigator.clipboard.writeText(url); alert(strings.success, strings["copied to clipboard"]); break; case "open_with": try { const shareableUri = await getShareableUri(url); if (!shareableUri) { toast(strings["no app found to handle this file"]); break; } const mimeType = mimeTypes.lookup(name) || mimeTypes.lookup(shareableUri) || "text/plain"; system.fileAction(shareableUri, name, "VIEW", mimeType, () => { toast(strings["no app found to handle this file"]); }); } catch (error) { console.error(error); toast(strings.error); } break; } } async function renameFile(newname) { if (url.startsWith("content://com.termux.documents/tree/")) { if (helpers.isDir(type)) { alert(strings.warning, strings["rename not supported"]); return; } else { // Special handling for Termux content files const fs = fsOperation(url); try { const content = await fs.readFile(); const newUrl = Url.join(Url.dirname(url), newname); await fsOperation(Url.dirname(url)).createFile(newname, content); await fs.delete(); recents.removeFile(url); recents.addFile(newUrl); const file = editorManager.getFile(url, "uri"); if (file) { file.uri = newUrl; file.filename = newname; } openFolder.renameItem(url, newUrl, newname); toast(strings.success); reload(); return; } catch (err) { window.log("error", err); helpers.error(err); return; } } } const fs = fsOperation(url); try { const newUrl = await fs.renameTo(newname); recents.removeFile(url); recents.addFile(newUrl); const file = editorManager.getFile(url, "uri"); if (file) { file.uri = newUrl; file.filename = newname; } openFolder.renameItem(url, newUrl, newname); toast(strings.success); reload(); } catch (err) { window.log("error", err); helpers.error(err); } } async function removeFile() { try { if (helpers.isDir(type)) { if (url.startsWith("content://com.termux.documents/tree/")) { const fs = fsOperation(url); const entries = await fs.lsDir(); if (entries.length === 0) { await fs.delete(); } else { const deleteRecursively = async (currentUrl) => { const currentFs = fsOperation(currentUrl); const currentEntries = await currentFs.lsDir(); for (const entry of currentEntries) { if (entry.isDirectory) { await deleteRecursively(entry.url); } else { await fsOperation(entry.url).delete(); } } await currentFs.delete(); }; await deleteRecursively(url); } } else { await fsOperation(url).delete(); } helpers.updateUriOfAllActiveFiles(url); recents.removeFolder(url); } else { const fs = fsOperation(url); await fs.delete(); const openedFile = editorManager.getFile(url, "uri"); if (openedFile) openedFile.uri = null; } recents.removeFile(url); openFolder.removeItem(url); toast(strings.success); delete cachedDir[url]; reload(); } catch (err) { window.log("error", err); helpers.error(err); } } function removeStorage() { if (url) { recents.removeFolder(url); recents.removeFile(url); } storageList = storageList.filter((storage) => { if (storage.uuid !== uuid) { return true; } if (storage.url) { const parsedUrl = URLParse(storage.url, true); const keyFile = decodeURIComponent( parsedUrl.query["keyFile"] || "", ); if (keyFile) { fsOperation(keyFile).delete(); } } return false; }); localStorage.storageList = JSON.stringify(storageList); reload(); } function renameStorage(newname) { storageList = storageList.map((storage) => { if (storage.uuid === uuid) storage.name = newname; return storage; }); localStorage.storageList = JSON.stringify(storageList); reload(); } function openDoc() { checkFiles.check = false; sdcard.openDocumentFile( (res) => { res.url = res.uri; resolve({ type: "file", ...res, name: res.filename, mode: "single", }); $page.hide(); }, (err) => { helpers.error(err); }, ); } } function handleContextMenu(e) { handleClick(e, true); } async function listAllStorages() { let hasInternalStorage = true; allStorages.length = 0; if (ANDROID_SDK_INT === 29) { const rootDirName = cordova.file.externalRootDirectory; const testDirName = "Acode_Test_file" + helpers.uuid(); const testDirFs = fsOperation(Url.join(rootDirName, testDirName)); try { await fsOperation(rootDirName).createDirectory(testDirName); await testDirFs.createFile("test" + helpers.uuid()); hasInternalStorage = !!(await testDirFs.lsDir()).length; } catch (error) { console.error(error); } finally { testDirFs.delete(); } } else if (ANDROID_SDK_INT > 29) { hasInternalStorage = false; } if (hasInternalStorage) { util.pushFolder( allStorages, "Internal storage", cordova.file.externalRootDirectory, { uuid: "internal-storage", }, ); } // Check for Terminal Home Directory storage try { const isTerminalInstalled = await Terminal.isInstalled(); if (typeof Terminal !== "undefined" && isTerminalInstalled) { const isTerminalSupported = await Terminal.isSupported(); if (isTerminalSupported && isTerminalInstalled) { const terminalHomeUrl = cordova.file.dataDirectory + "alpine/home"; // Check if this storage is not already in the list const terminalStorageExists = allStorages.find( (storage) => storage.uuid === "terminal-home" || storage.url === terminalHomeUrl, ); if (!terminalStorageExists) { util.pushFolder(allStorages, "Terminal Home", terminalHomeUrl, { uuid: "terminal-home", }); } } } } catch (error) { console.error("Error checking Terminal installation:", error); } try { const res = await externalFs.listStorages(); res.forEach((storage) => { if (storageList.find((s) => s.uuid === storage.uuid)) return; let path; if (storage.path && isStorageManager) { path = "file://" + storage.path; } util.pushFolder(allStorages, storage.name, path || "", { ...storage, storageType: "sd", }); }); } catch (err) { console.warn("Unable to list external storages.", err); } storageList.forEach((storage) => { let url = storage.url || /**@deprecated */ storage["uri"]; util.pushFolder(allStorages, storage.name, url, { storageType: storage.storageType, uuid: storage.uuid, home: storage.home, }); }); if (!allStorages.length) { util.pushFolder(allStorages, strings["add a storage"], "", { storageType: "notification", uuid: "addstorage", }); } if (IS_FILE_MODE) { util.pushFolder(allStorages, "Select document", null, { "open-doc": true, }); } return allStorages; } /** * Gets directory for given url for rendering * @param {String} url * @param {String} name * @returns {Promise<{name: String, url: String, list: [], scroll: Number}>} */ async function getDir(url, name) { const { fileBrowser } = appSettings.value; let list = []; let error = false; if (url in cachedDir) { return cachedDir[url]; } else { if (url === "/") { list = await listAllStorages(); } else { const id = helpers.uuid(); progress[id] = true; const timeout = setTimeout(() => { loader.create(name, strings.loading + "...", { timeout: 10000, callback() { loader.destroy(); navigate("/", "/"); progress[id] = false; }, }); }, 100); const fs = fsOperation(url); try { list = (await fs.lsDir()) ?? []; } catch (err) { if (progress[id]) { helpers.error(err, url); } else { console.error(err); } } error = !progress[id]; delete progress[id]; clearTimeout(timeout); loader.destroy(); } if (error) return null; return { url, name, scroll: 0, list: helpers.sortDir(list, fileBrowser, mode), }; } } /** * Navigates to specific directory * @param {String} url * @param {String} name */ async function navigate(url, name, assignBackButton = true) { if (document.getElementById("search-bar")) { hideSearchBar(); } if (!url) { throw new Error('navigate(url, name): "url" is required.'); } if (!name) { throw new Error('navigate(url, name): "name" is required.'); } if (url === "/") { if (IS_FOLDER_MODE) $openFolder.disabled = true; } else { if (IS_FOLDER_MODE) $openFolder.disabled = false; } const $nav = tag.get(`#${getNavId(url)}`); //If navigate to previous directories, clear the rest navigation if ($nav) { let $topNav; while (($topNav = $navigation.lastChild) !== $nav) { const url = $topNav.dataset.url; actionStack.remove(url); $topNav.remove(); } while (1) { const location = state.slice(-1)[0]; if (!location || location.url === url) break; state.pop(); } localStorage.fileBrowserState = JSON.stringify(state); const dir = await getDir(url, name); if (dir) { render(dir); } return; } const dir = await getDir(url, name); if (dir) { const { url: curl, name: cname } = currentDir; let action; if (doesOpenLast) pushState({ name, url }); if (curl && cname && assignBackButton) { action = () => { navigate(curl, cname, false); }; } pushToNavbar(name, url, action); render(dir); } } /** * @param {"file"|"folder"|"project"} arg */ async function create(arg) { const { url } = currentDir; const alreadyCreated = []; const options = []; let ctUrl = ""; let projectLocation = null; let projectFiles = ""; let projectName = ""; let project = ""; let newUrl; if (arg === "file" || arg === "folder") { let title = strings["enter folder name"]; if (arg === "file") { title = strings["enter file name"]; } let entryName = await prompt(title, "", "filename", { match: constants.FILE_NAME_REGEX, required: true, }); if (!entryName) return; entryName = helpers.fixFilename(entryName); if (arg === "folder") { newUrl = await helpers.createFileStructure(url, entryName, false); } if (arg === "file") { newUrl = await helpers.createFileStructure(url, entryName); } if (!newUrl.created) return; return newUrl.uri; } if (arg === "project") { projects.list().map((project) => { const { name, icon } = project; options.push([name, name, icon]); }); project = await select(strings["new project"], options); loader.create(project, strings.loading + "..."); projectFiles = await projects.get(project).files(); loader.destroy(); projectName = await prompt(strings["project name"], project, "text", { required: true, match: constants.FILE_NAME_REGEX, }); if (!projectName) return; loader.create(projectName, strings.loading + "..."); const fs = fsOperation(url); const files = Object.keys(projectFiles); // All project files newUrl = await fs.createDirectory(projectName); projectLocation = Url.join(url, projectName, "/"); await createProject(files); // Creating project loader.destroy(); return newUrl; } async function createProject(files) { // checking if it's the last file if (!files.length) { reload(); return; } ctUrl = ""; const file = files.pop(); await createFile(file); return await createProject(files); } function createFile(fileUrl) { const paths = fileUrl.split("/"); const filename = paths.pop(); return createDir(projectFiles, fileUrl, filename, paths); } async function createDir(project, fileUrl, filename, paths) { const lclUrl = Url.join(projectLocation, ctUrl); const fs = fsOperation(lclUrl); if (paths.length === 0) { const data = project[fileUrl].replace(/<%name%>/g, projectName); await fs.createFile(filename, data); return; } const name = paths.splice(0, 1)[0]; const toCreate = Url.join(lclUrl, name); if (!alreadyCreated.includes(toCreate)) { await fs.createDirectory(name); alreadyCreated.push(toCreate); } ctUrl += name + "/"; return await createDir(project, fileUrl, filename, paths); } } /** * Pushes a navigation button to navbar * @param {String} id * @param {String} name * @param {String} url */ function pushToNavbar(name, url, action) { if (!url) return; const displayName = name || Url.basename(url) || url; $navigation.append( , ); $navigation.scrollLeft = $navigation.scrollWidth; if (action && !actionStack.has(url)) { actionStack.push({ id: url, action, }); } } /** * Loads up given states * @param {Array} states */ function loadStates(states) { if (!Array.isArray(states) || !states.length) return; const backNavigation = []; const lastState = states.pop(); if (!lastState || !lastState.url) return; const { url } = lastState; const name = lastState.name || Url.basename(url) || url; let { url: lastUrl, name: lastName } = currentDir; while (states.length) { const location = states.splice(0, 1)[0]; if (!location || !location.url) { continue; } const { url, name } = location; let action; if (doesOpenLast) pushState({ name, url }); if (lastUrl && lastName) { backNavigation.push([lastUrl, lastName]); action = () => { const [url, name] = backNavigation.pop(); navigate(url, name, false); }; } pushToNavbar(name, url, action); lastUrl = url; lastName = name; } currentDir = { url: lastUrl, name: lastName }; navigate(url, name); } /** * * @param {String} url */ function getNavId(url) { return `nav_${url.hashCode()}`; } /** * * @param {Storage} storage * @param {Boolean} doesReload */ function updateStorage(storage, doesReload = true) { if (storage.uuid) { storageList = storageList.filter((s) => s.uuid !== storage.uuid); } else { storage.uuid = helpers.uuid(); } if (!storage.type) { storage.type = "dir"; } if (!storage.storageType) { storage.storageType = storage.type; } storageList.push(storage); localStorage.storageList = JSON.stringify(storageList); if (doesReload) reload(); } function render(dir) { const { list, scroll } = dir; const $list = helpers.parseHTML( mustache.render(_list, { msg: strings["empty folder message"], list, }), ); if (document.getElementById("search-bar")) { hideSearchBar(); } const $oldList = $content.get("#list"); if ($oldList) { const { url } = currentDir; if (url && cachedDir[url]) { cachedDir[url].scroll = $oldList.scrollTop; } $oldList.remove(); } $content.append($list); $list.scrollTop = scroll; $list.focus(); currentDir = dir; cachedDir[dir.url] = dir; } function reload() { const { url, name } = currentDir; delete cachedDir[url]; navigate(url, name); } function pushState({ url, name }) { if (!url || !name) return; if (state.find((l) => l.url === url)) return; state.push({ url, name }); localStorage.fileBrowserState = JSON.stringify(state); } /** * Adds a new storage and refresh location */ function addStorage() { util .addPath() .then((res) => { storageList.push(res); localStorage.storageList = JSON.stringify(storageList); reload(); }) .catch((err) => { helpers.error(err); }); } }); } export default FileBrowserInclude; ================================================ FILE: src/pages/fileBrowser/fileBrowser.scss ================================================ #file-browser { data-url { display: none; } .tile { &[storageType="notification"] { background-color: rgb(153, 153, 255); background-color: var(--primary-color); color: rgb(255, 255, 255); color: var(--primary-text-color); height: 40px; text-align: center; .icon { display: none; content: ""; } } .icon { position: relative; color: rgb(65, 85, 133); &[storageType]::after { position: absolute; top: 50%; left: 50%; content: attr(storageType); transform: translate(-50%, -50%); color: rgb(255, 255, 255); font-size: 0.6rem; font-weight: 600; text-transform: uppercase; } &.clearclose { color: currentColor; font-size: 1.2rem; } &.folder { color: rgb(206, 206, 53); &.user-added-storage { color: rgb(53, 101, 206); } } &.code { color: rgb(79, 155, 79); } } &.symlink { .icon { position: relative; color: var(--link-text-color); &::after { content: "🔗"; position: absolute; bottom: 13px; right: 10px; font-size: 0.4em; color: inherit; } } .text { color: var(--link-text-color); &::after { content: " (symlink)"; font-size: 0.8em; color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); } } } } .info { height: 30px; display: flex; align-items: center; justify-content: center; font-size: 0.9rem; background-color: rgba($color: #000000, $alpha: 0.1); } #list { height: calc(100% - 60px); overflow-y: auto; .tile { &[disabled] { .text { opacity: 0.5; } } &[read-only] { .text::after { content: "Read only"; font-size: 0.6em; color: rgb(255, 255, 255); background-color: rgb(62, 100, 138); border-radius: 4px; padding: 5px; margin: auto 15px; } } } } .selection-header { display: flex; align-items: center; padding: 5px; background: var(--primary-color); color: var(--primary-text-color); position: sticky; top: 0; z-index: 1; gap: 10px; .selection-count { font-size: 0.9em; } } } ================================================ FILE: src/pages/fileBrowser/index.js ================================================ import alert from "dialogs/alert"; import openFile from "lib/openFile"; import openFolder, { addedFolder } from "lib/openFolder"; import helpers from "utils/helpers"; import Url from "utils/Url"; /** * @typedef {"file"|"folder"|"both"} BrowseMode * @typedef {{type: 'file' | 'folder', url: String, name: String}} SelectedFile */ /** * * @param {BrowseMode} [mode='file'] Specify file browser mode, value can be 'file', 'folder' or 'both' * @param {string} info A small message to show what's file browser is opened for * @param {boolean} doesOpenLast Should file browser open lastly visited directory? * @param {Array<{name: String, url: String}>} defaultDir Default directory to open. * @returns {Promise} */ function FileBrowser(mode, info, doesOpenLast, ...args) { return new Promise((resolve, reject) => { import(/* webpackChunkName: "fileBrowser" */ "./fileBrowser").then( (res) => { const FileBrowser = res.default; FileBrowser(mode, info, doesOpenLast, ...args) .then(resolve) .catch(reject); }, ); }); } FileBrowser.openFile = (res) => { const { url, name, mode } = res; const createOption = { uri: url, name, render: true, }; if (mode) { createOption.mode = mode; } openFile(url, createOption); }; FileBrowser.openFileError = (err) => { const ERROR = strings.error.toUpperCase(); const message = `${strings["unable to open file"]}. ${helpers.errorMessage(err.code)}`; if (err.code) { alert(ERROR, message); } else if (err.code !== 0) { alert(ERROR, strings["unable to open file"]); } }; FileBrowser.openFolder = async (res) => { const { url, name } = res; const protocol = Url.getProtocol(url); if (protocol === "ftp:") { openFolder(url, { name: name, saveState: false, }); } else { openFolder(url, { name: name, }); } const folder = addedFolder.find((folder) => folder.url === url); folder?.$node?.$title?.click(); }; FileBrowser.openFolderError = (err) => { const ERROR = strings.error.toUpperCase(); const message = `${strings["unable to open folder"]}. ${helpers.errorMessage(err.code)}`; if (err.code) { alert(ERROR, message); } else if (err.code !== 0) { alert(ERROR, strings["unable to open folder"]); } }; FileBrowser.open = (res) => { if (res.type === "folder") { FileBrowser.openFolder(res); return; } FileBrowser.openFile(res); }; FileBrowser.openError = (err) => { FileBrowser.openFileError(err); }; export default FileBrowser; ================================================ FILE: src/pages/fileBrowser/list.hbs ================================================
            {{#list}} {{#.}}
          • {{name}}
            {{url}}
          • {{/.}} {{/list}}
          ================================================ FILE: src/pages/fileBrowser/util.js ================================================ import multiPrompt from "dialogs/multiPrompt"; import helpers from "utils/helpers"; export default { /** * * @param {Array} list * @param {String} name * @param {String} url * @param {Object} extra */ pushFolder(list, name, url, extra = {}) { list.push({ url: url, name: name, isDirectory: true, parent: true, type: "dir", ...extra, }); }, /** * Save a new path using storage access framework * @param {String} name * @returns {Promise<{name: String, uri: String, uuid: string}>} */ async addPath(name, uuid) { const res = await multiPrompt( strings["add path"], [ { id: "uri", placeholder: strings["select folder"], type: "text", required: true, readOnly: true, onclick() { sdcard.getStorageAccessPermission( uuid, (res) => { const $name = tag.get("#name"); if (!$name.value && res) { const name = window .decodeURIComponent(res) ?.split(":") .pop() ?.split("/") .pop(); $name.value = name ?? ""; } this.value = res; }, (err) => { helpers.error(err); }, ); }, }, { id: "name", placeholder: strings["folder name"], type: "text", required: true, value: name ?? "", }, ], "https://acode.app/faqs/224761680", ); if (!res) return; return { name: res.name, uri: res.uri, uuid: helpers.uuid(), }; }, }; ================================================ FILE: src/pages/fontManager/fontManager.js ================================================ import "./style.scss"; import fsOperation from "fileSystem"; import Page from "components/page"; import searchBar from "components/searchbar"; import { DEFAULT_TERMINAL_SETTINGS } from "components/terminal"; import toast from "components/toast"; import box from "dialogs/box"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; import fonts from "lib/fonts"; import appSettings from "lib/settings"; import { hideAd } from "lib/startAd"; import FileBrowser from "pages/fileBrowser"; import { updateActiveTerminals } from "settings/terminalSettings"; import helpers from "utils/helpers"; import Url from "utils/Url"; export default function fontManager() { const defaultEditorFont = "Roboto Mono"; const defaultTerminalFont = DEFAULT_TERMINAL_SETTINGS.fontFamily; const defaultAppFontLabel = strings.default || "Default"; const targetLabels = { app: "App", editor: "Editor", terminal: "Terminal", all: "All", }; const $page = Page(strings.fonts?.capitalize()); const $search = ; const $addFont = ; const list = Ref(); $page.classList.add("font-manager-page"); actionStack.push({ id: "fontManager", action: () => { $page.hide(); $page.removeEventListener("click", clickHandler); }, }); $page.onhide = () => { hideAd(); actionStack.remove("fontManager"); }; $page.body =
          ; $page.querySelector("header").append($search, $addFont); app.append($page); renderFonts(); helpers.showAd(); $page.addEventListener("click", clickHandler); function renderFonts() { const fontNames = fonts.getNames(); let $currentItem; const content = []; const defaultAppliedTargets = getAppliedTargets(""); const $defaultItem = ( chooseApplyTarget("")} /> ); if (defaultAppliedTargets.length) $currentItem = $defaultItem; content.push($defaultItem); fontNames.forEach((fontName) => { const appliedTargets = getAppliedTargets(fontName); const $item = ( chooseApplyTarget(fontName)} onDelete={() => deleteFont(fontName)} /> ); if (!$currentItem && appliedTargets.length) $currentItem = $item; content.push($item); }); list.el.content = content; $currentItem?.scrollIntoView(); } async function clickHandler(e) { const $target = e.target; if (!($target instanceof HTMLElement)) return; const action = $target.getAttribute("action") || $target.dataset.action; if (!action) return; switch (action) { case "search": searchBar(list.el); break; case "add-font": await addNewFont(); break; } } async function addNewFont() { try { const { url, name } = await FileBrowser( "file", "Select font file (.ttf, .otf, .woff)", false, ); // Check if file is a font file const ext = name.toLowerCase().split(".").pop(); if (!["ttf", "otf", "woff", "woff2"].includes(ext)) { toast("Please select a valid font file (.ttf, .otf, .woff)"); return; } const fontName = await prompt( "Font Name", name.replace(/\.(ttf|otf|woff|woff2)$/i, ""), ); if (!fontName) return; // Check if font already exists if (fonts.get(fontName)) { toast("Font with this name already exists"); return; } await addFontFromFile(fontName, url); } catch (error) { if (error.message !== "User cancelled") { toast("Failed to add font: " + error.message); } } } async function addFontFromFile(fontName, fontUrl) { try { // Download the font to local storage first loader.showTitleLoader(); const FONT_DIR = Url.join(DATA_STORAGE, "fonts"); const fontFileName = `${fontName.replace(/[^a-zA-Z0-9]/g, "_")}.ttf`; const FONT_FILE = Url.join(FONT_DIR, fontFileName); // Create fonts directory if it doesn't exist if (!(await fsOperation(FONT_DIR).exists())) { await fsOperation(DATA_STORAGE).createDirectory("fonts"); } // Read and save the font file const fontData = await fsOperation(fontUrl).readFile(); await fsOperation(FONT_DIR).createFile(fontFileName, fontData); // Get internal URI for the saved font const internalUrl = await helpers.toInternalUri(FONT_FILE); // Generate CSS for the font let css = `@font-face { font-family: '${fontName}'; src: url(${internalUrl}) format('truetype'); font-weight: normal; font-style: normal; }`; loader.removeTitleLoader(); // Show CSS preview/edit dialog const editedCSS = await showCSSEditor(css, fontName); if (editedCSS === null) return; // User cancelled // Add the font fonts.addCustom(fontName, editedCSS); renderFonts(); toast(`Font "${fontName}" added successfully`); } catch (error) { loader.removeTitleLoader(); toast("Failed to add font: " + error.message); } } async function showCSSEditor(css, fontName) { return new Promise((resolve) => { const htmlContent = `
          Edit the CSS @font-face rule below:
          `; const dialog = box( `Edit CSS - ${fontName}`, htmlContent, "Save", "Cancel", ) .then((children) => { const textarea = children[0].querySelector(".font-css-editor"); if (textarea) { textarea.focus(); textarea.select(); } }) .ok(() => { const textarea = document.querySelector(".font-css-editor"); const value = textarea ? textarea.value : css; resolve(value); dialog.hide(); }) .cancel(() => { resolve(null); dialog.hide(); }); }); } function getAppliedTargets(fontName) { const appFont = appSettings.value.appFont || ""; const editorFont = appSettings.value.editorFont || defaultEditorFont; const terminalFont = appSettings.value.terminalSettings?.fontFamily || defaultTerminalFont; const appliedTargets = []; if (fontName) { if (appFont === fontName) appliedTargets.push("app"); if (editorFont === fontName) appliedTargets.push("editor"); if (terminalFont === fontName) appliedTargets.push("terminal"); return appliedTargets; } if (!appFont) appliedTargets.push("app"); return appliedTargets; } function getTargetOptionText(fontName, target) { if (fontName) { return `Apply to ${targetLabels[target]}`; } switch (target) { case "app": return "Reset App font"; case "editor": return "Reset Editor font"; case "terminal": return "Reset Terminal font"; case "all": return "Reset all fonts"; default: return "Reset font"; } } async function chooseApplyTarget(fontName) { const title = fontName ? `Apply "${fontName}"` : `${defaultAppFontLabel} font`; const target = await select( title, [ ["app", getTargetOptionText(fontName, "app")], ["editor", getTargetOptionText(fontName, "editor")], ["terminal", getTargetOptionText(fontName, "terminal")], ["all", getTargetOptionText(fontName, "all")], ], true, ).catch(() => null); if (!target) return; await applyFontToTarget(fontName, target); } async function applyFontToTarget(fontName, target) { try { const nextEditorFont = fontName || defaultEditorFont; const nextTerminalFont = fontName || defaultTerminalFont; const nextTerminalSettings = { ...(appSettings.value.terminalSettings || DEFAULT_TERMINAL_SETTINGS), }; const nextSettings = {}; switch (target) { case "app": await fonts.setAppFont(fontName); nextSettings.appFont = fontName; break; case "editor": await fonts.setEditorFont(nextEditorFont); nextSettings.editorFont = nextEditorFont; break; case "terminal": nextTerminalSettings.fontFamily = nextTerminalFont; nextSettings.terminalSettings = nextTerminalSettings; await updateActiveTerminals("fontFamily", nextTerminalFont); break; case "all": await fonts.setAppFont(fontName); await fonts.setEditorFont(nextEditorFont); nextTerminalSettings.fontFamily = nextTerminalFont; await updateActiveTerminals("fontFamily", nextTerminalFont); nextSettings.appFont = fontName; nextSettings.editorFont = nextEditorFont; nextSettings.terminalSettings = nextTerminalSettings; break; default: return; } await appSettings.update(nextSettings, false); toast(getApplyToast(fontName, target)); renderFonts(); } catch (error) { toast("Failed to apply font: " + error.message); } } function getApplyToast(fontName, target) { const label = fontName ? `"${fontName}"` : "default font"; switch (target) { case "app": return `${label} applied to app`; case "editor": return `${label} applied to editor`; case "terminal": return `${label} applied to terminal`; case "all": return `${label} applied to app, editor, and terminal`; default: return "Font applied"; } } async function deleteFont(fontName) { // Don't allow deleting default fonts if (!fonts.isCustom(fontName)) { toast("Cannot delete default fonts"); return; } const shouldDelete = await confirm( "Delete Font", `Are you sure you want to delete "${fontName}"?`, ); if (shouldDelete) { try { const currentEditorFont = appSettings.value.editorFont || defaultEditorFont; const currentAppFont = appSettings.value.appFont || ""; const currentTerminalFont = appSettings.value.terminalSettings?.fontFamily || defaultTerminalFont; const isCurrentEditorFont = fontName === currentEditorFont; const isCurrentAppFont = fontName === currentAppFont; const isCurrentTerminalFont = fontName === currentTerminalFont; // Remove from fonts collection fonts.remove(fontName); // Try to delete the font file from storage const FONT_DIR = Url.join(DATA_STORAGE, "fonts"); const fontFileName = `${fontName.replace(/[^a-zA-Z0-9]/g, "_")}.ttf`; const FONT_FILE = Url.join(FONT_DIR, fontFileName); const fs = fsOperation(FONT_FILE); if (await fs.exists()) { await fs.delete(); } if (isCurrentAppFont) { await fonts.setAppFont(""); } if (isCurrentEditorFont) { await fonts.setEditorFont(defaultEditorFont); } if (isCurrentTerminalFont) { await updateActiveTerminals("fontFamily", defaultTerminalFont); } if (isCurrentAppFont || isCurrentEditorFont || isCurrentTerminalFont) { await appSettings.update( { ...(isCurrentAppFont ? { appFont: "" } : {}), ...(isCurrentEditorFont ? { editorFont: defaultEditorFont } : {}), ...(isCurrentTerminalFont ? { terminalSettings: { ...(appSettings.value.terminalSettings || DEFAULT_TERMINAL_SETTINGS), fontFamily: defaultTerminalFont, }, } : {}), }, false, ); } if (isCurrentAppFont || isCurrentEditorFont || isCurrentTerminalFont) { const restoredTargets = [ isCurrentAppFont ? "app" : null, isCurrentEditorFont ? "editor" : null, isCurrentTerminalFont ? "terminal" : null, ].filter(Boolean); toast( `Font "${fontName}" deleted, restored ${restoredTargets.join(", ")} font defaults`, ); } else { toast(`Font "${fontName}" deleted`); } renderFonts(); } catch (error) { // Font removed from collection even if file deletion fails const currentEditorFont = appSettings.value.editorFont || defaultEditorFont; const currentAppFont = appSettings.value.appFont || ""; const currentTerminalFont = appSettings.value.terminalSettings?.fontFamily || defaultTerminalFont; const isCurrentEditorFont = fontName === currentEditorFont; const isCurrentAppFont = fontName === currentAppFont; const isCurrentTerminalFont = fontName === currentTerminalFont; if (isCurrentAppFont || isCurrentEditorFont || isCurrentTerminalFont) { try { if (isCurrentAppFont) { await fonts.setAppFont(""); } if (isCurrentEditorFont) { await fonts.setEditorFont(defaultEditorFont); } if (isCurrentTerminalFont) { await updateActiveTerminals("fontFamily", defaultTerminalFont); } await appSettings.update( { ...(isCurrentAppFont ? { appFont: "" } : {}), ...(isCurrentEditorFont ? { editorFont: defaultEditorFont } : {}), ...(isCurrentTerminalFont ? { terminalSettings: { ...(appSettings.value.terminalSettings || DEFAULT_TERMINAL_SETTINGS), fontFamily: defaultTerminalFont, }, } : {}), }, false, ); toast(`Font "${fontName}" deleted (file cleanup may have failed)`); } catch (setFontError) { toast( `Font "${fontName}" deleted, but failed to restore a fallback font`, ); } } else { toast(`Font "${fontName}" deleted (file cleanup may have failed)`); } renderFonts(); } } } function FontItem({ name, appliedTargets, subtitle, deletable = true, onSelect, onDelete, }) { const isBuiltIn = name !== defaultAppFontLabel && !fonts.isCustom(name); const isApplied = appliedTargets.length > 0; const resolvedSubtitle = subtitle || (isApplied ? "Applied font" : isBuiltIn ? "Built-in font" : "Custom font"); const $item = (
          {name}
          {resolvedSubtitle}
          {appliedTargets.length || deletable ? (
          {appliedTargets.map((target) => ( {targetLabels[target]} ))} {deletable ? ( ) : null}
          ) : null}
          ); $item.onclick = (e) => { const $target = e.target; const action = $target.dataset.action; if (action === "delete" && deletable) { e.stopPropagation(); onDelete(); } else if ( !$target.classList.contains("font-manager-action") || action === "select-font" ) { onSelect(); } }; return $item; } } ================================================ FILE: src/pages/fontManager/index.js ================================================ export default function fontManager(...args) { import(/* webpackChunkName: "fontManager" */ "./fontManager").then( (module) => { module.default(...args); }, ); } ================================================ FILE: src/pages/fontManager/style.scss ================================================ wc-page.font-manager-page { background: var(--secondary-color); .font-manager-list { display: flex; flex-direction: column; width: 100%; max-width: 48rem; margin: 0 auto; padding: 0.5rem 0 5.5rem; box-sizing: border-box; background: var(--secondary-color); } .font-manager-list > .list-item { display: flex; width: 100%; min-height: 4.1rem; margin: 0; padding: 0.75rem 1rem; box-sizing: border-box; align-items: center; gap: 0.85rem; background: transparent; cursor: pointer; transition: background-color 140ms ease; text-decoration: none; &:not(:last-of-type) { border-bottom: 1px solid var(--border-color); border-bottom: 1px solid color-mix(in srgb, var(--border-color), transparent 20%); } &:focus, &:active { background: color-mix( in srgb, var(--secondary-color), var(--popup-text-color) 4% ); } > .icon:first-child { display: flex; align-items: center; justify-content: center; width: 1.4rem; min-width: 1.4rem; height: 1.4rem; font-size: 1.15rem; color: color-mix(in srgb, var(--secondary-text-color), transparent 18%); } > .container { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: visible; gap: 0.24rem; padding-right: 0.6rem; > .text { display: flex; align-items: center; min-width: 0; font-size: 1rem; line-height: 1.2; font-weight: 600; color: var(--popup-text-color); } > .value { display: block; font-size: 0.82rem; line-height: 1.35; color: color-mix(in srgb, var(--secondary-text-color), transparent 30%); text-transform: none; white-space: normal; overflow: visible; overflow-wrap: anywhere; opacity: 1; } } > .setting-tail { display: flex; align-items: center; justify-content: flex-end; flex-shrink: 0; min-height: 1.65rem; gap: 0.65rem; margin-left: 0.9rem; align-self: center; } } .font-manager-list > .list-item.current-font { background: color-mix(in srgb, var(--secondary-color), var(--active-color) 8%); } .font-manager-list > .list-item.current-font:focus, .font-manager-list > .list-item.current-font:active { background: color-mix(in srgb, var(--secondary-color), var(--active-color) 12%); } .font-manager-list > .list-item.current-font > .icon:first-child { color: color-mix(in srgb, var(--active-color), transparent 10%); } .font-manager-list > .list-item.current-font > .container > .text { font-weight: 700; } @media screen and (min-width: 768px) { .font-manager-list { padding-left: 0.5rem; padding-right: 0.5rem; } } .font-manager-badge { display: inline-flex; align-items: center; justify-content: center; padding: 0.16rem 0.48rem; border-radius: 999px; font-size: 0.82rem; font-weight: 600; line-height: 1.2; background: color-mix(in srgb, var(--secondary-color), var(--active-color) 10%); color: color-mix(in srgb, var(--active-color), transparent 12%); } .font-manager-badge-editor { background: color-mix(in srgb, var(--secondary-color), #4ca3ff 14%); color: color-mix(in srgb, #4ca3ff, white 4%); } .font-manager-badge-terminal { background: color-mix(in srgb, var(--secondary-color), #21b36b 14%); color: color-mix(in srgb, #21b36b, white 4%); } .font-manager-badge-app { background: color-mix(in srgb, var(--secondary-color), var(--active-color) 12%); color: color-mix(in srgb, var(--active-color), transparent 14%); } .font-manager-action { display: inline-flex; align-items: center; justify-content: center; width: 1.2rem; min-width: 1.2rem; height: 1.2rem; font-size: 1.1rem; line-height: 1; color: color-mix(in srgb, var(--secondary-text-color), transparent 28%); cursor: pointer; transition: color 140ms ease; &:hover, &:active { color: var(--error-text-color); } } } .prompt.box { .font-css-editor { &.input { border-bottom: 1px solid var(--border-color); min-height: 120px; margin-top: 5px; &:focus { border-bottom-color: var(--active-color); } } } } ================================================ FILE: src/pages/markdownPreview/index.js ================================================ import "./style.scss"; import fsOperation from "fileSystem"; import Page from "components/page"; import DOMPurify from "dompurify"; import actionStack from "lib/actionStack"; import openFile from "lib/openFile"; import { highlightCodeBlock, initHighlighting } from "utils/codeHighlight"; import { getMarkdownBaseUri, hasMathContent, isExternalLink, isMarkdownPath, renderMarkdown, resolveMarkdownTarget, } from "./renderer"; let previewController = null; let mermaidModulePromise = null; let mermaidThemeSignature = ""; let mathStylesPromise = null; function getThemeColor(name, fallback) { const value = getComputedStyle(document.documentElement) .getPropertyValue(name) .trim(); return value || fallback; } function isDarkColor(color) { const normalized = color.replace(/\s+/g, ""); const match = normalized.match(/^#([0-9a-f]{6})$/i); if (!match) return true; const value = match[1]; const r = Number.parseInt(value.slice(0, 2), 16); const g = Number.parseInt(value.slice(2, 4), 16); const b = Number.parseInt(value.slice(4, 6), 16); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance < 0.5; } function escapeHtml(text) { return String(text ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function getTargetElement(container, targetId) { const decodedId = decodeURIComponent(targetId || ""); if (!decodedId) return null; const elements = container.querySelectorAll("[id], [name]"); return ( Array.from(elements).find( (element) => element.getAttribute("id") === decodedId || element.getAttribute("name") === decodedId, ) || null ); } function getOffsetTopWithinContainer(target, container) { let top = 0; let element = target; while (element && element !== container) { top += element.offsetTop || 0; element = element.offsetParent; } return top; } async function getMermaid() { if (!mermaidModulePromise) { mermaidModulePromise = import("mermaid") .then(({ default: mermaid }) => mermaid) .catch((error) => { mermaidModulePromise = null; throw error; }); } return mermaidModulePromise; } async function ensureMathStyles() { if (!mathStylesPromise) { mathStylesPromise = Promise.all([ import("katex/dist/katex.min.css"), import("markdown-it-texmath/css/texmath.css"), ]).catch((error) => { mathStylesPromise = null; throw error; }); } return mathStylesPromise; } function getMermaidThemeConfig() { const backgroundColor = getThemeColor("--background-color", "#1e1e1e"); const panelColor = getThemeColor("--popup-background-color", "#2a2f3a"); const borderColor = getThemeColor("--border-color", "#4a4f5a"); const primaryTextColor = getThemeColor("--primary-text-color", "#f5f5f5"); const accentColor = getThemeColor("--link-text-color", "#4ba3ff"); const activeColor = getThemeColor("--active-color", accentColor); return { startOnLoad: false, securityLevel: "strict", htmlLabels: false, theme: "base", flowchart: { htmlLabels: false, }, themeVariables: { darkMode: isDarkColor(backgroundColor), background: backgroundColor, mainBkg: panelColor, primaryColor: panelColor, mainContrastColor: primaryTextColor, textColor: primaryTextColor, primaryTextColor, primaryBorderColor: borderColor, lineColor: primaryTextColor, secondaryColor: accentColor, secondaryBorderColor: borderColor, secondaryTextColor: primaryTextColor, tertiaryColor: backgroundColor, tertiaryBorderColor: borderColor, tertiaryTextColor: primaryTextColor, clusterBkg: panelColor, clusterBorder: borderColor, nodeBorder: borderColor, nodeTextColor: primaryTextColor, titleColor: primaryTextColor, defaultLinkColor: activeColor, actorTextColor: primaryTextColor, labelTextColor: primaryTextColor, loopTextColor: primaryTextColor, noteTextColor: primaryTextColor, sectionBkgColor: panelColor, sectionBkgColor2: backgroundColor, sectionTitleColor: primaryTextColor, sequenceNumberColor: primaryTextColor, signalTextColor: primaryTextColor, taskTextColor: primaryTextColor, taskTextDarkColor: primaryTextColor, taskTextOutsideColor: primaryTextColor, edgeLabelBackground: backgroundColor, pieTitleTextColor: primaryTextColor, pieLegendTextColor: primaryTextColor, pieSectionTextColor: primaryTextColor, git0: panelColor, git1: backgroundColor, git2: accentColor, git3: activeColor, }, }; } function initializeMermaid(mermaid) { const config = getMermaidThemeConfig(); const signature = JSON.stringify(config); if (signature === mermaidThemeSignature) return; mermaid.initialize(config); mermaidThemeSignature = signature; } async function copyText(text) { if (cordova?.plugins?.clipboard) { cordova.plugins.clipboard.copy(text); return; } if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } throw new Error("Clipboard API unavailable"); } async function fileToObjectUrl(file) { const fs = fsOperation(file); const fileInfo = await fs.stat(); const binData = await fs.readFile(); return URL.createObjectURL( new Blob([binData], { type: fileInfo.mime || "application/octet-stream" }), ); } function revokeObjectUrls(urls) { urls.forEach((url) => { try { URL.revokeObjectURL(url); } catch (error) { console.warn("Failed to revoke object URL", error); } }); } async function resolveRenderedImages(container, file) { const baseUri = getMarkdownBaseUri(file); const objectUrls = []; const images = Array.from(container.querySelectorAll("img[src]")); images.forEach((image) => { const src = image.getAttribute("src"); if (!src || src.startsWith("data:") || src.startsWith("blob:")) return; if (src.startsWith("#") || isExternalLink(src)) return; if (!image.hasAttribute("data-markdown-local-src")) { image.setAttribute( "data-markdown-local-src", resolveMarkdownTarget(src, baseUri), ); } }); await Promise.all( images.map(async (image) => { const resolvedPath = image.getAttribute("data-markdown-local-src"); if (!resolvedPath) return; try { const objectUrl = await fileToObjectUrl(resolvedPath); image.setAttribute("src", objectUrl); image.setAttribute("data-source-uri", resolvedPath); image.setAttribute("loading", "lazy"); image.setAttribute("decoding", "async"); image.classList.add("markdown-image"); objectUrls.push(objectUrl); } catch (error) { console.warn("Failed to resolve markdown image:", resolvedPath, error); } }), ); return objectUrls; } function createMarkdownPreview(file) { const $page = Page(file.filename); const $content =
          ; $page.body = $content; app.append($page); const previewState = { page: $page, file, content: $content, renderVersion: 0, objectUrls: [], pendingHash: "", disposed: false, }; const removeAction = () => actionStack.remove("markdown-preview"); actionStack.push({ id: "markdown-preview", action: () => $page.hide(), }); $page.onhide = () => { removeAction(); dispose(); }; const onFileChanged = (changedFile) => { if (changedFile?.id !== previewState.file?.id) return; void render(); }; const onFileRenamed = (renamedFile) => { if (renamedFile?.id !== previewState.file?.id) return; previewState.file = renamedFile; $page.settitle(renamedFile.filename); void render(); }; const onFileRemoved = (removedFile) => { if (removedFile?.id !== previewState.file?.id) return; if ($page.isConnected) { $page.hide(); } else { dispose(); } }; previewState.content.addEventListener("click", onContentClick, true); editorManager.on("file-content-changed", onFileChanged); editorManager.on("rename-file", onFileRenamed); editorManager.on("remove-file", onFileRemoved); initHighlighting(); async function onContentClick(event) { const link = event.target.closest("a[href]"); if (!link) return; const originalHref = link.getAttribute("href") || ""; const resolvedHref = link.getAttribute("data-resolved-href") || resolveMarkdownTarget( originalHref, getMarkdownBaseUri(previewState.file), ); event.preventDefault(); event.stopPropagation(); if (originalHref.startsWith("#")) { scrollToHash(originalHref.slice(1)); return; } if (isExternalLink(originalHref)) { system.openInBrowser(originalHref); return; } const hashIndex = resolvedHref.indexOf("#"); const targetPath = hashIndex === -1 ? resolvedHref : resolvedHref.slice(0, hashIndex); const targetHash = hashIndex === -1 ? "" : resolvedHref.slice(hashIndex + 1); if (!targetPath && targetHash) { scrollToHash(targetHash); return; } if (isMarkdownPath(resolvedHref)) { await openFile(targetPath, { render: true }); const nextFile = editorManager.getFile(targetPath, "uri") || editorManager.activeFile; if (nextFile) { await bind(nextFile, targetHash); } return; } $page.hide(); await openFile(targetPath, { render: true }); } function scrollToHash(targetId) { const target = getTargetElement(previewState.content, targetId); if (!target) return; const topOffset = 12; const top = getOffsetTopWithinContainer(target, previewState.content) - topOffset; previewState.content.scrollTo({ top: Math.max(0, top), behavior: "smooth", }); } async function enhanceCodeBlocks(version) { const codeBlocks = Array.from(previewState.content.querySelectorAll("pre")); await Promise.all( codeBlocks.map(async (pre) => { const codeElement = pre.querySelector("code"); if (!codeElement || codeElement.closest(".mermaid-error")) return; const language = codeElement.dataset.language || codeElement.className.match(/language-(\S+)/)?.[1]; if (!language) return; const originalCode = codeElement.textContent || ""; codeElement.classList.add("cm-highlighted"); const highlighted = await highlightCodeBlock(originalCode, language); if ( previewState.disposed || version !== previewState.renderVersion || !codeElement.isConnected ) { return; } if (highlighted && highlighted !== originalCode) { codeElement.innerHTML = DOMPurify.sanitize(highlighted, { ALLOWED_TAGS: ["span"], ALLOWED_ATTR: ["class"], }); } }), ); if (previewState.disposed || version !== previewState.renderVersion) return; codeBlocks.forEach((pre) => { if (pre.querySelector(".copy-button")) return; pre.style.position = "relative"; const copyButton = document.createElement("button"); copyButton.className = "copy-button"; copyButton.textContent = "Copy"; copyButton.addEventListener("click", async (event) => { event.preventDefault(); event.stopPropagation(); try { const code = pre.querySelector("code")?.textContent || ""; await copyText(code); copyButton.textContent = "Copied!"; setTimeout(() => { if (copyButton.isConnected) copyButton.textContent = "Copy"; }, 2000); } catch (error) { console.warn("Failed to copy markdown code block", error); copyButton.textContent = "Failed to copy"; setTimeout(() => { if (copyButton.isConnected) copyButton.textContent = "Copy"; }, 2000); } }); pre.append(copyButton); }); } async function renderMermaidBlocks(version) { const mermaidBlocks = Array.from( previewState.content.querySelectorAll(".mermaid"), ); if (!mermaidBlocks.length) return; const mermaid = await getMermaid(); if (previewState.disposed || version !== previewState.renderVersion) return; initializeMermaid(mermaid); let index = 0; await Promise.all( mermaidBlocks.map(async (block) => { const source = block.textContent || ""; const id = `acode-markdown-mermaid-${Date.now()}-${version}-${index++}`; try { const { svg, bindFunctions } = await mermaid.render(id, source); if ( previewState.disposed || version !== previewState.renderVersion || !block.isConnected ) { return; } const sanitizedSvg = DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true }, ADD_TAGS: ["style"], ADD_ATTR: ["data-et", "data-id", "data-node", "data-zoom", "class"], }); block.innerHTML = sanitizedSvg; bindFunctions?.(block); } catch (error) { if (!block.isConnected) return; block.classList.add("mermaid-error"); block.innerHTML = `
          ${escapeHtml(source)}
          ${escapeHtml(error?.message || "Failed to render Mermaid diagram.")}
          `; } }), ); } async function render() { const version = ++previewState.renderVersion; previewState.page.settitle(previewState.file.filename); revokeObjectUrls(previewState.objectUrls); previewState.objectUrls = []; const markdownText = previewState.file.session?.doc?.toString?.() || ""; const pendingRenderTasks = [ renderMarkdown(markdownText, previewState.file), ]; if (hasMathContent(markdownText)) { pendingRenderTasks.push(ensureMathStyles()); } const [{ html }] = await Promise.all(pendingRenderTasks); if (previewState.disposed || version !== previewState.renderVersion) { return; } previewState.content.innerHTML = DOMPurify.sanitize(html, { FORBID_TAGS: ["style"], ADD_TAGS: ["eq", "eqn"], }); const objectUrls = await resolveRenderedImages( previewState.content, previewState.file, ); if (previewState.disposed || version !== previewState.renderVersion) { revokeObjectUrls(objectUrls); return; } previewState.objectUrls = objectUrls; await enhanceCodeBlocks(version); await renderMermaidBlocks(version); if ( previewState.pendingHash && !previewState.disposed && version === previewState.renderVersion ) { scrollToHash(previewState.pendingHash); previewState.pendingHash = ""; } } async function bind(nextFile, hash = "") { previewState.file = nextFile; previewState.pendingHash = hash; if (!previewState.page.isConnected) { app.append(previewState.page); } await render(); } function dispose() { if (previewState.disposed) return; previewState.disposed = true; previewState.content.removeEventListener("click", onContentClick, true); editorManager.off("file-content-changed", onFileChanged); editorManager.off("rename-file", onFileRenamed); editorManager.off("remove-file", onFileRemoved); revokeObjectUrls(previewState.objectUrls); if (previewController === controller) { previewController = null; } } const controller = { bind, render, page: $page, }; return controller; } export default async function openMarkdownPreview(file, hash = "") { if (!file) return null; if (!previewController || previewController.page?.isConnected === false) { previewController = createMarkdownPreview(file); } await previewController.bind(file, hash); return previewController.page; } ================================================ FILE: src/pages/markdownPreview/renderer.js ================================================ import markdownIt from "markdown-it"; import anchor from "markdown-it-anchor"; import { full as markdownItEmoji } from "markdown-it-emoji"; import markdownItFootnote from "markdown-it-footnote"; import MarkdownItGitHubAlerts from "markdown-it-github-alerts"; import markdownItTaskLists from "markdown-it-task-lists"; import Url from "utils/Url"; const EXTERNAL_LINK_PATTERN = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; const IMAGE_PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=="; const BLOCK_MATH_PATTERN = /(^|[^\\])\$\$[\s\S]+?\$\$/m; const INLINE_MATH_PATTERN = /(^|[^\\])\$(?!\s)(?:\\.|[^$\\\n])*(?:\\[{^_(]|[{^_])(?:\\.|[^$\\\n])*\$(?!\w)/m; const BEGIN_END_MATH_PATTERN = /\\begin\{(?:equation|align|gather|multline|eqnarray)\*?\}[\s\S]*?\\end\{(?:equation|align|gather|multline|eqnarray)\*?\}/m; let mathModulesPromise = null; let mathMarkdownItPromise = null; function slugify(text) { return text .trim() .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\p{L}\p{N}]+/gu, "-") .replace(/^-+|-+$/g, ""); } function escapeAttribute(value = "") { return String(value) .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } function splitLinkTarget(target = "") { const hashIndex = target.indexOf("#"); if (hashIndex === -1) { return { path: target, hash: "" }; } return { path: target.slice(0, hashIndex), hash: target.slice(hashIndex), }; } export function isExternalLink(target = "") { return EXTERNAL_LINK_PATTERN.test(String(target).trim()); } export function isMarkdownPath(target = "") { return /\.md(?:[#?].*)?$/i.test(String(target).trim()); } export function getMarkdownBaseUri(file) { if (!file) return ""; if (file.uri) return file.uri; if (file.location && file.filename) { return Url.join(file.location, file.filename); } return file.location || ""; } export function resolveMarkdownTarget(target = "", baseUri = "") { if (!target || target.startsWith("#") || isExternalLink(target)) { return target; } const { path, hash } = splitLinkTarget(target); if (!path) return target; let resolvedPath = path; if (!path.startsWith("/")) { const baseDir = baseUri ? Url.dirname(baseUri) : ""; if (baseDir) { resolvedPath = Url.join(baseDir, path); } } return `${resolvedPath}${hash}`; } function resolveImageTarget(target = "", baseUri = "") { if ( !target || target.startsWith("#") || target.startsWith("data:") || target.startsWith("blob:") || isExternalLink(target) ) { return null; } const { path } = splitLinkTarget(target); if (!path) return null; let resolvedPath = path; if (!path.startsWith("/")) { const baseDir = baseUri ? Url.dirname(baseUri) : ""; if (baseDir) { resolvedPath = Url.join(baseDir, path); } } return resolvedPath; } function collectTokens(tokens, callback) { for (const token of tokens) { callback(token); if (Array.isArray(token.children) && token.children.length) { collectTokens(token.children, callback); } } } export function hasMathContent(text = "") { return ( BLOCK_MATH_PATTERN.test(text) || INLINE_MATH_PATTERN.test(text) || BEGIN_END_MATH_PATTERN.test(text) ); } async function getKatexAndTexmathModules() { if (!mathModulesPromise) { mathModulesPromise = Promise.all([ import("katex").then(({ default: katex }) => katex), import("markdown-it-texmath").then( ({ default: markdownItTexmath }) => markdownItTexmath, ), ]).then(([katex, markdownItTexmath]) => ({ katex, markdownItTexmath, })); mathModulesPromise = mathModulesPromise.catch((error) => { mathModulesPromise = null; throw error; }); } return mathModulesPromise; } function createMarkdownIt({ katex = null, markdownItTexmath = null } = {}) { const md = markdownIt({ html: true, linkify: true, }); md.use(MarkdownItGitHubAlerts) .use(anchor, { slugify }) .use(markdownItTaskLists) .use(markdownItFootnote); if (katex && markdownItTexmath) { md.use(markdownItTexmath, { engine: katex, delimiters: ["dollars", "beg_end"], katexOptions: { throwOnError: false, strict: "ignore", }, }); } md.use(markdownItEmoji); md.renderer.rules.image = (tokens, idx, options, env, self) => { const token = tokens[idx]; token.attrSet("loading", "lazy"); token.attrSet("decoding", "async"); const src = token.attrGet("src"); if (src && !src.startsWith("data:") && !isExternalLink(src)) { const resolvedPath = resolveImageTarget(src, env.markdownBaseUri); if (resolvedPath) { token.attrSet("data-markdown-local-src", resolvedPath); token.attrSet("src", IMAGE_PLACEHOLDER); } } return self.renderToken(tokens, idx, options); }; md.renderer.rules.fence = (tokens, idx) => { const token = tokens[idx]; const info = (token.info || "").trim(); const language = info.split(/\s+/)[0].toLowerCase(); const escapedCode = md.utils.escapeHtml(token.content || ""); if (language === "mermaid") { return `
          ${escapedCode}
          `; } const className = language ? ` class="language-${escapeAttribute(language)}"` : ""; const dataLanguage = ` data-language="${escapeAttribute(language)}"`; return `
          ${escapedCode}
          `; }; return md; } const baseMarkdownIt = createMarkdownIt(); async function getMarkdownIt(text = "") { if (!hasMathContent(text)) { return baseMarkdownIt; } if (!mathMarkdownItPromise) { mathMarkdownItPromise = getKatexAndTexmathModules() .then(({ katex, markdownItTexmath }) => createMarkdownIt({ katex, markdownItTexmath }), ) .catch((error) => { mathMarkdownItPromise = null; throw error; }); } return mathMarkdownItPromise; } export async function renderMarkdown(text, file) { const markdownText = text || ""; const md = await getMarkdownIt(markdownText); const env = {}; env.markdownBaseUri = getMarkdownBaseUri(file); const tokens = md.parse(markdownText, env); collectTokens(tokens, (token) => { if (token.type === "link_open") { const href = token.attrGet("href"); if (!href || href.startsWith("#") || isExternalLink(href)) return; token.attrSet( "data-resolved-href", resolveMarkdownTarget(href, env.markdownBaseUri), ); } }); return { html: md.renderer.render(tokens, md.options, env), }; } ================================================ FILE: src/pages/markdownPreview/style.scss ================================================ .markdown-preview { overflow: auto; padding: 16px; max-width: 960px; margin: 0 auto; user-select: text; [id] { scroll-margin-top: 12px; } pre { margin: 16px 0; border-radius: 8px; overflow-x: auto; max-width: 100%; } pre code { display: block; padding: 16px; margin: 0; background: transparent !important; white-space: pre; word-wrap: normal; tab-size: 2; user-select: text; -webkit-overflow-scrolling: touch; } code[class*="language-"] { font-size: 13px; line-height: 1.5; } eq { display: inline-block; max-width: 100%; } eqn { display: block; width: 100%; overflow-x: auto; overflow-y: hidden; padding: 8px 0; } section.eqno { display: flex; align-items: center; gap: 12px; overflow-x: auto; } section.eqno > eqn { margin-left: 0; } section.eqno > span { flex: 0 0 auto; opacity: 0.7; } .katex-display { margin: 1rem 0; overflow-x: auto; overflow-y: hidden; padding-bottom: 4px; } .markdown-image { display: block; margin: 1rem auto; max-width: 100%; height: auto; border-radius: 12px; background: color-mix(in srgb, var(--primary-color) 8%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary-color) 12%, transparent); } .mermaid { display: flex; justify-content: center; overflow-x: auto; padding: 12px 0; } .mermaid svg { max-width: 100%; height: auto; } .mermaid svg text, .mermaid svg tspan, .mermaid .label, .mermaid .nodeLabel, .mermaid .edgeLabel, .mermaid .cluster-label, .mermaid .flowchart-label { fill: var(--primary-text-color) !important; color: var(--primary-text-color) !important; } .mermaid svg foreignObject, .mermaid svg foreignObject * { color: var(--primary-text-color) !important; } .mermaid-error { border: 1px solid var(--danger-color); border-radius: 8px; padding: 12px; background: color-mix(in srgb, var(--danger-color) 10%, transparent); } .mermaid-error-message { margin-top: 8px; color: var(--danger-text-color); } } ================================================ FILE: src/pages/plugin/index.js ================================================ function plugin({ id, installed, install }, onInstall, onUninstall) { import(/* webpackChunkName: "plugins" */ "./plugin").then((res) => { const Plugin = res.default; Plugin(id, installed, onInstall, onUninstall, install); }); } export default plugin; ================================================ FILE: src/pages/plugin/plugin.js ================================================ 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 installPlugin from "lib/installPlugin"; import InstallState from "lib/installState"; import settings from "lib/settings"; import { hideAd } from "lib/startAd"; import markdownIt from "markdown-it"; import anchor from "markdown-it-anchor"; import markdownItFootnote from "markdown-it-footnote"; import MarkdownItGitHubAlerts from "markdown-it-github-alerts"; import markdownItTaskLists from "markdown-it-task-lists"; import { highlightCodeBlock, initHighlighting } from "utils/codeHighlight"; import helpers from "utils/helpers"; import Url from "utils/Url"; import view from "./plugin.view.js"; let $lastPluginPage; /** * Plugin page * @param {string} id * @param {boolean} installed * @param {() => void} [onInstall] * @param {() => void} [onUninstall] * @param {boolean} [installOnRender] */ export default async function PluginInclude( id, installed, onInstall, onUninstall, installOnRender, ) { if ($lastPluginPage) { $lastPluginPage.hide(); } installed = typeof installed !== "boolean" ? installed === "true" : installed; const $page = Page(strings["plugin"]); let plugin = {}; let currentVersion = ""; let purchased = false; let cancelled = false; let update = false; let isPaid = false; let price; let product; let purchaseToken; let $settingsIcon; let minVersionCode = -1; actionStack.push({ id: "plugin", action: $page.hide, }); $page.onhide = function () { hideAd(); actionStack.remove("plugin"); loader.removeTitleLoader(); cancelled = true; $lastPluginPage = null; }; $lastPluginPage = $page; app.append($page); helpers.showAd(); try { if (installed) { const manifest = Url.join(PLUGIN_DIR, id, "plugin.json"); const installedPlugin = await fsOperation(manifest) .readFile("json") .catch((err) => { alert(`Failed to load plugin metadata: ${manifest}`); console.error(err); }); const { author } = installedPlugin; const readme = Url.join( PLUGIN_DIR, id, installedPlugin.readme || "readme.md", ); const description = await fsOperation(readme) .readFile("utf8") .catch((err) => { alert(`Failed to load plugin readme: ${readme}`); console.error(err); }); let changelogs = ""; if (installedPlugin.changelogs) { const changelogPath = Url.join( PLUGIN_DIR, id, installedPlugin.changelogs, ); const changelogExists = await fsOperation(changelogPath).exists(); if (changelogExists) { changelogs = await fsOperation(changelogPath).readFile("utf8"); } } const iconUrl = await helpers.toInternalUri( Url.join(PLUGIN_DIR, id, installedPlugin.icon), ); const iconData = await fsOperation(iconUrl).readFile(); const icon = URL.createObjectURL( new Blob([iconData], { type: "image/png" }), ); plugin = { id, icon, name: installedPlugin.name, version: installedPlugin.version, author: author.name, author_github: author.github, source: installedPlugin.source, license: installedPlugin.license, keywords: installedPlugin.keywords, contributors: installedPlugin.contributors, repository: installedPlugin.repository, description, changelogs, }; isPaid = installedPlugin.price > 0; $page.settitle(plugin.name); render(); } await (async () => { try { loader.showTitleLoader(); if ((await helpers.checkAPIStatus()) && isValidSource(plugin.source)) { const remotePlugin = await fsOperation( constants.API_BASE, `plugin/${id}`, ) .readFile("json") .catch(() => null); if (cancelled || !remotePlugin) return; if (installed && remotePlugin?.version !== plugin.version) { currentVersion = plugin.version; update = true; } if (remotePlugin.min_version_code) { minVersionCode = remotePlugin.min_version_code; } plugin = Object.assign({}, remotePlugin); 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; } } catch (error) { helpers.error(error); } } } catch (error) { console.error(error); } finally { loader.removeTitleLoader(); } })(); $page.settitle(plugin.name); render(); if (installOnRender && !installed) { const $button = $page.get('[data-type="install"], [data-type="buy"]'); $button?.click(); } } catch (err) { console.error(err); helpers.error(err); } finally { loader.removeTitleLoader(); } async function install() { try { await Promise.all([ loadAd(this), installPlugin(plugin.source || id, plugin.name, purchaseToken), ]); if (onInstall) onInstall(plugin); installed = true; update = false; if (!plugin.price) { await helpers.showInterstitialIfReady(); } render(); } catch (err) { window.log("error", err); helpers.error(err); } } async function uninstall() { try { const pluginDir = Url.join(PLUGIN_DIR, plugin.id); const state = await InstallState.new(plugin.id); await Promise.all([ loadAd(this), fsOperation(pluginDir).delete(), state.delete(state.storeUrl), ]); acode.unmountPlugin(plugin.id); if (onUninstall) onUninstall(plugin.id); installed = false; update = false; if (!plugin.price) { await helpers.showInterstitialIfReady(); } render(); } catch (err) { window.log("error", err); helpers.error(err); } } async function buy(e) { const $button = e.target; const oldText = $button.textContent; try { if (!product) throw new Error("Product not found"); const apiStatus = await helpers.checkAPIStatus(); if (!apiStatus) { alert(strings.error, strings.api_error); return; } iap.setPurchaseUpdatedListener(...purchaseListener(onpurchase, onerror)); $button.textContent = strings["loading..."]; await helpers.promisify(iap.purchase, product.productId); async function onpurchase(e) { const purchase = await getPurchase(product.productId); await ajax.post(Url.join(constants.API_BASE, "plugin/order"), { data: { id: plugin.id, token: purchase?.purchaseToken, package: BuildInfo.packageName, }, }); purchaseToken = purchase?.purchaseToken; purchased = !!purchase; $button.textContent = oldText; install(); } async function onerror(error) { helpers.error(error); $button.textContent = oldText; } } catch (error) { window.log("error", "Failed to buy:"); window.log("error", error); helpers.error(error); $button.textContent = oldText; } } async function refund(e) { const $button = e.target; const oldText = $button.textContent; 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, }, }, ); if (refer) { system.openInBrowser(refer); return; } if (refunded) { toast(strings.success); if (installed) uninstall(); else render(); return; } toast(error || strings.error); } catch (error) { window.log("error", error); helpers.error(error); } finally { $button.textContent = oldText; } } async function render() { const pluginSettings = settings.uiSettings[`plugin-${plugin.id}`]; $page.body = view({ ...plugin, body: markdownIt({ html: true, xhtmlOut: true, }) .use(MarkdownItGitHubAlerts) .use(anchor, { slugify: (s) => s .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-"), }) .use(markdownItTaskLists) .use(markdownItFootnote) .render(plugin.description), changelogs: plugin.changelogs ? markdownIt({ html: true, xhtmlOut: true }) .use(MarkdownItGitHubAlerts) .use(anchor, { slugify: (s) => s .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-"), }) .use(markdownItTaskLists) .use(markdownItFootnote) .render(plugin.changelogs) : null, purchased, installed, update, isPaid, price, buy, refund, install, uninstall, currentVersion, minVersionCode, }); // Handle anchor links $page.body.querySelectorAll("a[href^='#']").forEach((link) => { const originalHref = link.getAttribute("href"); link.setAttribute("data-href", originalHref); link.style.cursor = "pointer"; // Remove default click behavior link.removeAttribute("href"); // Add custom click handler link.addEventListener( "click", (e) => { e.preventDefault(); e.stopPropagation(); const hash = link.getAttribute("data-href") || link.textContent; const targetId = hash.startsWith("#") ? hash.slice(1) : hash; // Look for either the anchor link or a heading with matching id const targetElement = $page.body.querySelector(`[name="${targetId}"]`) || $page.body.querySelector(`#${targetId}`); if (targetElement) { const headerOffset = document.querySelector("header")?.offsetHeight || 0; const elementPosition = targetElement.getBoundingClientRect().top; const offsetPosition = elementPosition - headerOffset; $page.body.scrollBy({ top: offsetPosition, behavior: "smooth", }); } return false; }, { capture: true }, ); }); // Initialize theme-aware highlight styles initHighlighting(); // Add copy button and syntax highlighting to code blocks const codeBlocks = $page.body.querySelectorAll("pre"); codeBlocks.forEach((pre) => { pre.style.position = "relative"; const copyButton = document.createElement("button"); copyButton.className = "copy-button"; copyButton.textContent = "Copy"; const codeElement = pre.querySelector("code"); if (codeElement) { const langMatch = codeElement.className.match(/language-(\w+)/); if (langMatch) { const lang = langMatch[1]; const originalCode = codeElement.textContent || ""; codeElement.classList.add("cm-highlighted"); highlightCodeBlock(originalCode, lang).then((highlighted) => { if (highlighted && highlighted !== originalCode) { codeElement.innerHTML = highlighted; } }); } } copyButton.addEventListener("click", async () => { const code = pre.querySelector("code")?.textContent || pre.textContent; try { cordova.plugins.clipboard.copy(code); copyButton.textContent = "Copied!"; setTimeout(() => { copyButton.textContent = "Copy"; }, 2000); } catch (err) { copyButton.textContent = "Failed to copy"; setTimeout(() => { copyButton.textContent = "Copy"; }, 2000); } }); pre.appendChild(copyButton); }); if ($settingsIcon) { $settingsIcon.remove(); $settingsIcon = null; } if (pluginSettings) { pluginSettings.setTitle(plugin.name); $settingsIcon = ( pluginSettings.show()} > ); if (!$page.header.contains($settingsIcon)) { $page.header.append($settingsIcon); } } } async function loadAd(el) { if (!helpers.canShowAds()) return; try { if (!(await window.iad?.isLoaded())) { const oldText = el.textContent; el.textContent = strings["loading..."]; await window.iad.load(); el.textContent = oldText; } } catch (error) { console.warn("Failed to load plugin page ad.", error); } } async function getPurchase(sku) { const purchases = await helpers.promisify(iap.getPurchases); const purchase = purchases.find((p) => p.productIds.includes(sku)); return purchase; } } function isValidSource(source) { return source ? source.startsWith(Url.join(constants.API_BASE, "plugin")) : true; } ================================================ FILE: src/pages/plugin/plugin.scss ================================================ @use "../../styles/mixins.scss"; #plugin { overflow: auto; max-width: 800px; padding: 20px; margin: 0 auto; .plugin-header { display: grid; grid-template-columns: auto 1fr auto; gap: 20px; align-items: start; margin-bottom: 24px; .plugin-icon { width: 80px; height: 80px; border-radius: 16px; background-position: center; background-repeat: no-repeat; background-size: contain; } .plugin-info { .title-wrapper { display: flex; align-items: center; flex-wrap: wrap; gap: 16px; margin-bottom: 8px; .plugin-name { font-size: 24px; margin-bottom: 0; } .source-indicator { display: flex; align-items: center; gap: 6px; background: rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); border-radius: 20px; padding: 6px 10px; font-size: 11px; color: var(--primary-text-color); cursor: pointer; transition: all 0.3s ease; border: 1px solid rgba(255, 255, 255, 0.1); text-decoration: none; &:hover { background-color: rgba(0, 0, 0, 0.5); transform: translateY(-1px); } .icon { color: var(--primary-text-color); font-size: 14px; } } } .plugin-meta { display: flex; gap: 16px; flex-wrap: wrap; color: var(--secondary-text-color); color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); font-size: 14px; margin-bottom: 12px; .meta-item { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; .icon { flex-shrink: 0; } } .author-name { a { text-decoration: none; color: inherit; } } .verified-tick { color: #3b82f6; font-size: 16px; } .version-updated { opacity: 0.6; font-size: 0.8em; margin-left: 4px; } } .metrics-row { display: flex; flex-wrap: wrap; gap: 16px; color: var(--secondary-text-color); color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); font-size: 14px; .metric { display: flex; align-items: center; gap: 4px; .metric-value { color: var(--primary-text-color); font-weight: 500; } .rating-value { padding: 2px 8px; border-radius: 12px; font-weight: 600; } .rating-high { background: var(--link-text-color); color: #0a3600; } .rating-medium { background: #f0a500; color: #3d2800; } .rating-low { background: var(--error-text-color); color: #fff; } } } .keywords { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 16px; position: relative; .keyword { background: var(--popup-background-color); background: color-mix(in srgb, var(--link-text-color) 10%, transparent); color: var(--link-text-color); padding: 6px 10px; border-radius: 12px; font-size: 13px; transition: all 0.2s; border: 1px solid var(--link-text-color); border: 1px solid color-mix(in srgb, var(--link-text-color) 25%, transparent); } } } .action-buttons { display: flex; gap: 8px; .error { display: flex; color: rgb(255, 185, 92); color: var(--error-text-color); align-items: center; a { color: inherit; text-decoration: none; } } .btn { padding: 8px 16px; border-radius: 6px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; &:hover { transform: translateY(-1px); } } .btn-install { background: var(--button-background-color); color: white; } .btn-update { background: var(--button-background-color); color: white; } .btn-uninstall { background-color: var(--danger-color) !important; color: white; } .btn-secondary { background: var(--primary-color); color: var(--primary-text-color); box-shadow: 0 0 10px var(--box-shadow-color); } } .more-info-small { text-align: center; font-style: italic; font-size: 0.8rem; opacity: 0.8; } } #plugin-tab { .options { display: flex; gap: 0; margin: 20px -20px; padding: 0 20px; border-bottom: 1px solid var(--border-color); .tab { color: var(--secondary-text-color); color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); } .tab.active { color: var(--primary-text-color); } } .tab-content { padding: 0 0 24px; #overview { pre { margin: 16px 0; border-radius: 8px; overflow: hidden; } code[class*="language-"] { display: block; font-size: 13px; line-height: 1.5; padding: 16px; margin: 0; overflow-x: auto; white-space: pre; word-wrap: normal; tab-size: 2; user-select: text; -webkit-overflow-scrolling: touch; } } } .content-section { display: none; } .content-section.active { display: block; } .content-section.md { overflow-x: hidden; overflow-wrap: break-word; img { max-width: 100%; height: auto; } pre { overflow-x: auto; max-width: 100%; } * { max-width: 100%; box-sizing: border-box; } } .contributor { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1); text-decoration: none; &:last-child { border-bottom: none; } img { width: 32px; height: 32px; border-radius: 50%; } .contributor-info { flex-grow: 1; .contributor-name { font-weight: 500; margin-bottom: 2px; color: var(--primary-text-color); } .contributor-role { font-size: 13px; color: var(--secondary-text-color); color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); } } } #changelog { .no-changelog { text-align: center; padding: 2rem; color: var(--secondary-text-color); color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); i { font-size: 3rem; margin-bottom: 1rem; opacity: 0.7; display: block; } p { margin: 0.5rem 0; } } } } @media (max-width: 768px) { .plugin-header { grid-template-columns: 1fr; justify-items: center; text-align: center; } .source-indicator { position: absolute; top: 20px; right: 20px; } .title-wrapper, .plugin-meta, .metrics-row, .keywords { justify-content: center; } .action-buttons { flex-direction: column; width: 100%; } .btn { width: 100%; justify-content: center; } .tabs { overflow-x: auto; margin: 24px -24px; padding: 0 24px; } } } .reviews-container { position: fixed; display: flex; flex-direction: column; width: 100vw; height: 70vh; top: auto; bottom: 0; background-color: rgb(255, 255, 255); background-color: var(--secondary-color); z-index: 999; box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); box-shadow: 0 0 20px var(--box-shadow-color); border-radius: 40px 40px 0 0; overflow: hidden; animation: slide-up 0.3s ease-in-out; &.hide { transition: all 0.3s ease-in-out !important; transform: translateY(100%) !important; } .reviews-header { height: 40px; display: flex; align-items: center; justify-content: center; &::after { content: ""; width: 40px; height: 5px; border-radius: 2.5px; background-color: rgba(0, 0, 0, 0.2); } } .write-review { padding: 0 10px; display: flex; align-content: center; justify-content: center; width: fit-content; margin: auto; span { height: fit-content; margin: auto; } .icon { height: 30px; margin-right: 10px; } } .reviews-body { flex: 1; overflow: auto; &.loading { @include mixins.loader(30px); } .review { padding: 10px; display: flex; flex-direction: column; border-bottom: solid 1px rgba(255, 255, 255, 0.1); .review-author { display: flex; height: 30px; align-items: center; justify-content: flex-start; margin-bottom: 5px; .vote { height: 40px; width: 40px; background-position: center; background-repeat: no-repeat; background-size: 20px; } .user-profile { height: 30px; width: 30px; border-radius: 50%; background-position: center; background-repeat: no-repeat; background-size: cover; margin-right: 10px; font-weight: 400; } } .review-body { display: flex; align-items: center; font-weight: 300; overflow: auto; } .author-reply { &::before { display: block; content: attr(data-author); font-weight: 400; margin-bottom: 10px; } padding: 10px; background-color: rgba(0, 0, 0, 0.1); } } } } ================================================ FILE: src/pages/plugin/plugin.view.js ================================================ import fsOperation from "fileSystem"; import TabView from "components/tabView"; import toast from "components/toast"; import dayjs from "dayjs/esm"; import dayjsRelativeTime from "dayjs/esm/plugin/relativeTime"; import dayjsUpdateLocale from "dayjs/esm/plugin/updateLocale"; import dayjsUtc from "dayjs/esm/plugin/utc"; 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 helpers from "utils/helpers"; import Url from "utils/Url"; dayjs.extend(dayjsRelativeTime); dayjs.extend(dayjsUtc); dayjs.extend(dayjsUpdateLocale); // Configure dayjs for shorter relative time format dayjs.updateLocale("en", { relativeTime: { future: "in %s", past: (value, withoutSuffix) => { if (value === "now") { return value; } return withoutSuffix ? value : `${value} ago`; }, s: "now", ss: "now", m: "1m", mm: "%dm", h: "1h", hh: "%dh", d: "1d", dd: "%dd", M: "1mo", MM: "%dmo", y: "1y", yy: "%dy", }, }); export default (props) => { const { id, name, body, icon, author, downloads, license, changelogs, repository, keywords: keywordsRaw, contributors: contributorsRaw, votes_up: votesUp, votes_down: votesDown, author_verified: authorVerified, author_github: authorGithub, comment_count: commentCount, package_updated_at: packageUpdatedAt, } = props; let rating = "unrated"; const keywords = typeof keywordsRaw === "string" ? JSON.parse(keywordsRaw) : keywordsRaw; const contributors = typeof contributorsRaw === "string" ? JSON.parse(contributorsRaw) : contributorsRaw; if (votesUp || votesDown) { rating = `${Math.round((votesUp / (votesUp + votesDown)) * 100)}%`; } const formatUpdatedDate = (dateString) => { if (!dateString) return null; try { const updateTime = dayjs.utc(dateString); if (!updateTime.isValid()) return null; return updateTime.fromNow(); } catch (error) { console.warn("Error parsing date with dayjs:", dateString, error); return null; } }; return (

          {name}

          {repository ? ( {strings.open_source} ) : null}
          {author} {authorVerified ? ( { toast(strings["verified publisher"]); }} className="icon verified verified-tick" > ) : ( "" )} {license || "Unknown"}
          {votesUp !== undefined ? (
          {helpers.formatDownloadCount( typeof downloads === "string" ? Number.parseInt(downloads) : downloads, )} {strings.downloads}
          = 80 ? "rating-high" : rating.replace("%", "") >= 50 ? "rating-medium" : "rating-low"}`} > {rating}
          {commentCount} {strings.reviews}
          ) : null} {Array.isArray(keywords) && keywords.length ? (
          {keywords.map((keyword) => ( {keyword} ))}
          ) : null}
          {strings.overview} {strings.contributors} {strings.changelog}
          {(() => { let contributorsList = contributors?.length ? [ { name: author, role: "Developer", github: authorGithub }, ...contributors, ] : [{ name: author, role: "Developer", github: authorGithub }]; return contributorsList.map(({ name, role, github }) => { let dp = Url.join(constants.API_BASE, `../user.png`); if (github) { dp = `https://avatars.githubusercontent.com/${github}`; } return ( {name}
          {name}
          {role}
          ); }); })()}

          No changelog is available for this plugin yet.

          Check back later for updates!

          ` } >
          ); }; function handleTabClick(e) { const $target = e.target; if (!$target.classList.contains("tab")) return; const tabs = document.querySelectorAll(".tab"); const contents = document.querySelectorAll(".content-section"); tabs.forEach((tab) => tab.classList.remove("active")); contents.forEach((content) => content.classList.remove("active")); $target.classList.add("active"); const tabId = $target.dataset.tab; document.getElementById(tabId).classList.add("active"); } function Buttons({ name, isPaid, installed, update, install, uninstall, purchased, price, buy, minVersionCode, }) { if ( typeof minVersionCode === "number" && minVersionCode > BuildInfo.versionCode ) { return ( ); } if (installed && update) { return ( <> ); } if (installed) { return ( ); } if (isPaid && !purchased && price) { return ( ); } if (isPaid && !purchased && !price) { return (
          alert(strings.info, strings["no-product-info"])} className="icon info" > {strings["product not available"]}
          ); } return ( ); } function Version({ currentVersion, version, packageUpdatedAt, formatUpdatedDate, }) { const updatedText = formatUpdatedDate && packageUpdatedAt ? formatUpdatedDate(packageUpdatedAt) : null; if (!currentVersion) { return ( v{version} {updatedText && ( ({updatedText}) )} ); } return ( v{currentVersion} → v{version} {updatedText && ({updatedText})} ); } async function showReviews(pluginId, author) { const mask = Ref(); const body = Ref(); const container = Ref(); actionStack.push({ id: "reviews", action: closeReviews, }); app.append( , ); app.append( , ); try { const reviews = await fsOperation( constants.API_BASE, `/comments/${pluginId}`, ).readFile("json"); if (!reviews.length) { body.style.textAlign = "center"; body.textContent = "No reviews yet"; return; } reviews.forEach((review) => { if (!review.comment) return; review.author = author; body.append(); }); } catch (error) { body.textContent = error.message; } finally { body.classList.remove("loading"); } function closeReviews() { actionStack.remove("reviews"); container.classList.add("hide"); setTimeout(() => { mask.el.remove(); container.el.remove(); }, 300); } /** * @param {TouchEvent} e */ function ontouchstart(e) { const { clientY } = e.touches[0]; const { top } = container.el.getBoundingClientRect(); const y = clientY - top; let dy = 0; container.style.transition = "none"; document.addEventListener("touchmove", ontouchmove); document.addEventListener("touchend", ontouchend); document.addEventListener("touchcancel", ontouchend); function ontouchmove(e) { const { clientY } = e.touches[0]; dy = clientY - top - y; if (dy < 0) dy = 0; container.style.transform = `translateY(${dy}px)`; } function ontouchend() { document.removeEventListener("touchmove", ontouchmove); document.removeEventListener("touchend", ontouchend); document.removeEventListener("touchcancel", ontouchcancel); if (dy < 100) { container.style.transition = "transform 0.3s ease-in-out"; container.style.transform = "translateY(0)"; return; } closeReviews(); } } } function Review({ name, github, vote, comment, author, author_reply: authorReply, }) { let dp = Url.join(constants.API_BASE, `../user.png`); let voteImage = Ref(); let review = Ref(); if (github) { dp = `https://avatars.githubusercontent.com/${github}`; } if (vote === 1) { voteImage.style.backgroundImage = `url(${Url.join(constants.API_BASE, `../thumbs-up.gif`)})`; } else if (vote === -1) { voteImage.style.backgroundImage = `url(${Url.join(constants.API_BASE, `../thumbs-down.gif`)})`; } if (authorReply) { setTimeout(() => { review.append(

          {authorReply}

          , ); }, 0); } return (
          {name}

          {comment}

          ); } function MoreInfo({ purchased, price, refund }) { if (!purchased) return ""; return ( {strings.owned}{price} •{" "} {strings.refund} ); } ================================================ FILE: src/pages/plugins/index.js ================================================ function plugins(updates) { import(/* webpackChunkName: "plugins" */ './plugins').then( (res) => { const Plugins = res.default; Plugins(updates); }, ); } export default plugins; ================================================ FILE: src/pages/plugins/item.js ================================================ import helpers from "utils/helpers"; import pluginIcon from './plugin-icon.png'; /** * Creates a plugin list item * @param {object} param0 * @param {string} [param0.id] * @param {string} [param0.name] * @param {string} [param0.icon] * @param {string} [param0.version] * @param {number} [param0.downloads] * @param {boolean} [param0.installed] * @param {boolean} [param0.enabled] * @param {function} [param0.onToggleEnabled] * @returns */ export default function Item({ 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; })(); 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}
          ); } ================================================ FILE: src/pages/plugins/plugins.js ================================================ 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 searchBar from "components/searchbar"; import FileBrowser from "pages/fileBrowser"; import installPlugin from "lib/installPlugin"; import prompt from "dialogs/prompt"; import actionStack from "lib/actionStack"; import Contextmenu from "components/contextmenu"; import settings from "lib/settings"; import loadPlugin from "lib/loadPlugin"; import { hideAd } from "lib/startAd"; /** * * @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); } } } ================================================ FILE: src/pages/plugins/plugins.scss ================================================ #plugins { display: flex; flex-direction: column; .filter-message { font-size: 0.9rem; font-weight: 500; color: var(--popup-text-color); padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-color); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); display: flex; justify-content: space-between; align-items: center; background: var(--secondary-color); border-radius: 8px 8px 0 0; margin-bottom: 2px; strong { color: var(--link-text-color); margin-left: 0.25rem; } span { margin-left: auto; padding: 0.5rem; border-radius: 6px; transition: all 0.2s ease; &:hover { cursor: pointer; background-color: var(--error-text-color); transform: scale(1.05); } } } .list { overflow-y: auto; padding: 0.5rem 0; &:empty::after { content: attr(empty-msg); display: flex; align-items: center; justify-content: center; height: 200px; color: var(--secondary-text-color); font-size: 0.9rem; font-style: italic; } } .list-item { margin: 0 0.5rem 0.5rem 0.5rem; background: var(--secondary-color); padding: 0.875rem; border-radius: 12px; transition: all 0.2s ease; display: flex; align-items: center; overflow: hidden; border: 1px solid var(--border-color); cursor: pointer; position: relative; &:hover { background: var(--primary-color); background: color-mix(in srgb, var(--primary-color) 12%, transparent); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border-color: var(--active-color); } &:active { transform: translateY(0); } .plugin-header { display: flex; align-items: center; gap: 0.75rem; width: 100%; max-width: 100%; height: fit-content; min-height: 40px; .plugin-icon { width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0; overflow: hidden; display: flex; align-items: center; justify-content: center; background: var(--primary-color); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); img { width: 100%; height: 100%; object-fit: cover; object-position: center; } } .plugin-info { flex: 1; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin: 0; height: fit-content; .plugin-main { min-width: 0; flex: 1; .plugin-title { display: flex; align-items: center; gap: 0.75rem; min-width: 0; margin-bottom: 0.375rem; .plugin-name { font-weight: 600; font-size: 0.95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: calc(100% - 60px); color: var(--primary-text-color); } .plugin-version { color: var(--secondary-text-color); font-size: 0.75rem; font-weight: 500; padding: 0.25rem 0.5rem; background: var(--primary-color); border-radius: 6px; white-space: nowrap; flex-shrink: 0; } } .plugin-meta { font-size: 0.875rem; color: var(--secondary-text-color); display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; .plugin-meta-dot { width: 4px; height: 4px; background: var(--secondary-text-color); border-radius: 50%; display: inline-block; opacity: 0.6; } .plugin-stats { display: flex; align-items: center; gap: 0.375rem; font-size: 0.875rem; &.plugin-author { max-width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .icon { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; font-size: 14px; line-height: 1; flex-shrink: 0; opacity: 0.8; } } } } .plugin-price { background: var(--active-color); color: var(--button-text-color); padding: 0.375rem 0.75rem; border-radius: 8px; font-size: 0.875rem; font-weight: 600; display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0; height: fit-content; box-shadow: 0 2px 4px var(--box-shadow-color); } } } .plugin-toggle-switch { display: flex !important; align-items: center; gap: 0; cursor: pointer; z-index: 100; min-width: auto; pointer-events: auto !important; position: relative; margin-left: auto; justify-content: flex-end; padding: 0; border-radius: 999px; transition: background-color 0.2s ease; &:hover { background-color: transparent; } } .plugin-toggle-track { width: 2.8rem; height: 1.65rem; border-radius: 999px; border: 1px solid var(--border-color); border: 1px solid color-mix(in srgb, var(--border-color), transparent 6%); background: var(--secondary-color); background: color-mix( in srgb, var(--secondary-color), var(--popup-background-color) 30% ); position: relative; transition: background-color 160ms ease, border-color 160ms ease, box-shadow 180ms ease; display: inline-block; margin-right: 0; box-sizing: border-box; box-shadow: none; } .plugin-toggle-switch[data-enabled="true"] .plugin-toggle-track, .plugin-toggle-track[data-enabled="true"] { background: var(--button-background-color); border-color: var(--button-background-color); border-color: color-mix(in srgb, var(--button-background-color), transparent 10%); box-shadow: inset 0 0 0 1px var(--button-background-color); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--button-background-color), transparent 12%); } .plugin-toggle-thumb { position: absolute; left: 0; top: 0; width: 1.25rem; height: 1.25rem; margin: 0.14rem; border-radius: 999px; background: var(--popup-text-color); background: color-mix( in srgb, var(--popup-text-color), var(--popup-background-color) 18% ); border: 1px solid var(--border-color); border: 1px solid color-mix(in srgb, var(--border-color), transparent 15%); box-sizing: border-box; box-shadow: 0 0 0 1px var(--border-color), 0 1px 3px rgba(0, 0, 0, 0.22); box-shadow: 0 0 0 1px color-mix(in srgb, var(--border-color), transparent 34%), 0 1px 3px rgba(0, 0, 0, 0.22); transition: transform 180ms cubic-bezier(0.2, 0.9, 0.3, 1), background-color 160ms ease, box-shadow 180ms ease; } .plugin-toggle-switch[data-enabled="true"] .plugin-toggle-thumb, .plugin-toggle-track[data-enabled="true"] .plugin-toggle-thumb { transform: translateX(1.12rem); background: var(--button-text-color); box-shadow: 0 2px 8px var(--button-background-color); box-shadow: 0 2px 8px color-mix(in srgb, var(--button-background-color), transparent 55%); } } } ================================================ FILE: src/pages/problems/index.js ================================================ function plugin({ id, installed }, onInstall, onUninstall) { import(/* webpackChunkName: "problems" */ "./problems").then((res) => { const Problems = res.default; Problems(); }); } export default plugin; ================================================ FILE: src/pages/problems/problems.js ================================================ import "./style.scss"; import { getLspDiagnostics } from "cm/lsp/diagnostics"; import Page from "components/page"; import actionStack from "lib/actionStack"; import EditorFile from "lib/editorFile"; import { hideAd } from "lib/startAd"; import helpers from "utils/helpers"; export default function Problems() { const $page = Page(strings["problems"]); /**@type {EditorFile[]} */ const files = editorManager.files; const $content =
          ; files.forEach((file) => { if (file.type !== "editor") return; const annotations = collectAnnotations(file); if (!annotations.length) return; const title = `${file.name} (${annotations.length})`; $content.append(
          {title}
          {annotations.map((annotation) => { const { type, text, row, column } = annotation; const icon = getIconForType(type); return (
          {text} {row + 1}:{column + 1}
          ); })}
          , ); }); $content.addEventListener("click", clickHandler); $page.body = $content; app.append($page); helpers.showAd(); $page.onhide = function () { hideAd(); actionStack.remove("problems"); }; actionStack.push({ id: "problems", action: $page.hide, }); /** * Click handler for problems page * @param {MouseEvent} e */ function clickHandler(e) { const $target = e.target.closest("[data-action='goto']"); if (!$target) return; const { action } = $target.dataset; if (action === "goto") { const { fileId } = $target.dataset; const annotation = $target.annotation; if (!annotation) return; const row = normalizeIndex(annotation.row); const column = normalizeIndex(annotation.column); editorManager.switchFile(fileId); editorManager.editor.gotoLine(row + 1, column); $page.hide(); setTimeout(() => { editorManager.editor.focus(); }, 100); } } function collectAnnotations(file) { const annotations = []; const { session } = file; const isActiveFile = editorManager.activeFile?.id === file.id; const state = isActiveFile && editorManager.editor ? editorManager.editor.state : session; if (session && typeof session.getAnnotations === "function") { const aceAnnotations = session.getAnnotations() || []; for (const item of aceAnnotations) { if (!item) continue; const row = normalizeIndex(item.row); const column = normalizeIndex(item.column); annotations.push({ row, column, text: item.text || "", type: normalizeSeverity(item.type), }); } } if (state && typeof state.field === "function") { annotations.push(...readLspAnnotations(state)); } return annotations; } function readLspAnnotations(state) { const diagnostics = getLspDiagnostics(state); if (!diagnostics.length) return []; const doc = state.doc; if (!doc || typeof doc.lineAt !== "function") return []; return diagnostics .map((diagnostic) => { const start = clampPosition(diagnostic.from, doc.length); const line = doc.lineAt(start); const row = Math.max(0, line.number - 1); const column = Math.max(0, start - line.from); let message = diagnostic.message || ""; if (diagnostic.source) { message = message ? `${message} (${diagnostic.source})` : diagnostic.source; } return { row: normalizeIndex(row), column: normalizeIndex(column), text: message, type: normalizeSeverity(diagnostic.severity), }; }) .filter((annotation) => annotation.text); } function clampPosition(pos, length) { if (typeof pos !== "number" || Number.isNaN(pos)) return 0; return Math.max(0, Math.min(pos, Math.max(0, length))); } function normalizeIndex(value) { if (typeof value === "number" && Number.isFinite(value)) { return Math.max(0, value); } const parsed = Number(value); if (Number.isFinite(parsed)) { return Math.max(0, parsed); } return 0; } function normalizeSeverity(severity) { switch (severity) { case "error": case "fatal": return "error"; case "warn": case "warning": return "warning"; default: return "info"; } } function getIconForType(type) { switch (type) { case "error": return "cancel"; case "warning": return "warningreport_problem"; default: return "info"; } } } ================================================ FILE: src/pages/problems/style.scss ================================================ #problems { height: 100%; width: 100%; .single-file { padding: 10px; box-sizing: border-box; summary { height: 40px; width: 100%; font-weight: bold; line-height: 40px; } .problem { display: flex; padding: 5px; border-bottom: solid 1px var(--border-color); * { pointer-events: none; } .icon { margin: 0 5px; font-size: 0.9rem; color: var(--primary-text-color) !important; } .problem-line { display: flex; align-items: center; margin-left: 10px; font-size: 0.9rem; } .problem-message { flex: 1; &[data-type='error'] { color: var(--danger-color); } &[data-type='warning'] { color: var(--error-text-color); } } } } } ================================================ FILE: src/pages/quickTools/index.js ================================================ export default async function QuickToolsSettings() { const { default: Settings } = await import("./quickTools.js"); Settings(); } ================================================ FILE: src/pages/quickTools/quickTools.js ================================================ import "./style.scss"; import Page from "components/page"; import items, { description } from "components/quickTools/items"; import actionStack from "lib/actionStack"; import settings from "lib/settings"; import { hideAd } from "lib/startAd"; import helpers from "utils/helpers"; export default function QuickTools() { const $page = Page(strings["shortcut buttons"]); $page.id = "quicktools-settings-page"; $page.style.overflow = "hidden"; $page.style.display = "flex"; $page.style.flexDirection = "column"; const manager = new QuickToolsManager(); $page.body = manager.getContainer(); const onShow = $page.onshow; $page.onshow = function () { if (onShow) onShow.call(this); const scrollContainer = $page.get(".scroll-container") || $page; scrollContainer.style.overflow = "hidden"; manager.getContainer().style.height = "100%"; }; actionStack.push({ id: "quicktools-settings", action: $page.hide, }); $page.onhide = () => { actionStack.remove("quicktools-settings"); hideAd(); // Cleanup manager manager.destroy(); }; app.append($page); helpers.showAd(); } class QuickToolsManager { constructor() { this.container =
          ; this.render(); this.bindEvents(); this.longPressTimer = null; this.dragState = null; } getContainer() { return this.container; } render() { this.destroy(); // Cleanup potential drag states before re-rendering this.container.textContent = ""; // --- Active Tools Section --- const activeSection =
          ; activeSection.appendChild(
          {strings["active tools"]}
          , ); const activeGrid =
          ; const totalSlots = settings.QUICKTOOLS_ROWS * settings.QUICKTOOLS_GROUPS * settings.QUICKTOOLS_GROUP_CAPACITY; for (let i = 0; i < totalSlots; i++) { const itemIndex = settings.value.quicktoolsItems[i]; const itemDef = items[itemIndex]; const el = this.createItemElement(itemDef, i, "active"); activeGrid.appendChild(el); } activeSection.appendChild(activeGrid); this.container.appendChild(activeSection); // --- Available Tools Section --- const availableSection =
          ; availableSection.appendChild(
          {strings["available tools"]}
          , ); // Group items const categories = { Modifiers: ["ctrl", "shift", "alt", "meta"], Commands: ["command", "undo", "redo", "save", "search"], Navigation: ["key"], Symbols: ["insert"], Other: [], }; const groupedItems = {}; items.forEach((item, index) => { let category = "Other"; for (const [cat, actions] of Object.entries(categories)) { if (actions.includes(item.action)) { category = cat; break; } } if (!groupedItems[category]) groupedItems[category] = []; groupedItems[category].push({ item, index }); }); Object.entries(groupedItems).forEach(([category, list]) => { const catHeader =
          {category}
          ; const catGrid =
          ; list.forEach(({ item, index }) => { const el = this.createItemElement(item, index, "source"); catGrid.appendChild(el); }); availableSection.appendChild(catHeader); availableSection.appendChild(catGrid); }); this.container.appendChild(availableSection); } createItemElement(itemDef, index, type) { if (!itemDef) return (
          ); const hasIcon = itemDef.icon && itemDef.icon !== "letters"; // If it's not an icon, we assume it relies on 'letters' // Some items might have both, but 'letters' mode implies text rendering const el = (
          {hasIcon ? : null}
          ); return el; } bindEvents() { const c = this.container; c.addEventListener("touchstart", this.handleTouchStart.bind(this), { passive: false, }); c.addEventListener("touchmove", this.handleTouchMove.bind(this), { passive: false, }); c.addEventListener("touchend", this.handleTouchEnd.bind(this)); c.addEventListener("contextmenu", (e) => e.preventDefault()); c.addEventListener("mousedown", this.handleMouseDown.bind(this)); } // --- Touch Handlers --- handleTouchStart(e) { // If already dragging or pending, ignore new touches (prevent multi-touch mess) if (this.dragState || this.longPressTimer) return; const target = e.target.closest(".tool-item"); if (!target) return; this.longPressTimer = setTimeout(() => { this.startDrag(target, e.touches[0]); }, 300); this.touchStartX = e.touches[0].clientX; this.touchStartY = e.touches[0].clientY; this.potentialTarget = target; } handleTouchMove(e) { const touch = e.touches[0]; if (this.dragState) { e.preventDefault(); this.updateDrag(touch); return; } if ( Math.hypot( touch.clientX - this.touchStartX, touch.clientY - this.touchStartY, ) > 10 ) { clearTimeout(this.longPressTimer); this.longPressTimer = null; this.potentialTarget = null; } } handleTouchEnd(e) { clearTimeout(this.longPressTimer); if (this.dragState) { this.endDrag(); } else if (this.potentialTarget) { // It was a tap if (e.cancelable) e.preventDefault(); this.handleClick(this.potentialTarget); } this.potentialTarget = null; } // --- Mouse Handlers --- handleMouseDown(e) { const target = e.target.closest(".tool-item"); if (!target) return; this.mouseDownInfo = { target, x: e.clientX, y: e.clientY, isDrag: false, }; const moveHandler = (ev) => { if (!this.mouseDownInfo.isDrag) { if ( Math.hypot( ev.clientX - this.mouseDownInfo.x, ev.clientY - this.mouseDownInfo.y, ) > 5 ) { this.mouseDownInfo.isDrag = true; this.startDrag(target, this.mouseDownInfo); } } if (this.dragState) { this.updateDrag(ev); } }; const upHandler = () => { document.removeEventListener("mousemove", moveHandler); document.removeEventListener("mouseup", upHandler); if (this.dragState) { this.endDrag(); } else { this.handleClick(target); } }; document.addEventListener("mousemove", moveHandler); document.addEventListener("mouseup", upHandler); } // --- Core Drag Logic --- startDrag(el, pointer) { // Double check state if (this.dragState) { this.destroy(); return; } if (navigator.vibrate) navigator.vibrate(30); const rect = el.getBoundingClientRect(); const ghost = el.cloneNode(true); ghost.classList.add("tool-ghost"); ghost.style.width = rect.width + "px"; ghost.style.height = rect.height + "px"; document.body.appendChild(ghost); el.classList.add("dragging"); const type = el.dataset.type; const index = Number.parseInt(el.dataset.index, 10); this.dragState = { el, type, // 'active' or 'source' index, // slot index (active) or item ID (source) ghost, offsetX: pointer.clientX - rect.left - rect.width / 2, offsetY: pointer.clientY - rect.top - rect.height / 2, }; this.updateDrag(pointer); } updateDrag(pointer) { const { ghost } = this.dragState; ghost.style.left = pointer.clientX + "px"; ghost.style.top = pointer.clientY + "px"; const elementBelow = document.elementFromPoint( pointer.clientX, pointer.clientY, ); this.cleanupHighlight(); const targetItem = elementBelow?.closest(".tool-item"); if (targetItem && targetItem.dataset.type === "active") { targetItem.classList.add("highlight-target"); this.dragState.dropTarget = targetItem; } else { this.dragState.dropTarget = null; } } cleanupHighlight() { const highlighted = this.container.querySelectorAll(".highlight-target"); highlighted.forEach((el) => el.classList.remove("highlight-target")); } endDrag() { const { el, ghost, dropTarget, type, index } = this.dragState; this.cleanupHighlight(); el.classList.remove("dragging"); ghost.remove(); this.dragState = null; if (dropTarget) { const targetIndex = Number.parseInt(dropTarget.dataset.index, 10); if (type === "active") { // Swap within active if (targetIndex !== index) { this.swapItems(index, targetIndex); } } else if (type === "source") { // Replace active slot with source item this.replaceItem(targetIndex, index); } } } swapItems(srcIndex, destIndex) { const temp = settings.value.quicktoolsItems[srcIndex]; settings.value.quicktoolsItems[srcIndex] = settings.value.quicktoolsItems[destIndex]; settings.value.quicktoolsItems[destIndex] = temp; settings.update(); this.render(); // Re-render to reflect changes } replaceItem(slotIndex, newItemId) { settings.value.quicktoolsItems[slotIndex] = newItemId; settings.update(); this.render(); } async handleClick(el) { const type = el.dataset.type; const index = Number.parseInt(el.dataset.index, 10); let itemDef; if (type === "active") { const itemIndex = settings.value.quicktoolsItems[index]; itemDef = items[itemIndex]; } else { itemDef = items[index]; } if (itemDef) { const desc = description(itemDef.id); window.toast(desc, 2000); } } destroy() { if (this.longPressTimer) clearTimeout(this.longPressTimer); this.longPressTimer = null; if (this.dragState) { if (this.dragState.ghost) { this.dragState.ghost.remove(); } if (this.dragState.el) { this.dragState.el.classList.remove("dragging"); } } this.cleanupHighlight(); this.dragState = null; this.potentialTarget = null; } } ================================================ FILE: src/pages/quickTools/style.scss ================================================ #quicktools-settings { display: flex; flex-direction: column; height: 100%; width: 100%; overflow-y: hidden; box-sizing: border-box; background-color: var(--primary-color); .section-title { font-size: 1rem; font-weight: bold; padding: 10px; color: var(--text-color); opacity: 0.8; background-color: var(--primary-color); z-index: 10; width: 100%; display: block; box-sizing: border-box; } .section { display: flex; flex-direction: column; width: 100%; box-sizing: border-box; } .section.active-tools { flex: 0 0 auto; border-bottom: 3px dashed var(--border-color); padding-bottom: 10px; margin-bottom: 15px; padding-left: 10px; padding-right: 10px; } .section.available-tools { flex: 1 1 auto; overflow-y: auto; padding: 0 10px 10px 10px; .category-header { font-size: 0.8rem; text-transform: uppercase; opacity: 0.6; margin-top: 15px; margin-bottom: 5px; margin-left: 5px; font-weight: bold; display: block; width: 100%; } } .quicktools-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); gap: 8px; width: 100%; padding-bottom: 5px; padding-top: 5px; &.active-grid { grid-template-columns: repeat(8, 1fr); min-height: auto; margin-bottom: 5px; } } .tool-item { background-color: var(--secondary-color); color: var(--text-color); border-radius: 8px; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; font-size: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: transform 0.2s ease, background-color 0.2s; user-select: none; -webkit-user-select: none; cursor: grab; position: relative; border: 1px solid transparent; border-bottom: 2px dashed var(--border-color); &:active { transform: scale(0.95); cursor: grabbing; } &.dragging { opacity: 0.3; transform: scale(0.9); border-color: var(--active-color); } &.highlight-target { border-color: var(--active-color); background-color: rgba(0, 0, 0, 0.1); transform: scale(1.05); } &.empty { background-color: transparent; border: 2px dashed rgba(0, 0, 0, 0.1); box-shadow: none; } .icon { font-size: 1.2rem; pointer-events: none; } &.has-letters::before { content: attr(data-letters); font-size: 0.9rem; font-weight: bold; text-transform: uppercase; } &.has-icon::before { display: none; } } } .tool-ghost { position: fixed; top: 0; left: 0; width: 60px; height: 60px; background-color: var(--active-color); color: #fff; border-radius: 12px; display: flex; align-items: center; justify-content: center; z-index: 9999; pointer-events: none; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); transform: translate(-50%, -50%) scale(1.1); .icon { font-size: 1.5rem; } &.has-letters::before { content: attr(data-letters); font-size: 1rem; font-weight: bold; text-transform: uppercase; } } ================================================ FILE: src/pages/sponsor/index.js ================================================ /** * Sponsor page * @param {() => void} onclose */ export default function Sponsor(onclose) { import("./sponsor").then((res) => res.default(onclose)); } ================================================ FILE: src/pages/sponsor/sponsor.js ================================================ 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"; 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 helpers from "utils/helpers"; //TODO: fix (-1 means, user is not logged in to any google account) /** * Sponsor page * @param {() => void} onclose */ export default function Sponsor(onclose) { const BASE_URL = "https://acode.app/res/"; const $page = Page(strings.sponsor); let cancel = false; actionStack.push({ id: "sponsor_page", action: $page.hide, }); $page.onhide = function () { onclose?.(); cancel = true; actionStack.remove("sponsor_page"); }; app.append($page); iap.setPurchaseUpdatedListener( (purchases) => { if (Array.isArray(purchases)) { (async function () { const promises = []; for (let purchase of purchases) { promises.push( new Promise((resolve, reject) => { iap.consume( purchase.purchaseToken, (resCode) => { purchase.consumed = resCode === iap.OK ? true : false; purchase.consumeCode = resCode; resolve(purchase); }, (err) => { reject(err); }, ); }), ); } const settledPromises = await Promise.allSettled(promises); const rejectedPromise = settledPromises.find( (promise) => promise.status === "rejected", ); let msg = ""; if (rejectedPromise) { msg = "Something went wrong.\n"; 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 = ``; msg += "

          Thank you for supporting Acode!

          "; } const order = settledPromises[0].value; const [productId] = order.productIds; const sponsorDetails = JSON.parse( localStorage.getItem(`sponsor_${productId}`), ); try { const res = await ajax.post(`${constants.API_BASE}/sponsor`, { data: { ...sponsorDetails, tier: productId, packageName: BuildInfo.packageName, purchaseToken: order.purchaseToken, }, }); if (res.error) { helpers.error(res.error); } else { box(strings.info.toUpperCase(), msg); localStorage.removeItem(`sponsor_${productId}`); $page.hide(); } } catch (error) { helpers.error(error); } finally { loader.removeTitleLoader(); } })(); } }, (err) => { if (err !== iap.USER_CANCELED) { alert(strings.error.toUpperCase(), err); } loader.removeTitleLoader(); }, ); loader.showTitleLoader(); render() .catch((error) => { actionStack.pop(); helpers.error(error); }) .finally(() => { loader.removeTitleLoader(); }); async function render() { let products = await new Promise((resolve, reject) => { iap.getProducts( constants.SKU_LIST, (products) => { resolve(products); }, (err) => { reject(err); }, ); }); if (cancel) return; products = products.sort((a, b) => { const aPrice = Number.parseFloat(a.price.replace(/[^0-9.]/g, "")); const bPrice = Number.parseFloat(b.price.replace(/[^0-9.]/g, "")); return bPrice - aPrice; }); $page.body = ( ); } } async function handlePurchase(productId, title) { let image; let result; const extraFields = []; if (["silver", "gold", "platinum", "titanium"].includes(productId)) { extraFields.push({ placeholder: "Website", required: false, id: "website", type: "url", }); } if (productId === "titanium") { extraFields.push({ placeholder: "Tagline", required: false, id: "tagline", type: "text", }); } if (["gold", "platinum", "titanium"].includes(productId)) { extraFields.push({ placeholder: "Logo/Image (500KB max)", required: false, type: "text", id: "image", onclick() { sdcard.openDocumentFile(async (res) => { if (res.length > 500000) { this.setError("File size exceeds 500KB"); return; } this.setError(""); this.value = res.filename; const arraybuffer = await fsOperation(res.uri).readFile(); const blob = new Blob([arraybuffer], { type: res.type }); const reader = new FileReader(); reader.onload = () => { image = reader.result; if (result && typeof result === "object") { result.image = image; localStorage.setItem( `sponsor_${productId}`, JSON.stringify(result), ); } }; reader.readAsDataURL(blob); }, "image/*"); }, }); } result = await multiPrompt(onlyTitle(title), [ { placeholder: "Name", required: true, id: "name", }, { placeholder: "Email", required: false, id: "email", type: "email", match: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, }, ...extraFields, { placeholder: "Show in sponsors list", required: false, id: "public", type: "checkbox", value: true, }, ]); if (!result) { return; } if (image) { result.image = image; } localStorage.setItem(`sponsor_${productId}`, JSON.stringify(result)); loader.showTitleLoader(); iap.purchase(productId); } function onlyTitle(title) { return title.replace(" (Acode - code editor | FOSS)", ""); } ================================================ FILE: src/pages/sponsor/style.scss ================================================ #sponsor-page { overflow: auto; .tier-icon { display: inline-block; width: 24px; height: 24px; margin-right: 8px; border-radius: 50%; &.crystal { background: linear-gradient(45deg, #95a5a6, #bdc3c7); } &.bronze { background: linear-gradient(45deg, #cd7f32, #ff8c42); } &.silver { background: linear-gradient(45deg, #c0c0c0, #e6e6fa); } &.gold { background: linear-gradient(45deg, #ffd700, #ffed4e); } &.platinum { background: linear-gradient(45deg, #e5e4e2, #ffffff); } &.titanium { background: linear-gradient(45deg, #878681, #c4c4b0); } } .header { text-align: center; margin-bottom: 10px; animation: fadeInUp 0.8s ease-out; } h1 { font-size: 28px; font-weight: 700; } .subtitle { font-size: 16px; opacity: 0.9; margin-bottom: 10px; } .tier { width: 90%; max-width: 400px; margin: 0 auto; border-radius: 16px; padding: 20px; margin-bottom: 15px; background-color: var(--popup-background-color); color: var(--popup-text-color); border: 1px solid var(--border-color); transition: all 0.3s ease; animation: fadeInUp 0.8s ease-out calc(0.6s + var(--delay, 0s)) both; box-sizing: border-box; } .tier-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .tier-name { font-size: 18px; font-weight: 600; display: flex; align-items: center; } .tier-icon { width: 24px; height: 24px; border-radius: 50%; margin-right: 10px; display: flex; align-items: center; justify-content: center; font-size: 14px; } .tier-price { font-size: 16px; font-weight: 600; opacity: 0.9; } .tier-description { font-size: 14px; opacity: 0.8; line-height: 1.4; } .purchase-btn { background: var(--button-background-color); color: var(--button-text-color); border: none; padding: 10px 20px; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; width: 100%; margin-top: 10px; } .purchase-btn:active { background: linear-gradient(45deg, #44a08d, #4ecdc4); } .purchase-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } } ================================================ FILE: src/pages/sponsors/index.js ================================================ export default function Sponsors() { import("./sponsors").then((res) => res.default()); } ================================================ FILE: src/pages/sponsors/sponsors.js ================================================ 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 Sponsor from "pages/sponsor"; import helpers from "utils/helpers"; export default function Sponsors() { const page = Page("Sponsors"); const titaniumSponsors = Ref(); const platinumSponsors = Ref(); const goldSponsors = Ref(); const silverSponsors = Ref(); const bronzeSponsors = Ref(); const crystalSponsors = Ref(); let cancel = false; actionStack.push({ id: "sponsors_page", action: page.hide, }); page.onhide = () => { actionStack.remove("sponsors_page"); cancel = true; }; page.body = (

          Join our community of supporters and help shape the future of mobile development

          Acode's Sponsors

          Titanium
          Platinum
          Gold
          Silver
          Bronze
          Crystal
          ); render(); app.append(page); async function render() { let sponsors = []; try { const res = await ajax.get(`${constants.API_BASE}/sponsors`); if (res.error) { toast("Unable to load sponsors..."); console.error("Error loading sponsors:", res.error); } else { sponsors = res; localStorage.setItem("cached_sponsors", JSON.stringify(sponsors)); } } catch (error) { toast("Unable to load sponsors..."); console.error("Error loading sponsors:", error); } if (!sponsors.length && "cached_sponsors" in localStorage) { try { const cachedSponsors = helpers.parseJSON( localStorage.getItem("cached_sponsors"), ); sponsors = Array.isArray(cachedSponsors) ? cachedSponsors : []; } catch (error) { console.error("Failed to parse cached sponsors", error); } } titaniumSponsors.content = ""; platinumSponsors.content = ""; goldSponsors.content = ""; silverSponsors.content = ""; bronzeSponsors.content = ""; crystalSponsors.content = ""; for (const sponsor of sponsors) { // Append each sponsor to the corresponding tier switch (sponsor.tier) { case "titanium": titaniumSponsors.append(); break; case "platinum": platinumSponsors.append(); break; case "gold": goldSponsors.append(); break; case "silver": silverSponsors.append(); break; case "bronze": bronzeSponsors.append(); break; case "crystal": crystalSponsors.append(); break; } } } } /** * Sponsor Card Component * @param {object} props * @param {string} props.name - The name of the sponsor * @param {string} props.image - The image URL of the sponsor * @param {string} props.website - The website URL of the sponsor * @param {string} props.tier - The tier of the sponsor * @param {string} props.tagline - The tagline of the sponsor * @returns {JSX.Element} */ function SponsorCard({ name, image, website, tier, tagline }) { // for crystal tier only text, for bronze slightly bigger text, for silver bigger clickable text, // for gold text with image, for platinum and titanium text with big image return (
          {image && (
          )}
          {name}
          {tagline &&
          {tagline}
          } {website && {website}}
          ); } /** * Handle link click * @param {MouseEvent} e * @returns */ function handleLinkClick(e) { const target = e.target.closest(".sponsor-card"); if (!target) return; const { website } = target.dataset; if (!website) return; if (!website.startsWith("http")) { website = "http://" + website; } system.openInBrowser(website); } ================================================ FILE: src/pages/sponsors/style.scss ================================================ #sponsors-page { .tier-icon { display: inline-block; width: 24px; height: 24px; border-radius: 50%; margin-right: 8px; &.crystal { background: linear-gradient(45deg, #95a5a6, #bdc3c7); } &.bronze { background: linear-gradient(45deg, #cd7f32, #ff8c42); } &.silver { background: linear-gradient(45deg, #c0c0c0, #e6e6fa); } &.gold { background: linear-gradient(45deg, #ffd700, #ffed4e); } &.platinum { background: linear-gradient(45deg, #e5e4e2, #ffffff); } &.titanium { background: linear-gradient(45deg, #878681, #c4c4b0); } } .cta-section { text-align: center; } .cta-button { background: linear-gradient(45deg, #ff6b6b, #4ecdc4); color: white; border: none; padding: 18px 40px; border-radius: 30px; font-size: 18px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-bottom: 15px; width: 90%; max-width: 400px; display: flex; align-items: center; justify-content: center; margin: auto; .icon { margin-left: 10px; } } .cta-text { font-size: 14px; opacity: 0.8; line-height: 1.4; padding: 1rem; } .sponsors-container { padding: 1rem; h2 { text-align: center; font-weight: 700; } .tier-name { font-size: 18px; font-weight: 600; display: flex; align-items: center; margin: 1rem 0 0.5rem 0; } .sponsors { display: flex; flex-wrap: wrap; gap: 1rem; &:empty::after { content: attr(data-empty-message); display: block; width: 100%; padding: 2rem; font-style: italic; } } .sponsor-card { flex-grow: 1; min-width: 150px; background: var(--popup-background-color); color: var(--popup-text-color); border-radius: 12px; padding: 15px; text-align: center; backdrop-filter: blur(10px); border: 1px solid var(--border-color); transition: all 0.3s ease; .sponsor-avatar { width: 50px; height: 50px; border-radius: 50%; display: flex; margin: 0 auto 10px; align-items: center; justify-content: center; font-weight: bold; font-size: 18px; overflow: hidden; background-color: currentColor; img { max-width: 100%; max-height: 100%; object-fit: cover; } } .sponsor-name { font-size: 14px; font-weight: 500; margin-bottom: 5px; } .sponsor-website { opacity: 0.6; font-size: 12px; } &.crystal, &.bronze, &.silver { .sponsor-avatar { display: none; } } &.bronze, &.crystal { pointer-events: none; } &.silver, &.bronze, &.gold, &.platinum, &.titanium { .sponsor-name { font-size: 16px; } } &.platinum, &.titanium { .sponsor-avatar { width: 100px; height: 100px; } } } } } ================================================ FILE: src/pages/themeSetting/index.js ================================================ export default function themeSetting(...args) { import(/* webpackChunkName: "themeSetting" */ "./themeSetting").then( (module) => { module.default(...args); }, ); } ================================================ FILE: src/pages/themeSetting/themeSetting.js ================================================ import "./themeSetting.scss"; import { javascript } from "@codemirror/lang-javascript"; // For CodeMirror preview import { EditorState } from "@codemirror/state"; import { oneDark } from "@codemirror/theme-one-dark"; import { getThemeConfig, getThemeExtensions, getThemes } from "cm/themes"; import { basicSetup, EditorView } from "codemirror"; import Page from "components/page"; import searchBar from "components/searchbar"; import TabView from "components/tabView"; import alert from "dialogs/alert"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; import removeAds from "lib/removeAds"; import appSettings from "lib/settings"; import { hideAd } from "lib/startAd"; import CustomTheme from "pages/customTheme"; import ThemeBuilder from "theme/builder"; import themes from "theme/list"; import helpers from "utils/helpers"; export default function () { const $page = Page(strings.theme.capitalize()); const $search = ; const $themePreview = (
          ); const list = new Ref(); let cmPreview = null; const previewDoc = `// Acode is awesome!\nconst message = "Welcome to Acode";\nconsole.log(message);`; function destroyPreview(context) { if (!cmPreview) return; try { cmPreview.destroy(); } catch (error) { console.warn(`Failed to destroy theme preview (${context}).`, error); } finally { cmPreview = null; } } function createPreview(themeId) { destroyPreview("create"); const theme = getThemeExtensions(themeId, [oneDark]); const fixedHeightTheme = EditorView.theme({ "&": { height: "100%", flex: "1 1 auto" }, ".cm-scroller": { height: "100%", overflow: "auto" }, }); const state = EditorState.create({ doc: previewDoc, extensions: [basicSetup, javascript(), fixedHeightTheme, ...theme], }); cmPreview = new EditorView({ state, parent: $themePreview }); cmPreview.contentDOM.setAttribute("aria-readonly", "true"); } actionStack.push({ id: "appTheme", action: () => { destroyPreview("close"); $page.hide(); $page.removeEventListener("click", clickHandler); }, }); $page.onhide = () => { hideAd(); actionStack.remove("appTheme"); }; $page.body = (
          App Editor
          ); $page.querySelector("header").append($search); app.append($page); renderAppThemes(); helpers.showAd(); $page.addEventListener("click", clickHandler); function renderAppThemes() { // Remove and destroy CodeMirror preview when showing app themes destroyPreview("switch-tab"); $themePreview.remove(); const content = []; if (!DOES_SUPPORT_THEME) { content.push(
          {strings["unsupported device"]}
          , ); } const currentTheme = appSettings.value.appTheme; let $currentItem; themes.list().forEach((themeSummary) => { const theme = themes.get(themeSummary.id); const isCurrentTheme = theme.id === currentTheme; const isPremium = theme.version === "paid" && IS_FREE_VERSION; const $item = ( setAppTheme(theme, isPremium)} /> ); content.push($item); if (isCurrentTheme) $currentItem = $item; }); list.el.content = content; $currentItem?.scrollIntoView(); } function renderEditorThemes() { const currentTheme = ( appSettings.value.editorTheme || "one_dark" ).toLowerCase(); if (innerHeight * 0.3 >= 120) { $page.body.append($themePreview); createPreview(currentTheme); } else { $themePreview.remove(); } const themeList = getThemes(); let $currentItem; list.el.content = themeList.map((t) => { const isCurrent = t.id === currentTheme; const $item = ( setEditorTheme({ caption: t.caption, theme: t.id })} /> ); if (isCurrent) $currentItem = $item; return $item; }); $currentItem?.scrollIntoView(); } /** * * @param {MouseEvent} e */ function clickHandler(e) { const $target = e.target; if (!($target instanceof HTMLElement)) return; const action = $target.getAttribute("action"); if (!action) return; switch (action) { case "search": searchBar(list.el); break; default: break; } } /** * Sets the selected theme * @param {ThemeBuilder} theme */ async function setAppTheme(theme, buy) { if (!DOES_SUPPORT_THEME) return; if (buy) { try { await removeAds(); renderAppThemes(); } catch (e) { return; } } if (theme.id === "custom") { CustomTheme(); return; } themes.apply(theme.id, true); updateCheckedItem(theme.name); } /** * Sets the selected editor theme * @param {object} param0 * @param {string} param0.theme */ function setEditorTheme({ caption, theme }) { if (appSettings.value.appTheme.toLowerCase() === "system") { alert( "Info", "App theme is set to 'System'. Changing the editor theme will not affect the editor appearance.", ); return; } const ok = editorManager.editor.setTheme(theme); if (!ok) { alert( "Invalid theme", "This editor theme is not compatible with Acode's CodeMirror runtime.", ); return; } if (cmPreview) createPreview(theme); appSettings.update( { editorTheme: theme, }, false, ); updateCheckedItem(caption); } /** * Updates the checked item * @param {string} theme */ function updateCheckedItem(theme) { list.get('[checked="true"]')?.uncheck(); list.get(`[theme="${theme}"]`)?.check(); } function Item({ name, swatches, onclick, isCurrent, isPremium }) { const check = ; const star = ; const $el = (
          {createSwatchPreview(swatches)}
          {name}
          {isCurrent && check} {isPremium && star}
          ); $el.uncheck = () => { check.remove(); $el.removeAttribute("checked"); }; $el.check = () => { $el.append(check); $el.setAttribute("checked", true); }; return $el; } function createSwatchPreview(swatches) { const colors = [...new Set((swatches || []).filter(Boolean))].slice(0, 3); while (colors.length < 3) { colors.push(colors[colors.length - 1] || "var(--border-color)"); } return ( ); } function getAppThemeSwatches(theme) { if (!theme) { return [ "var(--primary-color)", "var(--secondary-color)", "var(--active-color)", ]; } return [theme.primaryColor, theme.secondaryColor, theme.activeColor]; } function getEditorThemeSwatches(themeId) { const config = getThemeConfig(themeId); return [ config.background, config.keyword || config.function || config.foreground, config.string || config.variable || config.foreground, ]; } } ================================================ FILE: src/pages/themeSetting/themeSetting.scss ================================================ #theme-setting { display: flex; flex-direction: column; #theme-preview:not(:empty) { height: 120px; box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); box-shadow: 0 0 4px var(--box-shadow-color); pointer-events: none; } #theme-preview .cm-editor { width: 100%; } #theme-list { flex: 1; } .theme-swatch-slot { display: flex; align-items: center; justify-content: center; width: 60px; min-width: 60px; height: 60px; flex-shrink: 0; } .theme-swatch-preview { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr); grid-template-rows: repeat(2, minmax(0, 1fr)); width: 1.5rem; min-width: 1.5rem; height: 1.5rem; padding: 0.08rem; box-sizing: border-box; border: 1px solid var(--border-color); border: 1px solid color-mix(in srgb, var(--border-color), transparent 18%); border-radius: 0.45rem; background: var(--secondary-color); overflow: hidden; } .theme-swatch { display: block; min-width: 0; min-height: 0; } .theme-swatch-main { grid-row: 1 / 3; } } ================================================ FILE: src/pages/welcome/index.js ================================================ export { default } from "./welcome"; ================================================ FILE: src/pages/welcome/welcome.js ================================================ import "./welcome.scss"; import Logo from "components/logo"; import actionStack from "lib/actionStack"; import constants from "lib/constants"; import EditorFile from "lib/editorFile"; /** * Opens the Welcome tab as an EditorFile page */ export default function openWelcomeTab() { // Check if welcome tab is already open const existingFile = editorManager.files.find((f) => f.id === "welcome-tab"); if (existingFile) { existingFile.makeActive(); return; } const welcomeContent = createWelcomeContent(); const welcomeFile = new EditorFile("Welcome", { id: "welcome-tab", render: true, type: "page", content: welcomeContent, tabIcon: "icon acode", hideQuickTools: true, }); // Set custom subtitle for the header welcomeFile.setCustomTitle(() => "Get Started"); actionStack.push({ id: "welcome-tab", action: () => welcomeFile.remove(), }); } /** * Creates the welcome tab content * @returns {HTMLElement} */ function createWelcomeContent() { return (
          {/* Hero Section */}

          Welcome to Acode

          Powerful code editor for Android

          {/* Get Started Section */}

          GET STARTED

          acode.exec("new-file")} /> acode.exec("open-folder")} /> acode.exec("recent")} /> acode.exec("command-palette")} />
          {/* Configure Section */}

          CONFIGURE

          acode.exec("open", "settings")} /> acode.exec("change-app-theme")} /> acode.exec("open", "plugins")} />
          {/* Learn Section */}

          LEARN

          acode.exec("open", "help")} /> acode.exec("open", "about")} />
          {/* Links Section */}

          CONNECT

          ); } /** * Action row component */ function ActionRow({ icon, label, shortcut, onClick }) { return (
          {label} {shortcut && {shortcut}}
          ); } /** * Link item component - opens URL in external browser */ function LinkItem({ icon, label, url }) { const handleClick = (e) => { e.preventDefault(); system.openInBrowser(url); }; return ( {label} ); } ================================================ FILE: src/pages/welcome/welcome.scss ================================================ #welcome-tab { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; min-height: 100%; height: auto; padding: 32px 24px; overflow-y: auto; overflow-x: hidden; background-color: var(--secondary-color); // Hero Header .welcome-header { display: flex; align-items: center; gap: 16px; margin-bottom: 48px; .logo { width: 64px; height: 64px; flex-shrink: 0; &::after { background-size: 48px; } &::before { background: radial-gradient(circle, color-mix(in srgb, var(--button-background-color) 30%, transparent) 0%, transparent 70%); } } .welcome-header-text { h1 { font-size: 20px; font-weight: 600; color: var(--primary-text-color); margin: 0 0 4px 0; letter-spacing: -0.3px; } .tagline { font-size: 13px; color: color-mix(in srgb, var(--secondary-text-color) 60%, transparent); margin: 0; } } } // Section Styles .welcome-section { width: 100%; max-width: 400px; margin-bottom: 32px; .section-label { font-size: 11px; font-weight: 600; letter-spacing: 1px; color: color-mix(in srgb, var(--secondary-text-color) 50%, transparent); margin: 0 0 12px 0; padding-left: 4px; } } // Action List .action-list { display: flex; flex-direction: column; gap: 2px; } .action-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 6px; cursor: pointer; transition: background-color 0.15s ease; &:hover, &:active { background-color: color-mix(in srgb, var(--popup-background-color) 40%, transparent); } .icon { font-size: 16px; color: color-mix(in srgb, var(--secondary-text-color) 70%, transparent); width: 20px; text-align: center; } .action-label { flex: 1; font-size: 14px; color: var(--secondary-text-color); } .action-shortcut { font-size: 12px; font-family: 'Roboto Mono', monospace; color: color-mix(in srgb, var(--secondary-text-color) 40%, transparent); letter-spacing: 0.5px; } } // Links Section .welcome-links { margin-top: 16px; .link-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-start; } .link-item { display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 8px; text-decoration: none; color: var(--secondary-text-color); font-size: 13px; font-weight: 500; transition: all 0.15s ease; background-color: color-mix(in srgb, var(--popup-background-color) 25%, transparent); border: 1px solid color-mix(in srgb, var(--border-color) 20%, transparent); &:hover, &:active { background-color: color-mix(in srgb, var(--popup-background-color) 50%, transparent); border-color: color-mix(in srgb, var(--border-color) 40%, transparent); transform: translateY(-1px); } .icon { font-size: 16px; color: color-mix(in srgb, var(--secondary-text-color) 80%, transparent); } } } } // Responsive adjustments for smaller screens @media (max-width: 360px) { #welcome-tab { padding: 24px 16px; .welcome-header { flex-direction: column; text-align: center; margin-bottom: 36px; .logo { width: 56px; height: 56px; &::after { background-size: 40px; } } .welcome-header-text h1 { font-size: 18px; } } .welcome-section { margin-bottom: 24px; } .action-row { padding: 8px 10px; .action-label { font-size: 13px; } .action-shortcut { font-size: 11px; } } .welcome-links .link-row { justify-content: center; } } } // Larger screens - center content better @media (min-width: 600px) { #welcome-tab { .welcome-section { max-width: 480px; } .action-row { padding: 12px 16px; } } } // Discord icon .icon.discord { position: relative; &::before { content: ''; display: block; width: 16px; height: 16px; background-image: url(../../pages/about/discord.svg); background-size: contain; background-repeat: no-repeat; background-position: center; } } ================================================ FILE: src/palettes/changeEditorTheme/index.js ================================================ import { getThemes } from "cm/themes"; import palette from "components/palette"; import appSettings from "lib/settings"; export default function changeEditorTheme() { palette(generateHints, onselect, strings["editor theme"]); } function generateHints() { const themes = getThemes(); const current = String( appSettings.value.editorTheme || "one_dark", ).toLowerCase(); return themes.map((t) => { const isCurrent = current === t.id; return { value: t.id, text: `
          ${t.caption}${isCurrent ? 'current' : ""}
          `, }; }); } function onselect(themeId) { if (!themeId) return; const ok = editorManager.editor.setTheme(themeId); if (!ok) return; appSettings.update({ editorTheme: themeId }, false); } ================================================ FILE: src/palettes/changeEncoding/index.js ================================================ import fsOperation from "fileSystem"; import palette from "components/palette"; import confirm from "dialogs/confirm"; import encodings from "utils/encodings"; export default function changeEncoding() { palette(generateHints, reopenWithNewEncoding, strings.encoding); } function generateHints() { return Object.keys(encodings).map((id) => { const encoding = encodings[id]; const aliases = encoding.aliases.join(", "); return { value: id, text: `
          ${encoding.label} ${aliases}
          `, }; }); } export async function reopenWithNewEncoding(encoding) { const file = editorManager.activeFile; const editor = editorManager.editor; const message = strings["change encoding"] .replace("{file}", file.filename) .replace("{encoding}", encoding); const confirmation = await confirm(strings.warning, message); if (!confirmation) return; const text = await fsOperation(file.uri).readFile(encoding); const cursorPosition = editor.getCursorPosition(); file.encoding = encoding; file.session.setValue(text); file.isUnsaved = false; file.markChanged = false; editor.moveCursorToPosition(cursorPosition); editorManager.onupdate("encoding"); editorManager.emit("update", "encoding"); } ================================================ FILE: src/palettes/changeMode/index.js ================================================ import { getModes } from "cm/modelist"; import palette from "components/palette"; import helpers from "utils/helpers"; import Path from "utils/Path"; export default function changeMode() { palette(generateHints, onselect, strings["syntax highlighting"]); } function generateHints() { const modes = [...getModes()].sort((a, b) => a.caption.localeCompare(b.caption), ); const activeMode = editorManager.activeFile?.currentMode || ""; const activeIndex = modes.findIndex(({ mode }) => mode === activeMode); if (activeIndex > 0) { const [activeEntry] = modes.splice(activeIndex, 1); modes.unshift(activeEntry); } return modes.map(({ aliases = [], caption, extensions, mode }) => { const searchTerms = [caption, mode, extensions, ...aliases] .filter(Boolean) .join(" "); const title = caption.toLowerCase() === mode ? caption : `${caption} (${mode})`; return { active: mode === activeMode, value: mode, text: `
          ${title}
          `, }; }); } function onselect(mode) { const activeFile = editorManager.activeFile; let modeAssociated; try { modeAssociated = helpers.parseJSON(localStorage.modeassoc) || {}; } catch (error) { modeAssociated = {}; } modeAssociated[Path.extname(activeFile.filename)] = mode; localStorage.modeassoc = JSON.stringify(modeAssociated); activeFile.setMode(mode); } ================================================ FILE: src/palettes/changeTheme/index.js ================================================ import "./style.scss"; import palette from "components/palette"; import appSettings from "lib/settings"; import { isDeviceDarkTheme } from "lib/systemConfiguration"; import themes from "theme/list"; import { updateSystemTheme } from "theme/preInstalled"; import changeEditorTheme from "../changeEditorTheme"; export default function changeTheme(type = "editor") { if (type === "editor") return changeEditorTheme(); palette( () => generateHints(type), (value) => onselect(value), strings["app theme"], ); } function generateHints(type) { // Editor handled by changeEditorTheme // App themes const currentTheme = appSettings.value.appTheme; const availableThemes = themes .list() .filter((theme) => !(theme.version === "paid" && IS_FREE_VERSION)); return availableThemes.map((theme) => { const isCurrent = theme.id === currentTheme; return { value: JSON.stringify({ type: "app", theme: theme.id, }), text: `
          ${theme.name} ${isCurrent ? 'current' : ""}
          `, }; }); } let previousDark = isDeviceDarkTheme(); const updateTimeMs = 2000; let intervalId = null; function syncSystemTheme() { if (appSettings.value.appTheme.toLowerCase() === "system") { const isDark = isDeviceDarkTheme(); if (isDark !== previousDark) { previousDark = isDark; updateSystemTheme(isDark); } } } function startSystemThemeWatcher() { if (intervalId) return; intervalId = setInterval(syncSystemTheme, updateTimeMs); } function stopSystemThemeWatcher() { if (!intervalId) return; clearInterval(intervalId); intervalId = null; } function updateSystemThemeWatcher(theme) { if (String(theme).toLowerCase() === "system") { startSystemThemeWatcher(); syncSystemTheme(); return; } stopSystemThemeWatcher(); } updateSystemThemeWatcher(appSettings.value.appTheme); appSettings.on("update:appTheme", updateSystemThemeWatcher); function onselect(value) { if (!value) return; const selection = JSON.parse(value); updateSystemThemeWatcher(selection.theme); if (selection.type === "editor") { editorManager.editor.setTheme(selection.theme); appSettings.update( { editorTheme: selection.theme, }, false, ); } else { if (selection.theme === "custom") { CustomTheme(); return; } themes.apply(selection.theme, true); } } ================================================ FILE: src/palettes/changeTheme/style.scss ================================================ .theme-item { display: flex; align-items: center; justify-content: space-between; padding: 4px; width: 100%; span { font-size: 1rem; } .current { color: var(--error-text-color); background-color: var(--primary-color); border-radius: 5px; padding: 2px 6px; font-size: 0.8rem; margin-left: 8px; } } ================================================ FILE: src/palettes/commandPalette/index.js ================================================ import { executeCommand, getRegisteredCommands } from "cm/commandRegistry"; import palette from "components/palette"; import helpers from "utils/helpers"; export default async function commandPalette() { const recentCommands = RecentlyUsedCommands(); const { editor } = editorManager; const wasFocused = editor?.hasFocus ?? false; palette(generateHints, onselect, strings["type command"], () => { if (wasFocused) editor?.focus(); }); function generateHints() { const registeredCommands = getRegisteredCommands(); const hints = []; registeredCommands.forEach(({ name, description, key }) => { const keyLabel = key ? key.split("|")[0] : ""; const item = (recentlyUsed) => ({ value: name, text: `${description ?? name}${keyLabel}`, }); if (recentCommands.commands.includes(name)) { hints.unshift(item(true)); return; } hints.push(item(false)); }); return hints; } function onselect(value) { const executed = executeCommand(value, editorManager.editor); if (executed) recentCommands.push(value); } } function RecentlyUsedCommands() { return { /** * @returns {string[]} */ get commands() { return ( helpers.parseJSON(localStorage.getItem("recentlyUsedCommands")) || [] ); }, /** * Saves command to recently used commands * @param {string} command Command name * @returns {void} */ push(command) { const { commands } = this; if (commands.length > 10) { commands.pop(); } if (commands.includes(command)) { commands.splice(commands.indexOf(command), 1); } commands.unshift(command); localStorage.setItem("recentlyUsedCommands", JSON.stringify(commands)); }, }; } ================================================ FILE: src/palettes/findFile/index.js ================================================ import palette from "components/palette"; import files from "lib/fileList"; import openFile from "lib/openFile"; import recents from "lib/recents"; import helpers from "utils/helpers"; /** * @typedef {import('components/inputhints').HintModification} HintModification */ /**@type {HintModification} */ let hintsModification; export default async function findFile() { palette(generateHints, onselect, strings["type filename"], () => { files.off("add-file", onAddFile); files.off("remove-file", onRemoveFile); }); files.on("add-file", onAddFile); files.on("remove-file", onRemoveFile); /** * Generates hint for inputhints * @param {HintModification} hints Hint modification object */ async function generateHints(hints) { hintsModification = hints; const list = []; editorManager.files.forEach((file) => { const { uri, name } = file; let { location = "" } = file; if (location) { location = helpers.getVirtualPath(location); } list.push(hintItem(name, location, uri)); }); list.push(...files(hintItem)); return list; } function onselect(value) { if (!value) return; openFile(value); } } /** * Generates hint item for inputhints * @param {string|{name: string, path: string, url: string}} name Hint text * @param {string} path Hint subtext * @param {string} url Hint value * @returns {{text: string, value: string}} */ function hintItem(name, path, url) { if (typeof name === "object") { ({ name, path, url } = name); } const recent = recents.files.find((file) => file === url); let subText = (path || url) ?? strings["new file"]; if (subText.length > 50) { subText = `...${subText.slice(-50)}`; } return { text: `
          ${name} ${subText}
          `, value: url, }; } function onAddFile({ name, url, path: visiblePath }) { hintsModification?.add(hintItem(name, visiblePath, url)); } function onRemoveFile({ name, url, path: visiblePath }) { hintsModification?.remove(hintItem(name, visiblePath, url)); } ================================================ FILE: src/plugins/auth/package.json ================================================ { "name": "com.foxdebug.acode.rk.auth", "version": "1.0.0", "description": "Authentication plugin", "cordova": { "id": "com.foxdebug.acode.rk.auth", "platforms": [ "android" ] }, "keywords": [ "ecosystem:cordova", "cordova-android" ], "author": "@RohitKushvaha01", "license": "MIT" } ================================================ FILE: src/plugins/auth/plugin.xml ================================================ Authentication ================================================ FILE: src/plugins/auth/src/android/Authenticator.java ================================================ package com.foxdebug.acode.rk.auth; import android.util.Log; // Required for logging 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 PREFS_FILENAME = "acode_auth_secure"; private static final String KEY_TOKEN = "auth_token"; private EncryptedPreferenceManager prefManager; @Override protected void pluginInitialize() { Log.d(TAG, "Initializing Authenticator Plugin..."); this.prefManager = new EncryptedPreferenceManager(this.cordova.getContext(), PREFS_FILENAME); } @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); return true; case "saveToken": String token = args.getString(0); Log.d(TAG, "Saving new token..."); prefManager.setString(KEY_TOKEN, token); callbackContext.success(); return true; default: Log.w(TAG, "Attempted to call unknown action: " + action); return false; } } 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; } 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()); } }); } 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(); } return null; } } ================================================ FILE: src/plugins/auth/src/android/EncryptedPreferenceManager.java ================================================ package com.foxdebug.acode.rk.auth; import android.content.Context; import android.content.SharedPreferences; import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKeys; import java.io.IOException; import java.security.GeneralSecurityException; public class EncryptedPreferenceManager { private SharedPreferences sharedPreferences; /** * @param context The Android Context * @param prefName The custom name for your preference file (e.g., "user_session") */ public EncryptedPreferenceManager(Context context, String prefName) { try { String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC); // EncryptedSharedPreferences handles encryption of both Keys and Values sharedPreferences = EncryptedSharedPreferences.create( prefName, masterKeyAlias, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } catch (GeneralSecurityException | IOException e) { // Fallback to standard private preferences if hardware-backed encryption fails sharedPreferences = context.getSharedPreferences(prefName, Context.MODE_PRIVATE); } } // --- Reusable Methods --- public void setString(String key, String value) { sharedPreferences.edit().putString(key, value).apply(); } public String getString(String key, String defaultValue) { return sharedPreferences.getString(key, defaultValue); } public void setInt(String key, int value) { sharedPreferences.edit().putInt(key, value).apply(); } public int getInt(String key, int defaultValue) { return sharedPreferences.getInt(key, defaultValue); } public void remove(String key) { sharedPreferences.edit().remove(key).apply(); } public boolean exists(String key) { return sharedPreferences.contains(key); } public void clear() { sharedPreferences.edit().clear().apply(); } } ================================================ FILE: src/plugins/browser/android/com/foxdebug/browser/Browser.java ================================================ package com.foxdebug.browser; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; import android.net.Uri; import android.os.Build; import android.text.InputType; import android.text.TextUtils; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.webkit.ConsoleMessage; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import com.foxdebug.browser.Emulator; import com.foxdebug.browser.Menu; import com.foxdebug.system.Ui; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import android.app.AlertDialog; import android.app.DownloadManager; import android.content.Context; import android.net.Uri; import android.os.Environment; import android.webkit.DownloadListener; import android.webkit.URLUtil; import android.webkit.WebView; import android.widget.Toast; import android.os.Handler; import android.os.Looper; public class Browser extends LinearLayout { public int FILE_SELECT_CODE = 1; public Menu menu; public WebView webView; private Ui.Theme theme; public Context context; private TextView urlText; private LinearLayout main; private ImageView favicon; private TextView titleText; private ProgressBar loading; private Emulator deviceEmulator; public boolean emulator = false; public boolean console = false; public boolean desktopMode = false; private String url = ""; private String title = "Browser"; private int padding; private int fontSize; private int imageSize; private int titleHeight; private int titleTextHeight; private boolean onlyConsole; ValueCallback filePathCallback; final int REQUEST_SELECT_FILE = 1; public Browser(Context context, Ui.Theme theme, Boolean onlyConsole) { super(context); this.theme = theme; this.context = context; this.onlyConsole = onlyConsole; this.padding = Ui.dpToPixels(context, 5); this.fontSize = Ui.dpToPixels(context, 5); this.imageSize = Ui.dpToPixels(context, 35); this.titleHeight = Ui.dpToPixels(context, 45); this.titleTextHeight = Ui.dpToPixels(context, 35); this.init(); } public void init() { WebSettings settings; ImageButton menuIcon; ImageButton refreshIcon; LinearLayout titleLayout; FrameLayout faviconFrame; LinearLayout webViewContainer; LinearLayout.LayoutParams faviconFrameParams; favicon = createIcon(Ui.Icons.LOGO); menuIcon = createIconButton(Ui.Icons.MORE_VERT); menuIcon.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { menu.show(view); } } ); refreshIcon = createIconButton(Ui.Icons.REFRESH); refreshIcon.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { webView.reload(); } } ); loading = new ProgressBar(context, null, android.R.attr.progressBarStyle); loading.setLayoutParams( new LinearLayout.LayoutParams(this.imageSize, this.imageSize, 0) ); faviconFrame = new FrameLayout(context); faviconFrameParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ); faviconFrameParams.gravity = Gravity.CENTER_VERTICAL; faviconFrame.setLayoutParams(faviconFrameParams); faviconFrame.addView(favicon); faviconFrame.addView(loading); titleLayout = createTile(); titleLayout.addView(faviconFrame); titleText = onlyConsole ? createTextView(title) : createEditText(title); titleLayout.addView(titleText); if (!onlyConsole) { titleLayout.addView(refreshIcon); titleLayout.addView(menuIcon); } webView = new WebView(context); webView.setFocusable(true); webView.setFocusableInTouchMode(true); webView.setBackgroundColor(0xFFFFFFFF); webView.setDownloadListener(new DownloadListener() { @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) { String fileName = URLUtil.guessFileName(url, contentDisposition, mimeType); new Handler(Looper.getMainLooper()).post(() -> { new AlertDialog.Builder(getContext()) .setTitle("Download file") .setMessage("Do you want to download \"" + fileName + "\"?") .setPositiveButton("Yes", (dialog, which) -> { DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); request.setMimeType(mimeType); request.addRequestHeader("User-Agent", userAgent); request.setDescription("Downloading file..."); request.setTitle(fileName); request.allowScanningByMediaScanner(); request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); DownloadManager dm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE); dm.enqueue(request); Toast.makeText(getContext(), "Download started...", Toast.LENGTH_SHORT).show(); }) .setNegativeButton("Cancel", null) .show(); }); } }); fitWebViewTo(0, 0, 1); webView.setWebChromeClient(new BrowserChromeClient(this)); webView.setWebViewClient(new BrowserWebViewClient(this)); settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); settings.setAllowContentAccess(true); settings.setDisplayZoomControls(false); settings.setDomStorageEnabled(true); webViewContainer = new LinearLayout(context); webViewContainer.setGravity(Gravity.CENTER); webViewContainer.setLayoutParams( new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1 ) ); webViewContainer.setBackgroundColor(theme.get("primaryColor")); webViewContainer.addView(webView); setOrientation(LinearLayout.VERTICAL); setFocusableInTouchMode(true); setFocusable(true); createMenu(); addView(titleLayout); addView(webViewContainer); } private void createMenu() { Browser browser = this; menu = new Menu(context, theme); menu.addItem(Ui.Icons.DEVICES, "Devices", false); menu.addItem(Ui.Icons.NO_CACHE, "Disable Cache", false); menu.addItem(Ui.Icons.TERMINAL, "Console", false); menu.addItem(Ui.Icons.OPEN_IN_BROWSER, "Open in Browser"); menu.addItem(Ui.Icons.EXIT, "Exit"); menu.setCallback( new Menu.Callback() { @Override public void onSelect(String action, Boolean checked) { switch (action) { case "Devices": if (deviceEmulator == null) { createDeviceEmulatorLayout(); } emulator = checked; if (checked) { setDesktopMode(true); setConsoleVisible(false); menu.setChecked("Console", false); menu.setVisible("Console", false); addView(deviceEmulator); fitWebViewTo( deviceEmulator.getWidthProgress(), deviceEmulator.getHeightProgress(), deviceEmulator.getScaleProgress() ); } else { menu.setVisible("Console", true); removeView(deviceEmulator); fitWebViewTo(0, 0, 1); webView .getViewTreeObserver() .addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { webView .getViewTreeObserver() .removeOnGlobalLayoutListener(this); setDesktopMode(false); } } ); } break; case "Console": setConsoleVisible(checked); break; case "Disable Cache": webView .getSettings() .setCacheMode( checked ? WebSettings.LOAD_NO_CACHE : WebSettings.LOAD_DEFAULT ); break; case "Open in Browser": Intent browserIntent = new Intent( Intent.ACTION_VIEW, Uri.parse(url) ); context.startActivity(browserIntent); exit(); break; case "Exit": exit(); break; } } } ); } private void createDeviceEmulatorLayout() { Browser browser = this; deviceEmulator = new Emulator(context, theme); deviceEmulator.setReference(webView); deviceEmulator.setChangeListener( new Emulator.Callback() { @Override public void onChange(int width, int height, float scale) { fitWebViewTo(width, height, scale); } } ); } private void setTextViewProperties(TextView textView, int height) { LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, height, 1 ); params.gravity = Gravity.CENTER_VERTICAL; textView.setMaxLines(1); textView.setEllipsize(TextUtils.TruncateAt.END); textView.setSingleLine(true); textView.setHorizontallyScrolling(true); textView.setTextColor(theme.get("primaryTextColor")); textView.setLayoutParams(params); textView.setGravity(Gravity.CENTER_VERTICAL); } private void setDesktopMode(boolean enabled) { int width = 0; int height = 0; WebSettings webSettings = webView.getSettings(); desktopMode = enabled; webSettings.setUserAgentString( enabled ? "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" : null ); if (enabled) { width = webView.getMeasuredWidth(); height = webView.getMeasuredHeight(); } webSettings.setLoadWithOverviewMode(enabled); webSettings.setUseWideViewPort(enabled); webSettings.setLoadWithOverviewMode(enabled); webSettings.setSupportZoom(enabled); webSettings.setBuiltInZoomControls(enabled); webView.reload(); } public void setDesktopMode() { if (desktopMode) { int width = webView.getMeasuredWidth(); int height = webView.getMeasuredHeight(); updateViewportDimension(width, height); } } public void setUrl(String url) { this.url = url; setTitle(url); setProgressBarVisible(true); webView.loadUrl(url); } public void setTitle(String title) { this.title = title; titleText.setText(title); } public void setFavicon(Bitmap icon) { favicon.setImageBitmap(icon); } public void setConsoleVisible(boolean visible) { console = visible; String javascript = "document.dispatchEvent(new CustomEvent('%sconsole'))"; javascript = String.format(javascript, visible ? "show" : "hide"); webView.evaluateJavascript(javascript, null); } public void setProgressBarVisible(boolean visible) { loading.setVisibility(visible ? View.VISIBLE : View.GONE); } private void updateViewportDimension(int width, int height) { String script = "!function(){var e=document.head;if(e){e.querySelectorAll(\"meta[name=viewport]\").forEach(function(e){e.remove()});var t=document.createElement(\"meta\");t.name=\"viewport\",t.content=\"width=%s, height=%s, initial-scale=%s\",e.append(t)}}();"; String w = "device-width"; String h = "device-height"; String r = "1"; if (width > 0) { w = String.valueOf(width); r = "0.1"; h = ""; } if (height > 0) { h = String.valueOf(height); } webView.evaluateJavascript(String.format(script, w, h, r), null); } private void fitWebViewTo(int width, int height, float scale) { webView.setScaleX(scale); webView.setScaleY(scale); webView.setLayoutParams( new LinearLayout.LayoutParams( width == 0 ? ViewGroup.LayoutParams.MATCH_PARENT : width, height == 0 ? ViewGroup.LayoutParams.MATCH_PARENT : height ) ); if (width > 0 && height > 0) { updateViewportDimension(width, height); } } private void styleIcon(ImageView view) { int padding = Ui.dpToPixels(context, 7); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( imageSize, imageSize ); params.gravity = Gravity.CENTER_VERTICAL; view.setBackgroundDrawable(null); view.setLayoutParams(params); view.setScaleType(ImageView.ScaleType.FIT_CENTER); view.setAdjustViewBounds(true); view.setPadding(padding, padding, padding, padding); } private void keyboardVisible(boolean visible) { if (visible) { InputMethodManager imm = (InputMethodManager) context.getSystemService( Context.INPUT_METHOD_SERVICE ); imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } else { InputMethodManager imm = (InputMethodManager) context.getSystemService( Context.INPUT_METHOD_SERVICE ); imm.hideSoftInputFromWindow(webView.getWindowToken(), 0); } } public boolean goBack() { if (console) { menu.setChecked("Console", false); setConsoleVisible(false); return true; } if (webView.canGoBack()) { webView.goBack(); url = webView.getOriginalUrl(); return true; } return false; } private TextView createTextView(String text) { TextView textView = new TextView(context); setTextViewProperties(textView, this.titleHeight); textView.setText(text); return textView; } private EditText createEditText(String text) { EditText editText = new EditText(context); GradientDrawable background = new GradientDrawable(); String themeType = theme.getType(); int radius = this.titleTextHeight / 2; background.setCornerRadius(radius); background.setColor(themeType.equals("light") ? 0x11000000 : 0x11ffffff); editText.setBackground(background); setTextViewProperties(editText, this.titleTextHeight); editText.setText(text); editText.setPadding(radius, 0, radius, 0); editText.setTextSize(this.fontSize < 10 ? 10 : this.fontSize); editText.setInputType(InputType.TYPE_TEXT_VARIATION_URI); editText.setImeOptions(EditorInfo.IME_ACTION_GO); editText.setOnFocusChangeListener( new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { titleText.setText(url); keyboardVisible(true); } else { titleText.setText(title); keyboardVisible(false); } } } ); editText.setOnEditorActionListener( new EditText.OnEditorActionListener() { @Override public boolean onEditorAction( TextView v, int actionId, KeyEvent event ) { if (actionId == EditorInfo.IME_ACTION_GO) { String url = v.getText().toString(); if (!url.startsWith("http://") && !url.startsWith("https://")) { url = "http://" + url; } title = url; setUrl(url); editText.clearFocus(); keyboardVisible(false); return true; } return false; } } ); return editText; } private LinearLayout createTile() { return createTile(titleHeight); } private LinearLayout createTile(int height) { LinearLayout tile = new LinearLayout(context); tile.setOrientation(LinearLayout.HORIZONTAL); tile.setBackgroundColor(theme.get("primaryColor")); tile.setLayoutParams( new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, height) ); tile.setHorizontalGravity(Gravity.LEFT); tile.setVerticalGravity(Gravity.TOP); return tile; } private ImageButton createIconButton(String icon) { Bitmap bitmap = Ui.Icons.get(context, icon, theme.get("primaryTextColor")); ImageButton button = createIconButton(bitmap); return button; } private ImageButton createIconButton(Bitmap icon) { ImageButton button = new ImageButton(context); button.setImageBitmap(icon); styleIcon(button); return button; } private ImageView createIcon(String code) { Bitmap bitmap = Ui.Icons.get(context, code, theme.get("primaryTextColor")); ImageView icon = new ImageView(context); icon.setImageBitmap(bitmap); styleIcon(icon); return icon; } public void exit() { if (console) { setConsoleVisible(false); return; } if (webView.canGoBack()) { webView.goBack(); return; } webView.destroy(); ((Activity) context).finish(); } } class BrowserChromeClient extends WebChromeClient { Browser browser; public BrowserChromeClient(Browser browser) { super(); this.browser = browser; } @Override public void onReceivedTitle(WebView view, String title) { super.onReceivedTitle(view, title); browser.setTitle(title); } @Override public void onReceivedIcon(WebView view, Bitmap icon) { super.onReceivedIcon(view, icon); browser.setFavicon(icon); } public boolean onShowFileChooser( WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams ) { if (browser.filePathCallback != null) { browser.filePathCallback.onReceiveValue(null); } browser.filePathCallback = filePathCallback; String[] acceptTypes = fileChooserParams.getAcceptTypes(); String mimeType = "*/*"; // default if empty or null if (acceptTypes != null && acceptTypes.length > 0) { String firstType = acceptTypes[0]; if (firstType != null && !firstType.trim().isEmpty()) { mimeType = firstType; } } Intent selectDocument = new Intent(Intent.ACTION_GET_CONTENT); selectDocument.addCategory(Intent.CATEGORY_OPENABLE); selectDocument.setType(mimeType); boolean isMultiple = (fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE); if (isMultiple) { selectDocument.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); } ((Activity) webView.getContext()).startActivityForResult( Intent.createChooser(selectDocument, "Select File"), browser.FILE_SELECT_CODE ); return true; } } class BrowserWebViewClient extends WebViewClient { private Browser browser; public BrowserWebViewClient(Browser browser) { super(); this.browser = browser; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { browser.setUrl(url); browser.setProgressBarVisible(true); return false; } @Override public void onPageStarted(WebView view, String url, Bitmap icon) { super.onPageStarted(view, url, icon); browser.setProgressBarVisible(true); } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); browser.setProgressBarVisible(false); // Inject console for external sites // this is not a good solution but for now its good, later we'll improve this if (!url.startsWith("http://localhost")) { try { File eruaFile = new File(browser.context.getFilesDir(), "eruda.js"); StringBuilder scriptContent = new StringBuilder(); BufferedReader reader = new BufferedReader(new FileReader(eruaFile)); String line; while ((line = reader.readLine()) != null) { scriptContent.append(line); scriptContent.append("\n"); } reader.close(); // Inject the script content directly String script = "if(!window.eruda){" + " var script = document.createElement('script');" + " script.textContent = `" + scriptContent.toString() + "`;" + " document.head.appendChild(script);" + " eruda.init({" + " theme: 'dark'" + " });" + " eruda._shadowRoot.querySelector('.eruda-entry-btn').style.display = 'none';" + " sessionStorage.setItem('__console_available', true);" + " document.addEventListener('showconsole', function() { eruda.show(); });" + " document.addEventListener('hideconsole', function() { eruda.hide(); });" + "}"; browser.webView.evaluateJavascript(script, null); } catch (IOException e) { e.printStackTrace(); // Fallback to CDN if local file fails String fallbackScript = "if(!window.eruda){" + " var script = document.createElement('script');" + " script.src = 'https://cdn.jsdelivr.net/npm/eruda';" + " script.crossOrigin = 'anonymous';" + " script.onload = function() {" + " eruda.init({" + " theme: 'dark'" + " });" + " eruda._shadowRoot.querySelector('.eruda-entry-btn').style.display = 'none';" + " sessionStorage.setItem('__console_available', true);" + " document.addEventListener('showconsole', function() { eruda.show(); });" + " document.addEventListener('hideconsole', function() { eruda.hide(); });" + " };" + " document.head.appendChild(script);" + "}"; browser.webView.evaluateJavascript(fallbackScript, null); } browser.menu.setChecked("Console", false); } else { browser.webView.evaluateJavascript( "sessionStorage.getItem('__console_available')", new ValueCallback() { @Override public void onReceiveValue(String value) { boolean show = !value.equals("null"); browser.menu.setVisible("Console", show && !browser.emulator); // If user had toggled "Console" on before, re-show it if (browser.console && show) { browser.setConsoleVisible(true); browser.menu.setChecked("Console", true); } } } ); } } @Override public void onLoadResource(WebView view, String url) { browser.setDesktopMode(); } } ================================================ FILE: src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java ================================================ package com.foxdebug.browser; import android.app.Activity; import android.content.Intent; import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.Window; import android.view.WindowInsetsController; import android.webkit.WebChromeClient; import com.foxdebug.system.Ui; import org.json.JSONObject; public class BrowserActivity extends Activity { private Browser browser; private Ui.Theme theme; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); String url = intent.getStringExtra("url"); String themeString = intent.getStringExtra("theme"); boolean onlyConsole = intent.getBooleanExtra("onlyConsole", false); try { JSONObject obj = new JSONObject(themeString); theme = new Ui.Theme(obj); } catch (Exception e) { theme = new Ui.Theme(new JSONObject()); } browser = new Browser(this, theme, onlyConsole); browser.setUrl(url); setContentView(browser); setSystemTheme(theme.get("primaryColor")); } @Override public void onBackPressed() { boolean didGoBack = browser.goBack(); if (!didGoBack) { browser.exit(); } } private void setSystemTheme(int systemBarColor) { try { Ui.Icons.setSize(Ui.dpToPixels(this, 18)); final Window window = getWindow(); // Method and constants not available on all SDKs but we want to be able to compile this code with any SDK window.clearFlags(0x04000000); // SDK 19: WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(0x80000000); // SDK 21: WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); try { // Using reflection makes sure any 5.0+ device will work without having to compile with SDK level 21 window .getClass() .getMethod("setNavigationBarColor", int.class) .invoke(window, systemBarColor); window .getClass() .getMethod("setStatusBarColor", int.class) .invoke(window, systemBarColor); if (Build.VERSION.SDK_INT < 30) { setStatusBarStyle(window); setNavigationBarStyle(window); } else { String themeType = theme.getType(); WindowInsetsController controller = window.getInsetsController(); int appearance = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; if (themeType.equals("light")) { controller.setSystemBarsAppearance(appearance, appearance); } else { controller.setSystemBarsAppearance(0, appearance); } } } catch (IllegalArgumentException error) { Log.w("BrowserActivity", "Invalid system bar color or appearance input.", error); } catch (Exception error) { Log.w("BrowserActivity", "Failed applying system bar theme values.", error); } } catch (Exception e) { Log.e("BrowserActivity", "Failed to apply system theme.", e); } } private void setStatusBarStyle(final Window window) { View decorView = window.getDecorView(); int uiOptions = decorView.getSystemUiVisibility(); String themeType = theme.getType(); if (themeType.equals("light")) { decorView.setSystemUiVisibility( uiOptions | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR ); return; } } private void setNavigationBarStyle(final Window window) { View decorView = window.getDecorView(); int uiOptions = decorView.getSystemUiVisibility(); String themeType = theme.getType(); if (themeType.equals("light")) { decorView.setSystemUiVisibility(uiOptions | 0x80000000 | 0x00000010); return; } } @Override protected void onActivityResult( int requestCode, int resultCode, Intent data ) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == browser.FILE_SELECT_CODE) { if (browser.filePathCallback == null) { return; } browser.filePathCallback.onReceiveValue( WebChromeClient.FileChooserParams.parseResult(resultCode, data) ); browser.filePathCallback = null; } } } ================================================ FILE: src/plugins/browser/android/com/foxdebug/browser/Emulator.java ================================================ package com.foxdebug.browser; import android.content.Context; import android.graphics.Typeface; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.SeekBar; import android.widget.TextView; import com.foxdebug.system.Ui; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; public class Emulator extends LinearLayout { private Ui.Theme theme; private View reference; private Context context; private Callback listener; private Device customDevice; private Device selectedDevice; private boolean initialized = false; private LinearLayout seekBarsLayout; private DeviceListView deviceListView; private HashMap seekBars = new HashMap(); public abstract static class Callback { public abstract void onChange(int width, int height, float scale); } public Emulator(Context context, Ui.Theme theme) { super(context); View border; LinearLayout main; this.context = context; this.theme = theme; customDevice = new Device("Custom", 0, 0, Ui.Icons.TUNE, false); deviceListView = new DeviceListView(context, theme); deviceListView.setLayoutParams( new LinearLayout.LayoutParams( Ui.dpToPixels(context, 100), LinearLayout.LayoutParams.MATCH_PARENT ) ); deviceListView.setOnSelect( new DeviceListView.Callback() { @Override public void onSelect(Device device) { selectDevice(device); } } ); deviceListView.add( customDevice, new Device("iPhone SE", 320, 568, Ui.Icons.PHONE_APPLE), new Device("iPhone 8", 375, 667, Ui.Icons.PHONE_APPLE), new Device("iPhone 8+", 414, 736, Ui.Icons.PHONE_APPLE), new Device("iPhone X", 375, 812, Ui.Icons.PHONE_APPLE), new Device("iPad", 768, 1024, Ui.Icons.TABLET_APPLE), new Device("iPad Pro", 1024, 1366, Ui.Icons.TABLET_APPLE), new Device("Galaxy S5", 360, 640, Ui.Icons.PHONE_ANDROID), new Device("Pixel 2", 411, 731, Ui.Icons.PHONE_ANDROID), new Device("Pixel 2 XL", 411, 823, Ui.Icons.PHONE_ANDROID), new Device("Nexus 5X", 411, 731, Ui.Icons.PHONE_ANDROID), new Device("Nexus 6P", 411, 731, Ui.Icons.PHONE_ANDROID), new Device("Nexus 7", 600, 960, Ui.Icons.TABLET_ANDROID), new Device("Nexus 10", 800, 1280, Ui.Icons.TABLET_ANDROID), new Device("Laptop", 1280, 800, Ui.Icons.LAPTOP), new Device("Laptop L", 1440, 900, Ui.Icons.LAPTOP), new Device("Laptop XL", 1680, 1050, Ui.Icons.LAPTOP), new Device("UHD 4k", 3840, 2160, Ui.Icons.TV) ); deviceListView.select(customDevice); seekBarsLayout = new LinearLayout(context); seekBarsLayout.setPadding(0, 10, 0, 0); seekBarsLayout.setOrientation(LinearLayout.VERTICAL); seekBarsLayout.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1 ) ); border = new View(context); border.setBackgroundColor(theme.get("borderColor")); border.setLayoutParams( new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1) ); main = new LinearLayout(context); main.setOrientation(LinearLayout.HORIZONTAL); main.setBackgroundColor(theme.get("primaryColor")); main.addView(seekBarsLayout); main.addView(deviceListView); main.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) ); addControl("width", 50, "Width"); addControl("height", 50, "Height"); addControl("scale", 50, "Scale"); addView(border); addView(main); setOrientation(LinearLayout.VERTICAL); } public void setChangeListener(Callback listener) { this.listener = listener; } public void setReference(View view) { SeekBar widthSeekBar = seekBars.get("width"); SeekBar heightSeekBar = seekBars.get("height"); SeekBar scaleSeekBar = seekBars.get("scale"); int maxWidth = view.getMeasuredWidth(); int maxHeight = view.getMeasuredHeight(); int width = widthSeekBar.getProgress(); int height = heightSeekBar.getProgress(); int minWidth = widthSeekBar.getMin(); int minHeight = heightSeekBar.getMin(); getViewTreeObserver() .addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { getViewTreeObserver().removeOnGlobalLayoutListener(this); int correctedHeight = maxHeight - getHeight(); int iHeight = height; int iWidth = width; widthSeekBar.setMax(maxWidth); heightSeekBar.setMax(correctedHeight); if (width > maxWidth || !initialized) { heightSeekBar.setProgress(maxHeight); iHeight = maxHeight; } if (height > correctedHeight || !initialized) { widthSeekBar.setProgress(maxWidth); iWidth = maxWidth; } setMaxScale(iWidth, iHeight); scaleSeekBar.setMin(100); scaleSeekBar.setProgress(100); if (listener != null) { listener.onChange(iWidth, correctedHeight, 1); } } } ); } public int getWidthProgress() { return seekBars.get("width").getProgress(); } public int getHeightProgress() { return seekBars.get("height").getProgress(); } public float getScaleProgress() { return seekBars.get("scale").getProgress() / 100f; } private void addControl(String id, int height, String label) { LinearLayout linearLayout = new LinearLayout(context); TextView textView = new TextView(context); SeekBar seekBar = new SeekBar(context); linearLayout.setOrientation(LinearLayout.VERTICAL); linearLayout.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) ); seekBar.setMin(300); seekBar.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, height, 1 ) ); textView.setText(String.format(label, 0)); textView.setPadding(10, 0, 0, 0); linearLayout.addView(textView); linearLayout.addView(seekBar); seekBarsLayout.addView(linearLayout); seekBars.put(id, seekBar); seekBar.setOnSeekBarChangeListener( new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged( SeekBar seekBar, int progress, boolean fromUser ) { if (!fromUser || listener == null) { return; } String seekBarName = seekBar == seekBars.get("width") ? "width" // : seekBar == seekBars.get("height") // ? "height" // : "scale"; // Log.d("Emulator", seekBarName); int height = getHeightProgress(); int width = getWidthProgress(); float scale = getScaleProgress(); if (seekBarName != "scale") { SeekBar scaleSeekBar = seekBars.get("scale"); setMaxScale(width, height); scale = 1; } listener.onChange(width, height, scale); if ( seekBarName == "scale" || selectedDevice == null || selectedDevice.id == customDevice.id ) { return; } selectDevice(customDevice); } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) {} } ); } private void selectDevice(Device device) { if (selectedDevice != null) selectedDevice.deselect(); device.select(); selectedDevice = device; if (device.id == customDevice.id) { return; } SeekBar widthSeekBar = seekBars.get("width"); SeekBar heightSeekBar = seekBars.get("height"); SeekBar scaleSeekBar = seekBars.get("scale"); int maxWidth = widthSeekBar.getMax(); int maxHeight = heightSeekBar.getMax(); int maxScale; int width = device.width; int height = device.height; if (width > maxWidth) { float ratio = (width - maxWidth) / (float) width; width = maxWidth; height = (int) (height - (height * ratio)); } if (height > maxHeight) { float ratio = (height - maxHeight) / (float) height; height = maxHeight; width = (int) (width - (width * ratio)); } widthSeekBar.setProgress(width); heightSeekBar.setProgress(height); setMaxScale(width, height); maxScale = scaleSeekBar.getMax(); scaleSeekBar.setProgress(maxScale); listener.onChange(width, height, maxScale / 100f); } private void setMaxScale(int width, int height) { SeekBar scaleSeekBar = seekBars.get("scale"); SeekBar widthSeekBar = seekBars.get("width"); SeekBar heightSeekBar = seekBars.get("height"); int maxWidth = widthSeekBar.getMax(); int maxHeight = heightSeekBar.getMax(); float scaleX = maxWidth / (float) width; float scaleY = maxHeight / (float) height; int scale = (int) (Math.min(scaleX, scaleY) * 100); scaleSeekBar.setMax(scale); scaleSeekBar.setProgress(100); } } class Device { public int id; public int width; public int height; public String name; public String icon; public DeviceView view; public boolean isDesktop; public Device( String name, int width, int height, String icon, boolean isDesktop ) { this.id = View.generateViewId(); this.name = name; this.icon = icon; this.width = width; this.height = height; this.isDesktop = isDesktop; } public Device(String name, int width, int height, String icon) { this(name, width, height, icon, true); } public Device(String name, int width, int height) { this(name, width, height, Ui.Icons.TUNE, true); } public void select() { if (view != null) { view.select(); } } public void deselect() { if (view != null) { view.deselect(); } } } class DeviceListView extends ScrollView { DeviceView selectedDeviceView; LinearLayout deviceListLayout; Callback callback; Context context; Ui.Theme theme; public abstract static class Callback { public abstract void onSelect(Device device); } public DeviceListView(Context context, Ui.Theme theme) { super(context); this.context = context; this.theme = theme; deviceListLayout = new LinearLayout(context); deviceListLayout.setOrientation(LinearLayout.VERTICAL); deviceListLayout.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) ); addView(deviceListLayout); } public void add(Device... devices) { for (Device device : devices) { add(device); } } public void select(Device device) { DeviceView deviceView = (DeviceView) findViewById(device.id); deviceView.select(); selectedDeviceView = deviceView; } public void add(Device device) { DeviceView deviceView = new DeviceView(context, device, theme); deviceListLayout.addView(deviceView); deviceView.setOnSelect( new DeviceView.Callback() { @Override public void onSelect(DeviceView view) { if (callback == null) return; if (selectedDeviceView != null) selectedDeviceView.deselect(); view.select(); callback.onSelect(view.device); selectedDeviceView = view; } } ); } public void setOnSelect(Callback callback) { this.callback = callback; } } class DeviceView extends LinearLayout { Ui.Theme theme; Device device; TextView label; ImageView icon; Context context; boolean isSelected = false; public abstract static class Callback { public abstract void onSelect(DeviceView device); } public DeviceView(Context context, Device device, Ui.Theme theme) { super(context); int primaryTextColor = theme.get("primaryTextColor"); this.theme = theme; this.device = device; this.context = context; this.device.view = this; setId(device.id); setClickable(true); setPadding(0, 5, 0, 5); setOrientation(LinearLayout.HORIZONTAL); icon = new ImageView(context); icon.setImageBitmap(Ui.Icons.get(context, device.icon, primaryTextColor)); icon.setPadding(0, 0, 5, 0); icon.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ) ); label = new TextView(context); label.setSingleLine(true); label.setText(device.name); label.setTextColor(primaryTextColor); addView(icon); addView(label); } public void setOnSelect(Callback callback) { DeviceView self = this; setOnClickListener( new View.OnClickListener() { @Override public void onClick(View view) { callback.onSelect(self); } } ); } public void deselect() { int primaryTextColor = theme.get("primaryTextColor"); icon.setImageBitmap(Ui.Icons.get(context, device.icon, primaryTextColor)); label.setTextColor(primaryTextColor); label.setTypeface(null, Typeface.NORMAL); isSelected = false; } public void select() { int activeTextColor = theme.get("activeTextColor"); icon.setImageBitmap(Ui.Icons.get(context, device.icon, activeTextColor)); label.setTextColor(activeTextColor); label.setTypeface(null, Typeface.BOLD); isSelected = true; } } ================================================ FILE: src/plugins/browser/android/com/foxdebug/browser/Menu.java ================================================ package com.foxdebug.browser; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.drawable.GradientDrawable; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.ScrollView; import android.widget.TextView; import com.foxdebug.acode.R; import com.foxdebug.system.Ui; public class Menu extends PopupWindow { private Ui.Theme theme; private LinearLayout list; private Callback callback; Context context; int itemHeight; int imageSize; int padding; public abstract static class Callback { public abstract void onSelect(String action, Boolean checked); } public Menu(Context context, Ui.Theme theme) { super(context); this.theme = theme; this.context = context; padding = Ui.dpToPixels(context, 5); imageSize = Ui.dpToPixels(context, 30); itemHeight = Ui.dpToPixels(context, 40); MenuItem exit; GradientDrawable border; MenuItem toggleDisableCache; border = new GradientDrawable(); border.setColor(theme.get("popupBackgroundColor")); border.setCornerRadius(Ui.dpToPixels(context, 8)); list = new LinearLayout(context); list.setOrientation(LinearLayout.VERTICAL); list.setBackgroundDrawable(border); list.setPadding(padding, padding, padding, padding); list.setLayoutParams( new LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ) ); ScrollView scrollView = new ScrollView(context); scrollView.addView(list); setElevation(10f); setFocusable(true); setContentView(scrollView); setBackgroundDrawable(border); setAnimationStyle(R.style.MenuAnimation); } public void setCallback(Callback callback) { this.callback = callback; } public void addItem(String icon, String text) { addItem(icon, text, null); } public void addItem(String icon, String text, Boolean toggle) { int textColor = theme.get("popupTextColor"); int background = theme.get("popupBackgroundColor"); MenuItem menuItem = new MenuItem(this, text); menuItem.setBackgroundColor(background); menuItem.setIcon(icon, textColor); menuItem.setText(text, textColor); menuItem.setOnClickListener( new MenuItemCallback() { @Override public void onClick(MenuItem menuItem) { callback.onSelect(menuItem.action, menuItem.checked); hide(); } } ); if (toggle != null) { menuItem.setChecked(toggle); } list.addView(menuItem); } public void setChecked(String action, Boolean checked) { for (int i = 0; i < list.getChildCount(); i++) { MenuItem menuItem = (MenuItem) list.getChildAt(i); if (menuItem.action == action) { menuItem.setChecked(checked); break; } } } public void setVisible(String action, Boolean visible) { for (int i = 0; i < list.getChildCount(); i++) { MenuItem menuItem = (MenuItem) list.getChildAt(i); if (menuItem.action == action) { menuItem.setVisibility(visible ? View.VISIBLE : View.GONE); break; } } } public void show(View view) { int x = view.getLeft(); int y = view.getTop(); showAtLocation(view, Gravity.TOP | Gravity.RIGHT, padding, padding); } public void hide() { dismiss(); } } abstract class MenuItemCallback { public abstract void onClick(MenuItem menuItem); } class MenuItem extends LinearLayout { private Context context; public Boolean checked; public CheckBox checkBox; public String action; private int textColor; private int itemHeight; private int imageSize; private int padding; private int iconSize; private int paddingLeft = 0; private int paddingRight = 0; private int paddingVertical = 0; public MenuItem(Menu menu, String action) { super(menu.context); this.action = action; context = menu.context; padding = menu.padding; imageSize = menu.imageSize; itemHeight = menu.itemHeight; iconSize = Ui.dpToPixels(context, 10); paddingRight = imageSize; paddingVertical = Ui.dpToPixels(context, 5); setPadding(); setClickable(true); setLayoutParams( new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, itemHeight ) ); setGravity(Gravity.CENTER_VERTICAL); setOrientation(LinearLayout.HORIZONTAL); addView(new TextView(context)); } public void setPadding() { setPadding(paddingLeft, paddingVertical, paddingRight, paddingVertical); } public void setIcon(String icon, int iconColor) { ImageView imageView = new ImageView(context); Bitmap iconBitmap = Ui.Icons.get(context, icon, iconColor); imageView.setImageBitmap(iconBitmap); imageView.setBackgroundDrawable(null); imageView.setLayoutParams( new LinearLayout.LayoutParams(imageSize, imageSize) ); imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); imageView.setAdjustViewBounds(true); imageView.setPadding(padding, padding, padding, padding); addView(imageView, 0); } public void setText(String text, int color) { textColor = color; TextView textView = new TextView(context); LinearLayout.LayoutParams textViewParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 1 ); textView.setText(text); textView.setTextColor(textColor); textView.setGravity(Gravity.CENTER_VERTICAL); textView.setLayoutParams(textViewParams); addView(textView, 1); } public void setChecked(boolean checked) { this.checked = checked; if (checkBox != null) { checkBox.setChecked(checked); return; } checkBox = new CheckBox(context); checkBox.setChecked(checked); checkBox.setEnabled(false); checkBox.setClickable(false); checkBox.setButtonTintList( new ColorStateList( new int[][] { new int[] { android.R.attr.state_checked }, new int[] { -android.R.attr.state_checked }, }, new int[] { textColor, textColor } ) ); FrameLayout container = new FrameLayout(context); FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams( imageSize, imageSize ); containerParams.gravity = Gravity.CENTER_VERTICAL; container.setLayoutParams(containerParams); container.addView(checkBox); paddingRight = 0; setPadding(); addView(container); } public void setOnClickListener(MenuItemCallback listener) { MenuItem self = this; this.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { if (checkBox != null) { checked = !checked; checkBox.setChecked(checked); } listener.onClick(self); } } ); } } ================================================ FILE: src/plugins/browser/android/com/foxdebug/browser/Plugin.java ================================================ package com.foxdebug.browser; import android.content.Intent; import com.foxdebug.browser.BrowserActivity; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class Plugin extends CordovaPlugin { @Override public boolean execute( String action, JSONArray args, CallbackContext callbackContext ) throws JSONException { if (action.equals("open")) { String url = args.getString(0); JSONObject theme = args.getJSONObject(1); boolean onlyConsole = args.optBoolean(2, false); String themeString = theme.toString(); Intent intent = new Intent(cordova.getActivity(), BrowserActivity.class); intent.putExtra("url", url); intent.putExtra("theme", themeString); intent.putExtra("onlyConsole", onlyConsole); cordova.getActivity().startActivity(intent); callbackContext.success("Opened browser"); return true; } return false; } } ================================================ FILE: src/plugins/browser/index.js ================================================ import settings from 'lib/settings'; import themes from 'theme/list'; const SERVICE = 'Browser'; function open(url, isConsole = false) { const ACTION = 'open'; const success = () => { }; const error = () => { }; const theme = themes.get(settings.value.appTheme).toJSON('hex'); cordova.exec(success, error, SERVICE, ACTION, [url, theme, isConsole]); } export default { open, }; ================================================ FILE: src/plugins/browser/package.json ================================================ { "name": "cordova-plugin-browser", "version": "1.0.0", "description": "", "main": "", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: src/plugins/browser/plugin.xml ================================================ cordova-plugin-browser In app browser Apache 2.0 ================================================ FILE: src/plugins/browser/res/android/menu_enter.xml ================================================ ================================================ FILE: src/plugins/browser/res/android/menu_exit.xml ================================================ ================================================ FILE: src/plugins/browser/res/android/styles.xml ================================================ ================================================ FILE: src/plugins/browser/utils/updatePackage.js ================================================ const fs = require("fs"); const path = require("path"); const configXML = path.resolve(__dirname, "../../../config.xml"); const menuJava = path.resolve( __dirname, "../../../platforms/android/app/src/main/java/com/foxdebug/browser/Menu.java" ); const docProvider = path.resolve( __dirname, "../../../platforms/android/app/src/main/java/com/foxdebug/acode/rk/exec/terminal/AlpineDocumentProvider.java" ); const repeatChar = (char, times) => char.repeat(times); function replaceImport(filePath, appName) { if (!fs.existsSync(filePath)) { console.warn(`⚠ File not found: ${filePath}`); return; } const data = fs.readFileSync(filePath, "utf8"); const updated = data.replace( /(import\s+com\.foxdebug\.)(acode|acodefree)(\.R;)/, `$1${appName}$3` ); fs.writeFileSync(filePath, updated); } try { if (!fs.existsSync(configXML)) { throw new Error("config.xml not found"); } const config = fs.readFileSync(configXML, "utf8"); const match = /widget\s+id="([0-9a-zA-Z.\-_]+)"/.exec(config); if (!match) { throw new Error("Could not extract widget id from config.xml"); } const appName = match[1].split(".").pop(); replaceImport(docProvider, appName); replaceImport(menuJava, appName); const msg = `==== Changed package to com.foxdebug.${appName} ====`; console.log("\n" + repeatChar("=", msg.length)); console.log(msg); console.log(repeatChar("=", msg.length) + "\n"); } catch (error) { console.error("❌ Error:", error.message); process.exit(1); } ================================================ FILE: src/plugins/cordova-plugin-buildinfo/.gitignore ================================================ .DS_Store ignore/ node_modules ================================================ FILE: src/plugins/cordova-plugin-buildinfo/.npmignore ================================================ .npmignore .travis.yml .DS_Store ignore/ node_modules ================================================ FILE: src/plugins/cordova-plugin-buildinfo/.travis.yml ================================================ language: node_js sudo: false node_js: - "4.2" ================================================ FILE: src/plugins/cordova-plugin-buildinfo/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Mikihiro Hayashi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: src/plugins/cordova-plugin-buildinfo/package.json ================================================ { "name": "cordova-plugin-buildinfo", "version": "4.0.0", "description": "Cordova/PhoneGap Build Information Plugin. Get PackageName, Version, Debug and more...", "cordova": { "id": "cordova-plugin-buildinfo", "platforms": [ "android" ] }, "keywords": [ "debug", "buildconfig", "buildinfo", "phonegap", "cordova", "ecosystem:cordova", "cordova-android" ], "scripts": { "test": "npm run jshint", "jshint": "node node_modules/jshint/bin/jshint www && node node_modules/jshint/bin/jshint src && node node_modules/jshint/bin/jshint tests" }, "engines": { "cordovaDependencies": { "0.0.0": { "cordova": ">=4.0.0" }, "2.0.0": { "cordova": ">=5.4.0", "cordova-android": ">=5.0.0" }, "3.0.0": { "cordova": ">=5.4.0", "cordova-android": ">=5.0.0" }, "4.0.0": { "cordova": ">=5.4.0", "cordova-android": ">=5.0.0" }, "5.0.0": { "cordova": ">100" } } }, "author": "Mikihiro Hayashi", "license": "MIT", "devDependencies": { "jshint": "^2.6.0" } } ================================================ FILE: src/plugins/cordova-plugin-buildinfo/plugin.xml ================================================ BuildInfo Cordova/Phonegap Build Information Plugin. Get PackageName, Version, Debug and more... MIT debug,buildconfig,buildinfo,phonegap,cordova Mikihiro Hayashi ================================================ FILE: src/plugins/cordova-plugin-buildinfo/scripts/after_install.js ================================================ /* The MIT License (MIT) Copyright (c) 2019 Mikihiro Hayashi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 'use strict'; const path = require('path'), fs = require('fs'); function installWindows(windowsPath) { const targetPath = path.join(windowsPath, 'CordovaApp.projitems'); let projitems = fs.readFileSync(targetPath).toString(); let changed = false; // Replace to if (projitems.match(/[\s]*?[\s]*?/m; const replace = "\r\n" + " \r\n" + " "; projitems = projitems.replace(search, replace); changed = true; } // Add if (!projitems.match(//; const replace = " \r\n" + " \r\n" + " $([System.DateTime]::Now.ToString(\"yyyy-MM-dd\THH:mm:sszzz\"))\r\n" + " \r\n" + " \r\n" + " \r\n" + " \r\n" + " \r\n" + " \r\n" + " \r\n" + " \r\n" + ""; projitems = projitems.replace(search, replace); changed = true; } // if variable "changed" is true, write to file. if (changed) { fs.writeFileSync(targetPath, projitems); } } module.exports = function (context) { const projectRoot = context.opts.projectRoot; // Exists platform/windows const windowsPath = path.join(projectRoot, 'platforms', 'windows'); if (fs.existsSync(windowsPath) && context.opts.plugin.platform == 'windows') { installWindows(windowsPath); } }; ================================================ FILE: src/plugins/cordova-plugin-buildinfo/scripts/before_uninstall.js ================================================ /* The MIT License (MIT) Copyright (c) 2019 Mikihiro Hayashi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 'use strict'; const path = require('path'), fs = require('fs'); function uninstallWindows(context, windowsPath) { const targetPath = path.join(windowsPath, 'CordovaApp.projitems'); let projitems = fs.readFileSync(targetPath).toString(); let changed = false; // Replace to if (projitems.match(/[\s]*?[\s]*?/m; const replace = "\r\n" + " \r\n" + " "; projitems = projitems.replace(search, replace); changed = true; } // Remove if (projitems.match(//gm; projitems = projitems.replace(search, ''); changed = true; } // if variable "changed" is true, write to file. if (changed) { fs.writeFileSync(targetPath, projitems); } } module.exports = function (context) { const opts = context.opts || {}; const projectRoot = opts.projectRoot; if ('string' != typeof projectRoot) { return; } // Exists platform/windows const windowsPath = path.join(projectRoot, 'platforms', 'windows'); if (context.opts.plugin.platform == 'windows' && fs.existsSync(windowsPath)) { uninstallWindows(context, windowsPath); } }; ================================================ FILE: src/plugins/cordova-plugin-buildinfo/scripts/browser_after_prepare.js ================================================ /* The MIT License (MIT) Copyright (c) 2019 Mikihiro Hayashi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 'use strict'; const path = require('path'), fs = require('fs'); function rewriteBuildInfoProxy(context, pathPrefix) { const opts = context.opts || {}; const options = opts.options || {}; const release = options.release || false; const debug = options.debug || !release; const pluginId = (opts.plugin || {}).id || 'cordova-plugin-buildinfo'; const pathRewriteFile = path.join(pathPrefix, 'www', 'plugins', pluginId, 'src', 'browser', 'BuildInfoProxy.js'); if (!fs.existsSync(pathRewriteFile)) { console.error('File not found: '.pathRewriteFile); return; } const ConfigParser = context.requireCordovaModule('cordova-common').ConfigParser; const cfg = new ConfigParser('config.xml'); const json = { debug: debug, packageName: cfg.packageName(), basePackageName: cfg.packageName(), name: cfg.name(), displayName: cfg.shortName(), version: cfg.version(), versionCode: cfg.version() }; const code = 'const json = ' + JSON.stringify(json) + ';'; const contentJs = fs.readFileSync(pathRewriteFile, 'utf8'); const outputJs = contentJs.replace(/(\/\* \*\/).*?(\s*)(\/\* <\/EMBED_CODE> \*\/)/s, '$1$2' + code + '$2$3'); fs.writeFileSync(pathRewriteFile, outputJs); } module.exports = function (context) { const opts = context.opts || {}; const projectRoot = opts.projectRoot; const platforms = opts.platforms || []; if ('string' != typeof projectRoot) { return; } ['browser', 'electron'].forEach((value, index, array) => { if (!platforms.includes(value)) { return; } const pathPrefix = path.join(projectRoot, 'platforms', value); if (fs.existsSync(pathPrefix)) { rewriteBuildInfoProxy(context, pathPrefix); } }); }; ================================================ FILE: src/plugins/cordova-plugin-buildinfo/src/android/BuildInfo.gradle ================================================ android { defaultConfig { //buildConfigField "long", "_BUILDINFO_TIMESTAMP", System.currentTimeMillis() + "L" } } ================================================ FILE: src/plugins/cordova-plugin-buildinfo/src/android/BuildInfo.java ================================================ /* The MIT License (MIT) Copyright (c) 2016 Mikihiro Hayashi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package org.apache.cordova.buildinfo; import android.app.Activity; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Log; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Field; import java.text.SimpleDateFormat; /** * BuildInfo Cordova Plugin * * @author Mikihiro Hayashi * @since 1.0.0 */ public class BuildInfo extends CordovaPlugin { private static final String TAG = "BuildInfo"; /** * Cache of result JSON */ private static JSONObject mBuildInfoCache; /** * Constructor */ public BuildInfo() { } /** * execute * @param action The action to execute. * @param args The exec() arguments. * @param callbackContext The callback context used when calling back into JavaScript. * @return * @throws JSONException */ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { if ("init".equals(action)) { String buildConfigClassName = null; if (1 < args.length()) { buildConfigClassName = args.getString(0); } init(buildConfigClassName, callbackContext); return true; } return false; } /** * init * @param buildConfigClassName null or specified BuildConfig class name * @param callbackContext */ private void init(String buildConfigClassName, CallbackContext callbackContext) { // Cached check if (null != mBuildInfoCache) { callbackContext.success(mBuildInfoCache); return; } // Load PackageInfo Activity activity = cordova.getActivity(); String packageName = activity.getPackageName(); String basePackageName = packageName; CharSequence displayName = ""; long firstInstallTime = 0; PackageManager pm = activity.getPackageManager(); try { PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); firstInstallTime = pi.firstInstallTime; if (null != pi.applicationInfo) { displayName = pi.applicationInfo.loadLabel(pm); } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } // Load BuildConfig class Class c = null; if (null == buildConfigClassName) { buildConfigClassName = packageName + ".BuildConfig"; } try { c = Class.forName(buildConfigClassName); } catch (ClassNotFoundException e) { } if (null == c) { basePackageName = activity.getClass().getPackage().getName(); buildConfigClassName = basePackageName + ".BuildConfig"; try { c = Class.forName(buildConfigClassName); } catch (ClassNotFoundException e) { callbackContext.error("BuildConfig ClassNotFoundException: " + e.getMessage()); return; } } // Create result mBuildInfoCache = new JSONObject(); try { boolean debug = getClassFieldBoolean(c, "DEBUG", false); mBuildInfoCache.put("packageName" , packageName); mBuildInfoCache.put("basePackageName", basePackageName); mBuildInfoCache.put("displayName" , displayName); mBuildInfoCache.put("name" , displayName); // same as displayName mBuildInfoCache.put("version" , getClassFieldString(c, "VERSION_NAME", "")); mBuildInfoCache.put("versionCode" , getClassFieldInt(c, "VERSION_CODE", 0)); mBuildInfoCache.put("debug" , debug); //mBuildInfoCache.put("buildDate" , convertLongToDateTimeString(getClassFieldLong(c, "_BUILDINFO_TIMESTAMP", 0L))); mBuildInfoCache.put("installDate" , convertLongToDateTimeString(firstInstallTime)); mBuildInfoCache.put("buildType" , getClassFieldString(c, "BUILD_TYPE", "")); mBuildInfoCache.put("flavor" , getClassFieldString(c, "FLAVOR", "")); if (debug) { Log.d(TAG, "packageName : \"" + mBuildInfoCache.getString("packageName") + "\""); Log.d(TAG, "basePackageName: \"" + mBuildInfoCache.getString("basePackageName") + "\""); Log.d(TAG, "displayName : \"" + mBuildInfoCache.getString("displayName") + "\""); Log.d(TAG, "name : \"" + mBuildInfoCache.getString("name") + "\""); Log.d(TAG, "version : \"" + mBuildInfoCache.getString("version") + "\""); Log.d(TAG, "versionCode : " + mBuildInfoCache.getInt("versionCode")); Log.d(TAG, "debug : " + (mBuildInfoCache.getBoolean("debug") ? "true" : "false")); Log.d(TAG, "buildType : \"" + mBuildInfoCache.getString("buildType") + "\""); Log.d(TAG, "flavor : \"" + mBuildInfoCache.getString("flavor") + "\""); //Log.d(TAG, "buildDate : \"" + mBuildInfoCache.getString("buildDate") + "\""); Log.d(TAG, "installDate : \"" + mBuildInfoCache.getString("installDate") + "\""); } } catch (JSONException e) { e.printStackTrace(); callbackContext.error("JSONException: " + e.getMessage()); return; } callbackContext.success(mBuildInfoCache); } /** * Get boolean of field from Class * @param c * @param fieldName * @param defaultReturn * @return */ private static boolean getClassFieldBoolean(Class c, String fieldName, boolean defaultReturn) { boolean ret = defaultReturn; Field field = getClassField(c, fieldName); if (null != field) { try { ret = field.getBoolean(c); } catch (IllegalAccessException iae) { iae.printStackTrace(); } } return ret; } /** * Get string of field from Class * @param c * @param fieldName * @param defaultReturn * @return */ private static String getClassFieldString(Class c, String fieldName, String defaultReturn) { String ret = defaultReturn; Field field = getClassField(c, fieldName); if (null != field) { try { ret = (String)field.get(c); } catch (IllegalAccessException iae) { iae.printStackTrace(); } } return ret; } /** * Get int of field from Class * @param c * @param fieldName * @param defaultReturn * @return */ private static int getClassFieldInt(Class c, String fieldName, int defaultReturn) { int ret = defaultReturn; Field field = getClassField(c, fieldName); if (null != field) { try { ret = field.getInt(c); } catch (IllegalAccessException iae) { iae.printStackTrace(); } } return ret; } /** * Get long of field from Class * @param c * @param fieldName * @param defaultReturn * @return */ private static long getClassFieldLong(Class c, String fieldName, long defaultReturn) { long ret = defaultReturn; Field field = getClassField(c, fieldName); if (null != field) { try { ret = field.getLong(c); } catch (IllegalAccessException iae) { iae.printStackTrace(); } } return ret; } /** * Get field from Class * @param c * @param fieldName * @return */ private static Field getClassField(Class c, String fieldName) { Field field = null; try { field = c.getField(fieldName); } catch (NoSuchFieldException nsfe) { nsfe.printStackTrace(); } return field; } private static String convertLongToDateTimeString(long mills) { SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); return formatter.format(mills); } } ================================================ FILE: src/plugins/cordova-plugin-buildinfo/www/buildinfo.js ================================================ /* The MIT License (MIT) Copyright (c) 2016 Mikihiro Hayashi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var exec = require('cordova/exec'); var channel = require('cordova/channel'); module.exports = { baseUrl: null, packageName: '', basePackageName: '', displayName: '', name: '', version: '', versionCode: 0, debug: false, //buildDate: null, installDate: null, buildType: '', flavor: '' }; function _buldinfoCheckCordovaPlatform() { var allowPlatforms = ['android', 'ios', 'windows', 'osx', 'browser', 'electron']; var platformId = (cordova && cordova.platformId) ? cordova.platformId : null; return (-1 !== allowPlatforms.indexOf(platformId)); } function _findBaseUrl() { var path = null; var scripts = document.getElementsByTagName('script'); var findScriptPath = '/cordova.js'; var findScriptPathLen = findScriptPath.length; for (var i = scripts.length - 1; i >= 0; i--) { var src = scripts[i].src.replace(/\?.*$/, ''); if (src.length >= findScriptPathLen && src.substring(src.length - findScriptPathLen) == findScriptPath) { path = src.substring(0, src.length - findScriptPathLen) + '/'; break; } } return path; } if (_buldinfoCheckCordovaPlatform()) { channel.onCordovaReady.subscribe(function () { // Platform Check var allowPlatforms = ['android', 'ios', 'windows', 'osx', 'browser', 'electron']; var platformId = (cordova && cordova.platformId) ? cordova.platformId : null; if (-1 == allowPlatforms.indexOf(platformId)) { console.debug('BuildInfo init skip.'); return; } var args = []; // Android Only // defined buildInfoBuildConfigClassName variable // BuildConfig class name. // ex: if ('undefined' !== typeof buildInfoBuildConfigClassName) { args.push(buildInfoBuildConfigClassName); } exec( function (res) { if (!res) { return; } module.exports.baseUrl = _findBaseUrl(); if ('undefined' !== typeof res.packageName) { module.exports.packageName = res.packageName; } if ('undefined' !== typeof res.basePackageName) { module.exports.basePackageName = res.basePackageName; } if ('undefined' !== typeof res.displayName) { module.exports.displayName = res.displayName; } if ('undefined' !== typeof res.name) { module.exports.name = res.name; } if ('undefined' !== typeof res.version) { module.exports.version = res.version; } if ('undefined' !== typeof res.versionCode) { module.exports.versionCode = res.versionCode; } if ('undefined' !== typeof res.debug) { module.exports.debug = res.debug; } if ('undefined' !== typeof res.buildType) { module.exports.buildType = res.buildType; } if ('undefined' !== typeof res.flavor) { module.exports.flavor = res.flavor; } /*if ('undefined' !== typeof res.buildDate) { if (res.buildDate instanceof Date) { module.exports.buildDate = res.buildDate; } else { module.exports.buildDate = new Date(res.buildDate); } }*/ if ('undefined' !== typeof res.installDate) { if (res.installDate instanceof Date) { module.exports.installDate = res.installDate; } else { module.exports.installDate = new Date(res.installDate); } } if ('undefined' !== typeof res.windows) { module.exports.windows = res.windows; } }, function (msg) { console.error('BuildInfo init fail'); console.error(msg); }, 'BuildInfo', 'init', args ); }); } ================================================ FILE: src/plugins/custom-tabs/package.json ================================================ { "name": "com.foxdebug.acode.rk.customtabs", "version": "1.0.0", "description": "Custom tabs api", "cordova": { "id": "com.foxdebug.acode.rk.customtabs", "platforms": [ "android" ] }, "keywords": [ "ecosystem:cordova", "cordova-android" ], "author": "@RohitKushvaha01", "license": "MIT" } ================================================ FILE: src/plugins/custom-tabs/plugin.xml ================================================ Custom Tabs ================================================ FILE: src/plugins/custom-tabs/src/CustomTabsPlugin.java ================================================ package com.foxdebug.acode.rk.customtabs; import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; import android.graphics.Color; import androidx.browser.customtabs.CustomTabsIntent; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.json.JSONArray; import org.json.JSONObject; public class CustomTabsPlugin extends CordovaPlugin { @Override public boolean execute( String action, JSONArray args, CallbackContext callbackContext ) { if ("open".equals(action)) { try { final String url = args.getString(0); final JSONObject options = args.optJSONObject(1) != null ? args.optJSONObject(1) : new JSONObject(); cordova.getActivity().runOnUiThread(() -> { try { openCustomTab(url, options); callbackContext.success(); } catch (Exception e) { callbackContext.error(e.getMessage()); } }); return true; } catch (Exception e) { callbackContext.error(e.getMessage()); return true; } } return false; } private void openCustomTab(String url, JSONObject options) { CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); String toolbarColor = options.optString("toolbarColor", null); if (toolbarColor != null && !toolbarColor.isEmpty()) { builder.setToolbarColor(Color.parseColor(toolbarColor)); } CustomTabsIntent customTabsIntent = builder.build(); if (options.optBoolean("showTitle", true)) { customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.SHOW_PAGE_TITLE); } try { customTabsIntent.launchUrl( cordova.getActivity(), Uri.parse(url) ); } catch (ActivityNotFoundException e) { // Fallback to default browser Intent fallback = new Intent( Intent.ACTION_VIEW, Uri.parse(url) ); cordova.getActivity().startActivity(fallback); } } } ================================================ FILE: src/plugins/custom-tabs/www/customtabs.js ================================================ var exec = require('cordova/exec'); exports.open = function (url, options, success, error) { exec( success, error, 'CustomTabs', 'open', [url, options || {}] ); }; ================================================ FILE: src/plugins/ftp/LICENSE.md ================================================ - GoldRaccoon is under [original author's license](https://github.com/albertodebortoli/GoldRaccoon/blob/master/LICENSE.markdown) - ftp4j is under [LGPL](http://opensource.org/licenses/LGPL-2.1) - All other codes (writen by me) are under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) ================================================ FILE: src/plugins/ftp/README.md ================================================ # cordova-plugin-ftp ## Description This cordova plugin is created to use ftp (client) in web/js. Support both **iOS** and **Android** platform now. You can do the following things: - List a directory - Create a directory - Delete a directory (must be empty) - Delete a file - Download a file (with percent info) - Upload a file (with percent info) - Cancel upload/download ## Installation ```sh $ cordova plugin add cordova-plugin-ftp $ cordova prepare ``` Dependency: - For iOS, the plugin depends on *CFNetwork.framework*, which has been added to plugin.xml (and `cordova prepare` will add it to platform project), so you don't need to do anything. - But for Android, it depends on *com.android.support:support-v4:23.2.0*, which should be added to your platform project by hand. ## Usage You can access this plugin by js object `window.cordova.plugin.ftp`. Example: ```js document.addEventListener("deviceready", onDeviceReady, false); function onDeviceReady() { // First of all, connect to ftp server address without protocol prefix. e.g. "192.168.1.1:21", "ftp.xfally.github.io" // Notice: address port is only supported for Android, if not given, default port 21 will be used. window.cordova.plugin.ftp.connect('ftp.xfally.github.io', 'username', 'password', function(ok) { console.info("ftp: connect ok=" + ok); // You can do any ftp actions from now on... window.cordova.plugin.ftp.upload('/localPath/localFile', '/remotePath/remoteFile', function(percent) { if (percent == 1) { console.info("ftp: upload finish"); } else { console.debug("ftp: upload percent=" + percent * 100 + "%"); } }, function(error) { console.error("ftp: upload error=" + error); }); }, function(error) { console.error("ftp: connect error=" + error); }); } ``` Please refer to [ftp.js](./www/ftp.js) for all available APIs and usages. ## Notice 1. For iOS, `ftp.connect` will always succeed (even if `username` and `password` are incorrect), but it does NOT mean the later actions, e.g. `ls`... `download` will succeed too! So always check their `errorCallback`. 2. Want to upload/download multiple files? The plugin (Android part) inits just one connection and transmits all files via it. If you use asychronous syntax (e.g. `foreach`) to start multiple upload/download in a short time, it may mess the transfer. Instead, you can try [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) or [async](https://github.com/caolan/async) to transmit one after one. ## Thanks - The iOS native implementing is based on [GoldRaccoon](https://github.com/albertodebortoli/GoldRaccoon). - The Android native implementing is based on [ftp4j](http://www.sauronsoftware.it/projects/ftp4j/) jar (LGPL). ## License Apache License 2.0. Refer to [LICENSE.md](./LICENSE.md) for more info. ================================================ FILE: src/plugins/ftp/index.d.ts ================================================ interface FtpOptions { connectionMode: 'passive' | 'active'; securityType: 'ftp' | 'ftps'; encoding: 'utf8' | 'binary'; } type SuccessCallback = (res: any) => void; type ErrorCallback = (err: any) => void; interface Ftp{ connect( host: string, port: number, username: string, password: string, options: FtpOptions, onSuccess: SuccessCallback, onError: ErrorCallback ): void; listDirectory( id: string, // connection id path: string, onSuccess: SuccessCallback, onError: ErrorCallback ): void; execCommand( id: string, // connection id command: string, onSuccess: SuccessCallback, onError: ErrorCallback, ...args: string, ): void; isConnected( id: string, // connection id onSuccess: SuccessCallback, onError: ErrorCallback, ): void; disconnect( id: string, // connection id onSuccess: SuccessCallback, onError: ErrorCallback, ): void; downloadFile( id: string, // connection id remotePath: string, localPath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; uploadFile( id: string, // connection id localPath: string, remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; deleteFile( id: string, // connection id remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; deleteDirectory( id: string, // connection id remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; createDirectory( id: string, // connection id remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; createFile( id: string, // connection id remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; getStat( id: string, // connection id remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; exists( id: string, // connection id remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; changeDirectory( id: string, // connection id remotePath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; changeToParentDirectory( id: string, // connection id onSuccess: SuccessCallback, onError: ErrorCallback, ): void; getWorkingDirectory( id: string, // connection id onSuccess: SuccessCallback, onError: ErrorCallback, ): void; rename( id: string, // connection id oldPath: string, newPath: string, onSuccess: SuccessCallback, onError: ErrorCallback, ): void; sendNoOp( id: string, // connection id onSuccess: SuccessCallback, onError: ErrorCallback, ): void; } declare var ftp: Ftp; ================================================ FILE: src/plugins/ftp/package.json ================================================ { "name": "cordova-plugin-ftp", "version": "1.1.1", "description": "This cordova plugin is created to use ftp (client) in web/js.", "cordova": { "id": "cordova-plugin-ftp", "platforms": [ "android", "ios" ] }, "repository": { "type": "git", "url": "https://github.com/xfally/cordova-plugin-ftp" }, "keywords": [ "cordova", "ftp", "cordova-android", "cordova-ios" ], "author": "pax", "license": "Apache-2.0", "bugs": { "url": "https://github.com/xfally/cordova-plugin-ftp/issues" }, "homepage": "https://github.com/xfally/cordova-plugin-ftp", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" } } ================================================ FILE: src/plugins/ftp/plugin.xml ================================================ Ftp Cordova Ftp Plugin MIT cordova,ftp ================================================ FILE: src/plugins/ftp/src/android/com/foxdebug/ftp/Ftp.java ================================================ package com.foxdebug.ftp; import android.app.Activity; import android.content.Context; import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.lang.SecurityException; import java.lang.reflect.Method; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import org.apache.commons.net.ftp.*; import org.apache.commons.net.ftp.parser.ParserInitializationException; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class Ftp extends CordovaPlugin { HashMap ftpProfiles = new HashMap(); Context context; Activity activity; String connectionID; public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); context = cordova.getContext(); activity = cordova.getActivity(); } public boolean execute( String action, JSONArray args, CallbackContext callback ) { try { Method method = this.getClass() .getDeclaredMethod(action, JSONArray.class, CallbackContext.class); if (method != null) { method.invoke(this, args, callback); return true; } return false; } catch (NoSuchMethodException e) { callback.error(e.getMessage()); return false; } catch (SecurityException e) { callback.error(e.getMessage()); return false; } catch (Exception e) { callback.error(e.getMessage()); return false; } } public void connect(JSONArray args, CallbackContext callback) { connect(args, callback, false); } public void connect( JSONArray args, CallbackContext callback, boolean isRetry ) { cordova .getThreadPool() .execute( new Runnable() { public void run() { int reply; int port = args.optInt(1); String host = args.optString(0); String username = args.optString(2); String password = args.optString(3); String defaultPath = args.optString(4); String securityType = args.optString(5); String connectionMode = args.optString(6); String encryption = args.optString(7); String encoding = args.optString(8); String ftpId = getFtpId(host, port, username); FTPClient ftp = null; try { if (ftpProfiles.containsKey(ftpId)) { ftp = ftpProfiles.get(ftpId); reply = ftp.getReplyCode(); if (ftp.isConnected() && FTPReply.isPositiveCompletion(reply)) { ftp.setControlEncoding("UTF-8"); ftp.setAutodetectUTF8(true); System.setProperty("ftp.client.encoding", "UTF-8"); // test if connection is still valid ftp.sendNoOp(); Log.d("FTP", "FTPClient (" + ftpId + ") is connected"); callback.success(ftpId); return; } Log.d("FTP", "FTPClient (" + ftpId + ") is not connected"); ftp.disconnect(); Log.d("FTP", "FTPClient (" + ftpId + ") disconnecting..."); } else { Log.d("FTP", "Creating new FTPClient (" + ftpId + ")"); ftp = new FTPClient(); ftpProfiles.put(ftpId, ftp); } Log.d("FTP", "FTPClient (" + ftpId + ") connecting..."); ftp.connect(host, port); ftp.setControlKeepAliveTimeout(300); if (connectionMode.equals("active")) { Log.d("FTP", "Entering Local Active mode"); ftp.enterLocalActiveMode(); } else { Log.d("FTP", "Entering Passive Active mode"); ftp.enterLocalPassiveMode(); } Log.d("FTP", "FTPClient (" + ftpId + ") logging in..."); ftp.login(username, password); reply = ftp.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { ftp.disconnect(); Log.d( "FTP", "FTPClient (" + ftpId + ") server refused connection." ); callback.error("FTP server refused connection."); return; } ftp.setListHiddenFiles(true); ftpProfiles.put(ftpId, ftp); Log.d("FTP", "FTPClient (" + ftpId + ") connected"); callback.success(ftpId); } catch (IOException e) { Log.e("FTP", "FTPClient (" + ftpId + ")", e); if (ftp != null) { ftpProfiles.remove(ftpId); } if (!isRetry) { connect(args, callback, true); return; } callback.error(e.getMessage()); } catch (Exception e) { Log.e("FTP", "FTPClient (" + ftpId + ")", e); if (ftp != null) { ftpProfiles.remove(ftpId); } if (!isRetry) { connect(args, callback, true); return; } callback.error(e.getMessage()); } } } ); } private static String getBaseName(String path) { int lastSepIndex = path.lastIndexOf('/'); if (lastSepIndex == path.length() - 1) { return getBaseName(path.substring(0, lastSepIndex)); } return path.substring(lastSepIndex + 1); } private static String getParentPath(String path) { int lastSepIndex = path.lastIndexOf('/'); if (lastSepIndex == path.length() - 1) { lastSepIndex = path.substring(0, lastSepIndex).lastIndexOf('/'); } return path.substring(0, lastSepIndex); } public void listDirectory(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String path = args.optString(1); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { path = "/"; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } FTPFile[] files = ftp.listFiles(path); Log.d( "FTP", "FTPClient (" + ftpId + ") Listing files in " + path ); Log.d( "FTP", "FTPClient (" + ftpId + ") Found " + files.length + " files." ); JSONArray jsonFiles = new JSONArray(); for (FTPFile file : files) { String filename = file.getName(); if (filename.equals(".") || filename.equals("..")) { continue; } JSONObject jsonFile = new JSONObject(); jsonFile.put("name", filename); jsonFile.put("length", file.getSize()); jsonFile.put("url", joinPath(path, filename)); if (file.isSymbolicLink()) { jsonFile.put("isLink", true); String linkTarget = file.getLink(); jsonFile.put("link", linkTarget); String linkPath = linkTarget.startsWith("/") ? linkTarget : joinPath(path, linkTarget); try { FTPFile[] targetFiles = ftp.listFiles(linkPath); if (targetFiles.length > 0) { FTPFile targetFile = targetFiles[0]; jsonFile.put("isFile", targetFile.isFile()); jsonFile.put("isDirectory", targetFile.isDirectory()); jsonFile.put("url", linkPath); } else { jsonFile.put("isFile", false); jsonFile.put("isDirectory", false); } } catch (Exception e) { // Handle broken symlink jsonFile.put("isFile", false); jsonFile.put("isDirectory", false); } } else { jsonFile.put("isLink", false); jsonFile.put("isDirectory", file.isDirectory()); jsonFile.put("isFile", file.isFile()); jsonFile.put("link", null); } jsonFile.put( "lastModified", file.getTimestamp().getTimeInMillis() ); jsonFile.put( "canWrite", file.hasPermission( FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION ) ); jsonFile.put( "canRead", file.hasPermission( FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION ) ); jsonFiles.put(jsonFile); } callback.success(jsonFiles); } catch (ParserInitializationException e) { callback.error(e.getMessage()); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void exists(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { String ftpId = args.optString(0); String path = args.optString(1); try { if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { path = "/"; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } // check if file or directory exists FTPFile[] ftpFiles = ftp.listFiles(path); if (ftpFiles.length > 0) { callback.success(1); } else { callback.success(0); } } catch (ParserInitializationException e) { Log.e("FTP", "FTPClient (" + ftpId + ") path: " + path, e); callback.error(e.getMessage()); Log.e("FTP", "FTPClient (" + ftpId + ") path: " + path, e); } catch (FTPConnectionClosedException e) { Log.e("FTP", "FTPClient (" + ftpId + ") path: " + path, e); callback.error(e.getMessage()); } catch (IOException e) { Log.e("FTP", "FTPClient (" + ftpId + ") path: " + path, e); callback.error(e.getMessage()); } catch (Exception e) { Log.e("FTP", "FTPClient (" + ftpId + ") path: " + path, e); callback.error(e.getMessage()); } } } ); } public void sendNoOp(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.sendNoOp(); callback.success(); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void deleteFile(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String path = args.optString(1); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { callback.error("Path is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.deleteFile(path); callback.success(); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void deleteDirectory(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String path = args.optString(1); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { callback.error("Path is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } Log.d("FTP", "Deleting directory " + path); // delete all files in the directory emptyDirectory(path, ftp); callback.success(); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void rename(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String oldPath = args.optString(1); String newPath = args.optString(2); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (oldPath == null || oldPath.isEmpty()) { callback.error("Old path is required."); return; } if (newPath == null || newPath.isEmpty()) { callback.error("New path is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } // get list of files in the parent directory String parentPath = getParentPath(oldPath); FTPFile[] ftpFiles = ftp.listFiles(parentPath); Log.d("FTP", "Renaming " + oldPath + " to " + newPath); ftp.rename(oldPath, newPath); // check if file is renamed successfully FTPFile[] newFile = ftp.listFiles(newPath); if (newFile.length > 0) { callback.success(newPath); } else { // get latest list of files in the parent directory FTPFile[] latestFtpFiles = ftp.listFiles(parentPath); // some time src file is renamed and not moved to destination // check if for changed file and rename it original name FTPFile changedFile = null; for (FTPFile file : latestFtpFiles) { boolean found = false; for (FTPFile oldFile : ftpFiles) { if (oldFile.getName().equals(file.getName())) { found = true; break; } } if (!found) { changedFile = file; break; } } if (changedFile != null) { String changedFilePath = joinPath( parentPath, changedFile.getName() ); ftp.rename(changedFilePath, oldPath); } callback.error("Failed to rename file"); } callback.success(); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void downloadFile(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String path = args.optString(1); String localFilePath = args.optString(2); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { callback.error("Path is required."); return; } if (localFilePath == null || localFilePath.isEmpty()) { callback.error("Local file is required."); return; } URI uri = new URI(localFilePath); File localFile = new File(uri); FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.setFileType(FTP.BINARY_FILE_TYPE); // Delete existing cache file to prevent stale content if (localFile.exists()) { localFile.delete(); } try ( InputStream inputStream = ftp.retrieveFileStream(path) ) { if (inputStream == null) { Log.d( "FTP", "FTPClient (" + ftpId + ") path: " + path + " - not found" ); callback.error("File not found."); return; } try ( FileOutputStream outputStream = new FileOutputStream(localFile) ) { byte[] buffer = new byte[1024]; int bytesRead = -1; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } } if (!ftp.completePendingCommand()) { ftp.logout(); ftp.disconnect(); callback.error("File transfer failed."); return; } callback.success(); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (URISyntaxException e) { callback.error(e.getMessage()); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void uploadFile(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String localFilePath = args.optString(1); String remoteFilePath = args.optString(2); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (remoteFilePath == null || remoteFilePath.isEmpty()) { callback.error("Path is required."); return; } if (localFilePath == null || localFilePath.isEmpty()) { callback.error("Local file is required."); return; } Log.d("FTPUpload", "uploadFile: " + localFilePath); URI uri = new URI(localFilePath); File localFile = new File(uri); FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.setFileType(FTP.BINARY_FILE_TYPE); Log.d("FTPUpload", "Destination " + remoteFilePath); try ( InputStream inputStream = new FileInputStream(localFile); OutputStream outputStream = ftp.storeFileStream(remoteFilePath) ) { if (outputStream == null) { callback.error("File not found."); return; } byte[] buffer = new byte[1024]; int bytesRead = -1; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } if (!ftp.completePendingCommand()) { ftp.logout(); ftp.disconnect(); callback.error("File transfer failed."); return; } callback.success(); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (URISyntaxException e) { callback.error(e.getMessage()); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void getKeepAlive(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } callback.success((int) ftp.getControlKeepAliveTimeout()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void execCommand(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String command = args.optString(1); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (command == null || command.isEmpty()) { callback.error("Command is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.sendCommand(command); String reply = ftp.getReplyString(); callback.success(reply); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void isConnected(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } boolean connected = ftp.isConnected(); callback.success(connected ? 1 : 0); } catch (Exception e) { Log.e("FTP", "FTPClient", e); callback.error(e.getMessage()); } } } ); } public void disconnect(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); FTPClient ftp = ftpProfiles.get(ftpId); if (ftp != null) { ftp.disconnect(); ftpProfiles.remove(ftpId); } callback.success(); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void createDirectory(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String path = args.optString(1); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { callback.error("Path is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.makeDirectory(path); callback.success(); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void changeDirectory(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String path = args.optString(1); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { callback.error("Path is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.changeWorkingDirectory(path); callback.success(); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void changeToParentDirectory( JSONArray args, CallbackContext callback ) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } ftp.changeToParentDirectory(); callback.success(); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void getWorkingDirectory(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } String workingDirectory = ftp.printWorkingDirectory(); callback.success(workingDirectory); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } public void getStat(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String ftpId = args.optString(0); String path = args.optString(1); if (ftpId == null || ftpId.isEmpty()) { callback.error("FTP ID is required."); return; } if (path == null || path.isEmpty()) { callback.error("Path is required."); return; } FTPClient ftp = ftpProfiles.get(ftpId); if (ftp == null) { callback.error("FTP client not found."); return; } FTPFile[] files = ftp.listFiles(path); if (files == null || files.length == 0) { callback.error("File not found."); return; } FTPFile file = files[0]; JSONObject stat = new JSONObject(); stat.put("isFile", file.isFile()); stat.put("isValid", file.isValid()); stat.put("isUnknown", file.isUnknown()); stat.put("isDirectory", file.isDirectory()); stat.put("isLink", file.isSymbolicLink()); stat.put("linkCount", file.getHardLinkCount()); stat.put("length", file.getSize()); stat.put("name", getBaseName(file.getName())); stat.put("lastModified", file.getTimestamp().getTimeInMillis()); stat.put("link", file.getLink()); stat.put("group", file.getGroup()); stat.put("user", file.getUser()); stat.put( "canWrite", file.hasPermission( FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION ) ); stat.put( "canRead", file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION) ); callback.success(stat); } catch (ParserInitializationException e) { callback.error(e.getMessage()); } catch (FTPConnectionClosedException e) { callback.error(e.getMessage()); } catch (IOException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } private String getFtpId(String host, int port, String username) { return username + "@" + host + ":" + port; } private String errMessage(Exception e) { String res = e.getMessage(); if (res == null || res.equals("")) { return e.toString(); } return res; } private void emptyDirectory(String directory, FTPClient client) throws FTPConnectionClosedException, IOException { FTPFile[] files = client.listFiles(directory); for (FTPFile file : files) { String filename = file.getName(); if (filename.equals(".") || filename.equals("..")) { continue; } if (file.isDirectory()) { Log.d("FTP", "Removing directory: " + file.getName()); emptyDirectory(directory + "/" + file.getName(), client); } else { Log.d("FTP", "Removing file: " + file.getName()); client.deleteFile(directory + "/" + file.getName()); } } client.removeDirectory(directory); } private String joinPath(String p1, String p2) { if (!p1.endsWith("/")) { p1 += "/"; } return p1 + p2; } } ================================================ FILE: src/plugins/ftp/www/ftp.js ================================================ module.exports = { connect: function (host, port, username, password, options, onSuccess, onFail) { if (typeof port != 'number') { throw new Error('Port must be number'); } port = Number.parseInt(port); var connectionMode = "passive"; var securityType = "ftp"; var encoding = "utf8"; if (typeof options === 'function') { onFail = onSuccess; onSuccess = options; options = {}; } if (options) { if (options.connectionMode) { connectionMode = options.connectionMode; } if (options.securityType) { securityType = options.securityType; } if (options.encoding) { encoding = options.encoding; } } cordova.exec(onSuccess, onFail, 'Ftp', 'connect', [ host, port, username, password, connectionMode, securityType, encoding ]); }, listDirectory: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'listDirectory', [id, path]); }, execCommand: function (id, command, onSuccess, onFail, args) { cordova.exec(onSuccess, onFail, 'Ftp', 'execCommand', [id, command, args]); }, isConnected: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'isConnected', [id]); }, disconnect: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'disconnect', [id]); }, downloadFile: function (id, remotePath, localPath, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'downloadFile', [id, remotePath, localPath]); }, uploadFile: function (id, localPath, remotePath, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'uploadFile', [id, localPath, remotePath]); }, deleteFile: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'deleteFile', [id, path]); }, deleteDirectory: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'deleteDirectory', [id, path]); }, createDirectory: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'createDirectory', [id, path]); }, createFile: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'createFile', [id, path]); }, getStat: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'getStat', [id, path]); }, exists: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'exists', [id, path]); }, changeDirectory: function (id, path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'changeDirectory', [id, path]); }, changeToParentDirectory: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'changeToParentDirectory', [id]); }, getWorkingDirectory: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'getWorkingDirectory', [id]); }, rename: function (id, oldPath, newPath, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'rename', [id, oldPath, newPath]); }, getKeepAlive: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'getKeepAlive', [id]); }, sendNoOp: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Ftp', 'sendNoOp', [id]); } } ================================================ FILE: src/plugins/iap/index.d.ts ================================================ interface Iap { getProducts( skuList: Array, onSuccess: (skuList: Array) => void, onError: (err: String) => Error, ): void; setPurchaseUpdatedListener( onSuccess: (purchase: Object) => void, onError: (err: string) => void, ): void; startConnection( onSuccess: (responseCode: number) => void, onError: (err: string) => void, ): void; consume( purchaseToken: string, onSuccess: (responseCode: number) => void, onError: (err: string) => void, ): void; purchase( skuId: string, onSuccess: (responseCode: number) => void, onError: (err: string) => void, ): void; getPurchases( onSuccess: (purchaseList: Array) => void, onError: (err: string) => void, ): void; OK: 0; BILLING_UNAVAILABLE: 3; DEVELOPER_ERROR: 5; ERROR: 6; FEATURE_NOT_SUPPORTED: -2; ITEM_ALREADY_OWNED: 7; ITEM_NOT_OWNED: 8; ITEM_UNAVAILABLE: 4; SERVICE_DISCONNECTED: -1; SERVICE_TIMEOUT: -3; SERVICE_UNAVAILABLE: 2; USER_CANCELED: 1; PURCHASE_STATE_PURCHASED: 1; PURCHASE_STATE_PENDING: 2; PURCHASE_STATE_UNKNOWN: 0; } declare var iap: Iap; ================================================ FILE: src/plugins/iap/package.json ================================================ { "name": "cordova-plugin-iap", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } ================================================ FILE: src/plugins/iap/plugin.xml ================================================ cordova-plugin-iap In app purchase for Android. Apache 2.0 cordova,plugin,in app purchase ================================================ FILE: src/plugins/iap/src/com/foxdebug/iap/Iap.java ================================================ package com.foxdebug.iap; import android.app.Activity; import android.content.Context; import android.util.Log; import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClient.BillingResponseCode; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PendingPurchasesParams; import com.android.billingclient.api.ProductDetails; import com.android.billingclient.api.ProductDetailsResponseListener; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.PurchasesResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.QueryProductDetailsParams; import com.android.billingclient.api.QueryProductDetailsResult; import com.android.billingclient.api.QueryPurchasesParams; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class Iap extends CordovaPlugin { private BillingClient billingClient; private WeakReference contextRef; private WeakReference activityRef; private CallbackContext purchaseUpdated; public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); contextRef = new WeakReference<>(cordova.getContext()); activityRef = new WeakReference<>(cordova.getActivity()); billingClient = getBillingClient(); } @Override public boolean execute( String action, JSONArray args, CallbackContext callbackContext ) throws JSONException { String arg1 = getString(args, 0); switch (action) { case "startConnection": case "getProducts": case "setPurchaseUpdatedListener": case "purchase": case "consume": case "getPurchases": case "acknowledgePurchase": break; default: return false; } cordova .getThreadPool() .execute( new Runnable() { public void run() { switch (action) { case "startConnection": startConnection(callbackContext); break; case "getProducts": getProducts(getStringList(args, 0), callbackContext); break; case "setPurchaseUpdatedListener": setPurchaseUpdatedListener(callbackContext); break; case "purchase": purchase(arg1, callbackContext); break; case "consume": consume(arg1, callbackContext); break; case "getPurchases": getPurchases(callbackContext); break; case "acknowledgePurchase": acknowledgePurchase(arg1, callbackContext); break; } } } ); return true; } private BillingClient getBillingClient() { return BillingClient.newBuilder(this.contextRef.get()) .enablePendingPurchases( PendingPurchasesParams.newBuilder().enableOneTimeProducts().build() ) .setListener( new PurchasesUpdatedListener() { public void onPurchasesUpdated( BillingResult billingResult, List purchases ) { try { int responseCode = billingResult.getResponseCode(); if (responseCode == BillingResponseCode.OK) { JSONArray result = new JSONArray(); for (Purchase purchase : purchases) { result.put(purchaseToJson(purchase)); } sendPurchasePluginResult( new PluginResult(PluginResult.Status.OK, result) ); } else { sendPurchasePluginResult( new PluginResult(PluginResult.Status.ERROR, responseCode) ); } } catch (JSONException e) { sendPurchasePluginResult( new PluginResult(PluginResult.Status.ERROR, e.getMessage()) ); } } } ) .build(); } private void setPurchaseUpdatedListener(CallbackContext callbackContext) { purchaseUpdated = callbackContext; } private void consume(String token, CallbackContext callbackContext) { ConsumeParams consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(token) .build(); billingClient.consumeAsync( consumeParams, new ConsumeResponseListener() { public void onConsumeResponse( BillingResult billingResult, String purchaseToken ) { int responseCode = billingResult.getResponseCode(); if (responseCode == BillingResponseCode.OK) { callbackContext.success(responseCode); } else { callbackContext.error(responseCode); } } } ); } private void startConnection(CallbackContext callbackContext) { try { if (billingClient == null) { billingClient = getBillingClient(); } billingClient.startConnection( new BillingClientStateListener() { public void onBillingSetupFinished(BillingResult billingResult) { int responseCode = billingResult.getResponseCode(); if (responseCode == BillingResponseCode.OK) { callbackContext.success(responseCode); } else { callbackContext.error(responseCode); } } public void onBillingServiceDisconnected() { callbackContext.error("Billing service disconnected"); } } ); } catch (SecurityException e) { callbackContext.error(e.getMessage()); } } private void getProducts( List idList, CallbackContext callbackContext ) { if (billingClient == null) { billingClient = getBillingClient(); callbackContext.error("Billing client is not connected"); return; } List productList = new ArrayList<>(); for (String productId : idList) { productList.add( QueryProductDetailsParams.Product.newBuilder() .setProductId(productId) .setProductType(BillingClient.ProductType.INAPP) .build() ); } QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() .setProductList(productList) .build(); billingClient.queryProductDetailsAsync( params, new ProductDetailsResponseListener() { public void onProductDetailsResponse( BillingResult billingResult, QueryProductDetailsResult queryProductDetailsResult ) { try { int responseCode = billingResult.getResponseCode(); if (responseCode == BillingResponseCode.OK) { List productDetailsList = queryProductDetailsResult.getProductDetailsList(); JSONArray products = new JSONArray(); for (ProductDetails productDetails : productDetailsList) { JSONObject product = new JSONObject(); ProductDetails.OneTimePurchaseOfferDetails offerDetails = productDetails.getOneTimePurchaseOfferDetails(); if (offerDetails != null) { product.put("productId", productDetails.getProductId()); product.put("title", productDetails.getTitle()); product.put("description", productDetails.getDescription()); product.put("price", offerDetails.getFormattedPrice()); product.put( "priceAmountMicros", offerDetails.getPriceAmountMicros() ); product.put( "priceCurrencyCode", offerDetails.getPriceCurrencyCode() ); product.put("type", productDetails.getProductType()); } products.put(product); } callbackContext.success(products); } else { callbackContext.error(responseCode); } } catch (JSONException e) { callbackContext.error(e.getMessage()); } } } ); } private void purchase(String productIdOrJson, CallbackContext callbackContext) { try { if (productIdOrJson == null || productIdOrJson.trim().isEmpty()) { callbackContext.error("Product ID cannot be null or empty"); return; } final String productId = productIdOrJson; List productList = new ArrayList<>(); productList.add( QueryProductDetailsParams.Product.newBuilder() .setProductId(productId) .setProductType(BillingClient.ProductType.INAPP) .build() ); QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder() .setProductList(productList) .build(); billingClient.queryProductDetailsAsync( params, new ProductDetailsResponseListener() { public void onProductDetailsResponse( BillingResult billingResult, QueryProductDetailsResult queryProductDetailsResult ) { if (billingResult.getResponseCode() == BillingResponseCode.OK) { List productDetailsList = queryProductDetailsResult.getProductDetailsList(); if (!productDetailsList.isEmpty()) { ProductDetails productDetails = productDetailsList.get(0); BillingResult result = billingClient.launchBillingFlow( activityRef.get(), BillingFlowParams.newBuilder().setProductDetailsParamsList( Arrays.asList( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) .build() ) ).build() ); int responseCode = result.getResponseCode(); if (responseCode == BillingResponseCode.OK) { callbackContext.success(); } else { callbackContext.error(responseCode); } } else { callbackContext.error("No product details found for: " + productId); } } else { callbackContext.error(billingResult.getResponseCode()); } } } ); } catch (Exception e) { callbackContext.error("Purchase error: " + e.getMessage()); } } private void getPurchases(CallbackContext callbackContext) { if (billingClient == null) { billingClient = getBillingClient(); callbackContext.error("Billing client is not connected"); return; } QueryPurchasesParams params = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) .build(); billingClient.queryPurchasesAsync( params, new PurchasesResponseListener() { public void onQueryPurchasesResponse( BillingResult billingResult, List purchasesList ) { try { int responseCode = billingResult.getResponseCode(); if (responseCode == BillingResponseCode.OK) { JSONArray purchases = new JSONArray(); for (Purchase purchase : purchasesList) { purchases.put(purchaseToJson(purchase)); } callbackContext.success(purchases); } else { callbackContext.error(responseCode); } } catch (JSONException e) { callbackContext.error(e.getMessage()); } } } ); } private void acknowledgePurchase( String purchaseToken, CallbackContext callbackContext ) { if (billingClient == null) { billingClient = getBillingClient(); callbackContext.error("Billing client is not connected"); return; } AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchaseToken) .build(); billingClient.acknowledgePurchase( params, new AcknowledgePurchaseResponseListener() { public void onAcknowledgePurchaseResponse(BillingResult billingResult) { int responseCode = billingResult.getResponseCode(); if (responseCode == BillingResponseCode.OK) { callbackContext.success(); } else { callbackContext.error(responseCode); } } } ); } private JSONObject purchaseToJson(Purchase purchase) throws JSONException { JSONObject item = new JSONObject(); List skuList = purchase.getSkus(); JSONArray skuArray = new JSONArray(); for (String sku : skuList) { skuArray.put(sku); } item.put("productIds", skuArray); item.put("orderId", purchase.getOrderId()); item.put("signature", purchase.getSignature()); item.put("purchaseTime", purchase.getPurchaseTime()); item.put("purchaseToken", purchase.getPurchaseToken()); item.put("purchaseState", purchase.getPurchaseState()); item.put("isAcknowledged", purchase.isAcknowledged()); item.put("developerPayload", purchase.getDeveloperPayload()); return item; } private void sendPurchasePluginResult(PluginResult result) { if (purchaseUpdated != null) { result.setKeepCallback(true); purchaseUpdated.sendPluginResult(result); } } private String getString(JSONArray args, int index) { try { return args.getString(index); } catch (JSONException e) { return null; } } private List getStringList(JSONArray args, int index) { try { JSONArray array = args.getJSONArray(index); List list = new ArrayList(); for (int i = 0; i < array.length(); i++) { list.add(array.getString(i)); } return list; } catch (JSONException e) { return null; } } } ================================================ FILE: src/plugins/iap/www/plugin.js ================================================ module.exports = { getProducts: function (productIds, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Iap', 'getProducts', [productIds]); }, setPurchaseUpdatedListener: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Iap', 'setPurchaseUpdatedListener', []); }, startConnection: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Iap', 'startConnection', []); }, consume: function (purchaseToken, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Iap', 'consume', [purchaseToken]); }, purchase: function (productId, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Iap', 'purchase', [productId]); }, getPurchases: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Iap', 'getPurchases', []); }, acknowledgePurchase: function (purchaseToken, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Iap', 'acknowledgePurchase', [purchaseToken]); }, BILLING_UNAVAILABLE: 3, DEVELOPER_ERROR: 5, ERROR: 6, FEATURE_NOT_SUPPORTED: -2, ITEM_ALREADY_OWNED: 7, ITEM_NOT_OWNED: 8, ITEM_UNAVAILABLE: 4, OK: 0, SERVICE_DISCONNECTED: -1, SERVICE_TIMEOUT: -3, SERVICE_TIMEOUT: 2, USER_CANCELED: 1, PURCHASE_STATE_PURCHASED: 1, PURCHASE_STATE_PENDING: 2, PURCHASE_STATE_UNKNOWN: 0, }; ================================================ FILE: src/plugins/pluginContext/package.json ================================================ { "name": "com.foxdebug.acode.rk.plugin.plugincontext", "version": "1.0.0", "description": "PluginContext", "cordova": { "id": "com.foxdebug.acode.rk.plugin.plugincontext", "platforms": [ "android" ] }, "keywords": [ "ecosystem:cordova", "cordova-android" ], "author": "@RohitKushvaha01", "license": "MIT" } ================================================ FILE: src/plugins/pluginContext/plugin.xml ================================================ PluginContext ================================================ FILE: src/plugins/pluginContext/src/android/Tee.java ================================================ package com.foxdebug.acode.rk.plugin; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.UUID; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import android.content.Context; import org.apache.cordova.*; //auth plugin import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; public class Tee extends CordovaPlugin { // pluginId : token private /*static*/ final Map tokenStore = new ConcurrentHashMap<>(); //assigned tokens private /*static*/ final Set disclosed = ConcurrentHashMap.newKeySet(); // token : list of permissions private /*static*/ final Map> permissionStore = new ConcurrentHashMap<>(); private Context context; public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.context = cordova.getContext(); } @Override public boolean execute(String action, JSONArray args, CallbackContext callback) throws JSONException { if ("get_secret".equals(action)) { String token = args.getString(0); String key = args.getString(1); String defaultValue = args.getString(2); String pluginId = getPluginIdFromToken(token); if (pluginId == null) { callback.error("INVALID_TOKEN"); return true; } EncryptedPreferenceManager prefs = new EncryptedPreferenceManager(context, pluginId); String value = prefs.getString(key, defaultValue); callback.success(value); return true; } if ("set_secret".equals(action)) { String token = args.getString(0); String key = args.getString(1); String value = args.getString(2); String pluginId = getPluginIdFromToken(token); if (pluginId == null) { callback.error("INVALID_TOKEN"); return true; } EncryptedPreferenceManager prefs = new EncryptedPreferenceManager(context, pluginId); prefs.setString(key, value); callback.success(); return true; } if ("requestToken".equals(action)) { String pluginId = args.getString(0); String pluginJson = args.getString(1); handleTokenRequest(pluginId, pluginJson, callback); return true; } if ("grantedPermission".equals(action)) { String token = args.getString(0); String permission = args.getString(1); if (!permissionStore.containsKey(token)) { callback.error("INVALID_TOKEN"); return true; } boolean granted = grantedPermission(token, permission); callback.success(granted ? 1 : 0); return true; } if ("listAllPermissions".equals(action)) { String token = args.getString(0); if (!permissionStore.containsKey(token)) { callback.error("INVALID_TOKEN"); return true; } List permissions = listAllPermissions(token); JSONArray result = new JSONArray(permissions); callback.success(result); return true; } return false; } private String getPluginIdFromToken(String token) { for (Map.Entry entry : tokenStore.entrySet()) { if (entry.getValue().equals(token)) { return entry.getKey(); } } return null; } //============================================================ //do not change function signatures public boolean isTokenValid(String token, String pluginId) { String storedToken = tokenStore.get(pluginId); return storedToken != null && token.equals(storedToken); } public boolean grantedPermission(String token, String permission) { List permissions = permissionStore.get(token); return permissions != null && permissions.contains(permission); } public List listAllPermissions(String token) { List permissions = permissionStore.get(token); if (permissions == null) { return new ArrayList<>(); } return new ArrayList<>(permissions); // return copy (safe) } //============================================================ private synchronized void handleTokenRequest( String pluginId, String pluginJson, CallbackContext callback ) { if (disclosed.contains(pluginId)) { callback.error("TOKEN_ALREADY_ISSUED"); return; } String token = tokenStore.get(pluginId); if (token == null) { token = UUID.randomUUID().toString(); tokenStore.put(pluginId, token); } try { JSONObject json = new JSONObject(pluginJson); JSONArray permissions = json.optJSONArray("permissions"); List permissionList = new ArrayList<>(); if (permissions != null) { for (int i = 0; i < permissions.length(); i++) { permissionList.add(permissions.getString(i)); } } // Bind permissions to token permissionStore.put(token, permissionList); } catch (JSONException e) { callback.error("INVALID_PLUGIN_JSON"); return; } disclosed.add(pluginId); callback.success(token); } } ================================================ FILE: src/plugins/pluginContext/www/PluginContext.js ================================================ var exec = require("cordova/exec"); const PluginContext = (function () { //============================= class _PluginContext { constructor(uuid) { this.created_at = Date.now(); this.uuid = uuid; Object.freeze(this); } toString() { return this.uuid; } [Symbol.toPrimitive](hint) { if (hint === "number") { return NaN; // prevent numeric coercion } return this.uuid; } grantedPermission(permission) { return new Promise((resolve, reject) => { exec(resolve, reject, "Tee", "grantedPermission", [ this.uuid, permission, ]); }); } listAllPermissions() { return new Promise((resolve, reject) => { exec(resolve, reject, "Tee", "listAllPermissions", [this.uuid]); }); } getSecret(key, defaultValue = "") { return new Promise((resolve, reject) => { exec( resolve, reject, "Tee", "get_secret", [this.uuid, key, defaultValue] ); }); } setSecret(key, value) { return new Promise((resolve, reject) => { exec( resolve, reject, "Tee", "set_secret", [this.uuid, key, value] ); }); } } //Object.freeze(this); //=============================== return { generate: async function (pluginId, pluginJson) { try { function requestToken(pluginId) { return new Promise((resolve, reject) => { exec(resolve, reject, "Tee", "requestToken", [ pluginId, pluginJson, ]); }); } const uuid = await requestToken(pluginId); return new _PluginContext(uuid); } catch (err) { console.warn(`PluginContext creation failed for pluginId ${pluginId}:`, err); return null; } }, }; })(); module.exports = PluginContext; ================================================ FILE: src/plugins/proot/package.json ================================================ { "name": "com.foxdebug.acode.rk.exec.proot", "version": "1.0.0", "description": "Proot stuff", "cordova": { "id": "com.foxdebug.acode.rk.exec.proot", "platforms": [ "android" ] }, "keywords": [ "ecosystem:cordova", "cordova-android" ], "author": "@RohitKushvaha01", "license": "MIT" } ================================================ FILE: src/plugins/proot/plugin.xml ================================================ Proot ================================================ FILE: src/plugins/sdcard/index.d.ts ================================================ interface Storage { /** * Name of the storage */ name: string; /** * UUID of the storage */ uuid: string; } interface DirListItem { name: string; mime: string; isDirectory: Boolean; isFile: Boolean; uri: string; } interface Stats { canRead: boolean; canWrite: boolean; exists: boolean; //indicates if file can be found on device storage isDirectory: boolean; isFile: boolean; isVirtual: boolean; lastModified: number; length: number; name: string; type: string; uri: string; } interface DocumentFile { canWrite: boolean; filename: string; length: number; type: string; uri: string; } interface SDcard { /** * Copy file/directory to given destination * @param src Source url * @param dest Destination url * @param onSuccess Callback function on success returns url of copied file/dir * @param onFail Callback function on error returns error object */ copy( src: string, dest: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Creates new directory at given source url. * @param src Source url * @param dirName New directory name * @param onSuccess Callback function on success returns url of new directory * @param onFail callback function on error returns error object */ createDir( src: string, dirName: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Creates new file at given source url. * @param src Source url * @param fileName New file name * @param onSuccess Callback function on success returns url of new directory * @param onFail Callback function on error returns error object */ createFile( src: string, fileName: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Deletes file/directory * @param src Source url of file/directory * @param onSuccess Callback function on success returns source url * @param onFail Callback function on error returns error object */ delete( src: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Checks if given file/directory * @param src File/Directory url * @param onSuccess Callback function on success returns string "TRUE" or "FALSE" * @param onFail Callback function on error returns error object */ exists( src: string, onSuccess: (exists: 'TRUE' | 'FALSE') => void, onFail: (err: any) => void, ): void; /** * Converts virtual URL to actual url * @param src Virtual address returned by other methods * @param onSuccess Callback function on success returns actual url * @param onFail Callback function on error returns error object */ formatUri( src: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Gets actual url for relative path to src * e.g. getPath(src, "../path/to/file.txt") => actual url * @param src Directory url * @param path Relative file/directory path * @param onSuccess Callback function on success returns actual url * @param onFail Callback function on error returns error object */ getPath( src: string, path: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Requests user for storage permission * @param uuid UUID returned from listStorages method * @param onSuccess Callback function on success returns url for the storage root * @param onFail Callback function on error returns error object */ getStorageAccessPermission( uuid: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Lists all the storages * @param onSuccess Callback function on success returns list of storages * @param onFail Callback function on error returns error object */ listStorages( onSuccess: (storages: Array) => void, onFail: (err: any) => void, ): void; /** * Gets list of files/directory in the given directory * @param src Directory url * @param onSuccess Callback function on success returns list of files/directory * @param onFail Callback function on error returns error object */ listDir( src: string, onSuccess: (list: Array) => void, onFail: (err: any) => void, ): void; /** * Move file/directory to given destination * @param src Source url * @param dest Destination url * @param onSuccess Callback function on success returns url of moved file/dir * @param onFail Callback function on error returns error object */ move( src: string, dest: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Opens file provider to select file * @param onSuccess Callback function on success returns url of selected file * @param onFail Callback function on error returns error object * @param mimeType MimeType of file to be selected */ openDocumentFile( onSuccess: (url: DocumentFile) => void, onFail: (err: any) => void, mimeType: string, ): void; /** * Opens gallery to select image * @param onSuccess Callback function on success returns url of selected file * @param onFail Callback function on error returns error object * @param mimeType MimeType of file to be selected */ getImage( onSuccess: (url: DocumentFile) => void, onFail: (err: any) => void, mimeType: string, ): void; /** * Renames the given file/directory to given new name * @param src Url of file/directory * @param newname New name * @param onSuccess Callback function on success returns url of renamed file * @param onFail Callback function on error returns error object */ rename( src: string, newname: string, onSuccess: (url: string) => void, onFail: (err: any) => void, ): void; /** * Writes new content to the given file. * @param src file url * @param content new file content * @param onSuccess Callback function on success returns "OK" * @param onFail Callback function on error returns error object */ write( src: string, content: string, onSuccess: (res: 'OK') => void, onFail: (err: any) => void, ): void; /** * Writes new content to the given file. * @param src file url * @param content new file content * @param isBinary is data binary * @param onSuccess Callback function on success returns "OK" * @param onFail Callback function on error returns error object */ write( src: string, content: string, isBinary: Boolean, onSuccess: (res: 'OK') => void, onFail: (err: any) => void, ): void; /** * Gets stats of given file * @param src file/directory url * @param onSuccess Callback function on success returns file/directory stats * @param onFail Callback function on error returns error object */ stats( src: string, onSuccess: (stats: Stats) => void, onFail: (err: any) => void, ): void; /** * Listens for file changes * @param src File url * @param listener Callback function on file change returns file stats */ watchFile( src: string, listener: () => void, ): { unwatch: () => void; }; } declare var sdcard: SDcard; ================================================ FILE: src/plugins/sdcard/package.json ================================================ { "author": "", "bundleDependencies": false, "deprecated": false, "description": "Using this plugin, cordova apps can check for external storages and write/modify files.", "keywords": [ "cordova", "plugin", "sdcard", "external storage", "access external storage", "android" ], "license": "ISC", "main": "index.js", "name": "cordova-plugin-sdcard", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "version": "1.1.0" } ================================================ FILE: src/plugins/sdcard/plugin.xml ================================================ cordova-plugin-sdcard Choose folder to get read/write access to the document tree Apache 2.0 cordova,plugin,Folder ================================================ FILE: src/plugins/sdcard/readme.md ================================================ # Write/modify content on external storage Using this plugin, cordova apps can check for external storages and write/modify files. ## Usage ```ts interface SDcard { /** * Copy file/directory to given destination * @param src Source url * @param dest Destination url * @param onSuccess Callback function on success returns url of copied file/dir * @param onFail Callback function on error returns error object */ copy(src: String, dest: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Creates new directory at given source url. * @param src Source url * @param dirName New directory name * @param onSuccess Callback function on success returns url of new directory * @param onFail callback function on error returns error object */ createDir(src: String, dirName: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Creates new file at given source url. * @param src Source url * @param dirName New file name * @param onSuccess Callback function on success returns url of new directory * @param onFail Callback function on error returns error object */ createFile(src: String, fileName: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Deletes file/directory * @param src Source url of file/directory * @param onSuccess Callback function on success returns source url * @param onFail Callback function on error returns error object */ delete(src: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Checks if given file/directory * @param src File/Directory url * @param onSuccess Callback function on success returns string "TRUE" or "FALSE" * @param onFail Callback function on error returns error object */ exists(src: String, onSuccess: (exists: "TRUE" | "FALSE") => void, onFail: (err: any) => void): void; /** * Converts virtual URL to actual url * @param src Virtual address returned by other methods * @param onSuccess Callback function on success returns actual url * @param onFail Callback function on error returns error object */ formatUri(src: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Gets actual url for relative path to src * e.g. getPath(src, "../path/to/file.txt") => actual url * @param src Directory url * @param path Relative file/direcotry path * @param onSuccess Callback function on success returns actual url * @param onFail Callback function on error returns error object */ getPath(src: String, path: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Requests user for storage permission * @param uuid UUID returned from listStorages method * @param onSuccess Callback function on success returns url for the storage root * @param onFail Callback function on error returns error object */ getStorageAccessPermission(uuid: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Lists all the storages * @param onSuccess Callback function on success returns list of storages * @param onFail Callback function on error returns error object */ listStorages(onSuccess: (storages: Array) => void, onFail: (err: any) => void): void; /** * Gets list of files/directory in the given directory * @param src Directory url * @param onSuccess Callback function on success returns list of files/directory * @param onFail Callback function on error returns error object */ listDir(src: String, onSuccess: (list: Array) => void, onFail: (err: any) => void): void; /** * Move file/directory to given destination * @param src Source url * @param dest Destination url * @param onSuccess Callback function on success returns url of moved file/dir * @param onFail Callback function on error returns error object */ move(src: String, dest: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Opens file provider to select file * @param onSuccess Callback function on success returns url of selected file * @param onFail Callback function on error returns error object * @param mimeType MimeType of file to be selected */ openDocumentFile(onSuccess: (url: String) => void, onFail: (err: any) => void, mimeType: String): void; /** * Renames the given file/directory to given new name * @param src Url of file/directory * @param newname New name * @param onSuccess Callback function on success returns url of renamed file * @param onFail Callback function on error returns error object */ rename(src: String, newname: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Writes new content to the given file. * @param src file url * @param content new file content * @param onSuccess Callback function on success returns "OK" * @param onFail Callback function on error returns error object */ write(src: String, content: String, onSuccess: (res: "OK") => void, onFail: (err: any) => void): void; /** * Gets stats of given file * @param src file/directory url * @param onSuccess Callback function on success returns file/directory stats * @param onFail Callback function on error returns error object */ stats(src: String, onSuccess: (stats: Stats) => void, onFail: (err: any) => void): void; } interface Storage { /** * Name of the storage */ name: String; /** * UUID of the storage */ uuid: String; } interface DirListItem { name: String; mime: String; isDirectory: Boolean; isFile: Boolean; uri: String; } interface Stats { canRead: boolean; canWrite: boolean; exists: boolean; //indicates if file can be found on device storage isDirectory: boolean; isFile: boolean; isVirtual: boolean; lastModified: number; length: number; name: string; type: string; uri: string; } ``` ================================================ FILE: src/plugins/sdcard/src/android/SDcard.java ================================================ package com.foxdebug.sdcard; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.FileObserver; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import androidx.documentfile.provider.DocumentFile; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.URLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public class SDcard extends CordovaPlugin { private int mode; private int SDK_INT = android.os.Build.VERSION.SDK_INT; private int REQUEST_CODE; private final int ACCESS_INTENT = 6000; private final int DOCUMENT_TREE = 6001; private final int OPEN_DOCUMENT = 6002; private final int PICK_FROM_GALLERY = 6003; private final String SEPARATOR = "::"; private StorageManager storageManager; private Context context; private Activity activity; private ContentResolver contentResolver; private DocumentFile originalRootFile; private CallbackContext activityResultCallback; private HashMap fileObservers = new HashMap(); public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.REQUEST_CODE = this.ACCESS_INTENT; this.context = cordova.getContext(); this.activity = cordova.getActivity(); this.storageManager = (StorageManager) this.activity.getSystemService( Context.STORAGE_SERVICE ); } public boolean execute( String action, JSONArray args, CallbackContext callback ) throws JSONException { String arg1 = null, arg2 = null, arg3 = null, arg4 = null; int argLen = args.length(); if (argLen > 0) arg1 = args.getString(0); if (argLen > 1) arg2 = args.getString(1); if (argLen > 2) arg3 = args.getString(2); switch (action) { case "create directory": createDir(arg1, arg2, callback); break; case "create file": createFile(arg1, arg2, callback); break; case "open document file": openDocumentFile(arg1, callback); break; case "get image": getImage(arg1, callback); break; case "list volumes": getStorageVolumes(callback); break; case "storage permission": getStorageAccess(arg1, callback); break; case "read": readFile(arg1, callback); break; case "write": writeFile( formatUri(arg1), args.optString(1), args.optBoolean(2), callback ); break; case "rename": rename(arg1, arg2, callback); break; case "delete": delete(formatUri(arg1), callback); break; case "copy": copy(arg1, arg2, callback); break; case "move": move(arg1, arg2, callback); break; case "get path": getPath(formatUri(arg1), arg2, callback); break; case "exists": exists(formatUri(arg1), callback); break; case "format uri": callback.success(formatUri(arg1)); break; case "list directory": if (arg1.contains(SEPARATOR)) { String splittedStr[] = arg1.split(SEPARATOR, 2); arg1 = splittedStr[0]; arg2 = splittedStr[1]; } listDir(arg1, arg2, callback); break; case "stats": getStats(arg1, callback); break; case "watch file": watchFile(arg1, arg2, callback); break; case "unwatch file": unwatchFile(arg1); break; default: return false; } return true; } private String formatUri(String filename) { if (filename.contains(SEPARATOR)) { String splittedStr[] = filename.split(SEPARATOR, 2); String rootUri = splittedStr[0]; String docId = splittedStr[1]; Uri uri = getUri(rootUri, docId); return uri.toString(); } else { return filename; } } private void watchFile( final String fileUri, final String id, final CallbackContext listener ) { activity.runOnUiThread( new Runnable() { @Override public void run() { MyFileObserver observer; Uri uri = Uri.parse(fileUri); File file = new File(uri.getPath()); if (!file.exists()) { listener.error("File not found"); return; } if (SDK_INT >= 29) { observer = new MyFileObserver(file, listener); } else { observer = new MyFileObserver(fileUri, listener); } observer.startObserving(); fileObservers.put(id, observer); } } ); } private void unwatchFile(String id) { MyFileObserver observer = fileObservers.get(id); if (observer == null) return; observer.stopObserving(); fileObservers.remove(id); } public void openDocumentFile(String mimeType, CallbackContext callback) { Intent intent = new Intent(); if (mimeType == null) mimeType = "*/*"; intent.setAction(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(mimeType); activityResultCallback = callback; cordova.startActivityForResult(this, intent, this.OPEN_DOCUMENT); } public void getImage(String mimeType, CallbackContext callback) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); if (mimeType == null) mimeType = "image/*"; intent.setType(mimeType); activityResultCallback = callback; cordova.startActivityForResult(this, intent, this.PICK_FROM_GALLERY); } public void getStorageVolumes(CallbackContext callback) { try { JSONArray result = new JSONArray(); if (SDK_INT >= 24) { for (StorageVolume volume : this.storageManager.getStorageVolumes()) { String name = volume.getDescription(this.context); String uuid = volume.getUuid(); JSONObject volumeData = new JSONObject(); if (name != null && uuid != null) { volumeData.put("uuid", uuid); volumeData.put("name", name); if (SDK_INT >= 30) { File file = volume.getDirectory(); String path = file.getAbsolutePath(); volumeData.put("path", path); } result.put(volumeData); } } } else { File file = Environment.getExternalStorageDirectory(); if ( Environment.getExternalStorageState(file) == Environment.MEDIA_MOUNTED ) { String name = file.getName(); String path = file.getPath(); String absolutePath = file.getAbsolutePath(); JSONObject volumeData = new JSONObject(); volumeData.put("name", name); volumeData.put("path", path); volumeData.put("absolutePath", absolutePath); result.put(volumeData); } } callback.success(result); } catch (JSONException e) { callback.error(e.toString()); } } public void getStorageAccess(String SDCardUUID, CallbackContext callback) { Intent intent = null; if (SDK_INT >= 24) { StorageVolume sdCard = null; for (StorageVolume volume : this.storageManager.getStorageVolumes()) { String uuid = volume.getUuid(); if (uuid != null && uuid.equals(SDCardUUID)) { sdCard = volume; } } if (sdCard != null) { if (SDK_INT < 29) { intent = sdCard.createAccessIntent(null); } else if (SDK_INT >= 29) { intent = sdCard.createOpenDocumentTreeIntent(); } } } if (intent == null) { REQUEST_CODE = DOCUMENT_TREE; intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); } activityResultCallback = callback; cordova.startActivityForResult(this, intent, REQUEST_CODE); } public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (activityResultCallback == null) { Log.e("SDcard", "activityResultCallback is null"); return; } if (data == null) return; if (resultCode == Activity.RESULT_CANCELED) { activityResultCallback.error("Operation cancelled"); return; } if (requestCode == PICK_FROM_GALLERY) { if (resultCode == Activity.RESULT_OK) { Uri uri = data.getData(); if (uri == null) { activityResultCallback.error("No file selected"); } else { try { takePermission(uri); activityResultCallback.success(uri.toString()); } catch (Exception e) { activityResultCallback.error( "Error taking permission: " + e.getMessage() ); } } //activityResultCallback.success(uri.toString()); } else { activityResultCallback.error("Operation cancelled"); } return; } if (requestCode == OPEN_DOCUMENT) { if (resultCode == Activity.RESULT_OK) { try { Uri uri = data.getData(); if (uri == null) { activityResultCallback.error("No file selected"); return; } takePermission(uri); DocumentFile file = DocumentFile.fromSingleUri(context, uri); JSONObject res = new JSONObject(); res.put("length", file.length()); res.put("type", file.getType()); res.put("filename", file.getName()); res.put("canWrite", canWrite(file.getUri())); res.put("uri", uri.toString()); activityResultCallback.success(res); } catch (JSONException e) { activityResultCallback.error(e.toString()); } } return; } if (requestCode == DOCUMENT_TREE || requestCode == ACCESS_INTENT) { if ( requestCode == ACCESS_INTENT && resultCode == Activity.RESULT_CANCELED ) { activityResultCallback.error("Canceled"); return; } try { Uri uri = data.getData(); if (uri == null) { activityResultCallback.error("Empty uri"); return; } takePermission(uri); DocumentFile file = DocumentFile.fromTreeUri(context, uri); if (file != null && file.canWrite()) { activityResultCallback.success(uri.toString()); } else { activityResultCallback.error( "No write permission: " + uri.toString() ); } } catch (Exception error) { activityResultCallback.error(error.toString()); } return; } } private void readFile(String filename, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { Uri uri = Uri.parse(filename); InputStream is = context .getContentResolver() .openInputStream(uri); if (is == null) { callback.error("File not found"); return; } ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); final int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int bytesRead = 0; while ((bytesRead = is.read(buffer, 0, bufferSize)) != -1) { outputStream.write(buffer, 0, bytesRead); } is.close(); callback.success(outputStream.toByteArray()); } catch (Exception e) { callback.error(e.toString()); } } } ); } private void writeFile( final String filename, final String content, final Boolean isArrayBuffer, final CallbackContext callback ) { final Context context = this.context; cordova .getThreadPool() .execute( new Runnable() { public void run() { try { DocumentFile file = getFile(filename); if (file == null) { callback.error("File not found."); return; } if (canWrite(file.getUri())) { OutputStream op = context .getContentResolver() .openOutputStream(file.getUri(), "rwt"); PrintWriter pw = new PrintWriter(op, true); if (isArrayBuffer) { byte[] bytes = Base64.decode(content, Base64.DEFAULT); // write bytes to file op.write(bytes); } else { pw.print(content); } pw.flush(); pw.close(); op.close(); callback.success("OK"); } else { callback.error("No write permission"); } } catch (Exception e) { callback.error(e.toString()); } } } ); } private void createDir(String parent, String name, CallbackContext callback) { create(parent, name, Document.MIME_TYPE_DIR, callback); } private void createFile( String parent, String name, CallbackContext callback ) { String mimeType = URLConnection.guessContentTypeFromName(name); String ext = FilenameUtils.getExtension(name); if (mimeType == null && ext != null) mimeType = "text/" + ext; else mimeType = "text/plain"; create(parent, name, mimeType, callback); } private void create( String parent, String name, String mimeType, CallbackContext callback ) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String srcUri = null, docId = null; Uri parentUri = null; if (parent.contains(SEPARATOR)) { String splittedStr[] = parent.split(SEPARATOR, 2); srcUri = splittedStr[0]; docId = splittedStr[1]; parentUri = getUri(srcUri, docId); } else { srcUri = parent; parentUri = Uri.parse(srcUri); docId = DocumentsContract.getTreeDocumentId(parentUri); parentUri = DocumentsContract.buildDocumentUriUsingTree( parentUri, docId ); } ContentResolver contentResolver = context.getContentResolver(); Uri newDocumentUri = DocumentsContract.createDocument( contentResolver, parentUri, mimeType, name ); DocumentFile file = DocumentFile.fromTreeUri( context, newDocumentUri ); Log.i("SDcard", "Uri: " + newDocumentUri.toString()); if (!name.equals(file.getName()) && file.renameTo(name)) { newDocumentUri = file.getUri(); } docId = DocumentsContract.getDocumentId(newDocumentUri); if (newDocumentUri != null) { callback.success(srcUri + SEPARATOR + docId); } else { callback.error("Unable to create " + parent); } } catch (Exception e) { Log.e("CREATE_FILE", "Unable to create file", e); callback.error(e.toString()); } } } ); } private void rename( String filename, String newFile, CallbackContext callback ) { cordova .getThreadPool() .execute( new Runnable() { public void run() { String srcUri = null, docId = null; Uri fileUri = null; if (filename.contains(SEPARATOR)) { String splittedStr[] = filename.split(SEPARATOR, 2); srcUri = splittedStr[0]; docId = splittedStr[1]; fileUri = getUri(srcUri, docId); } else { srcUri = filename; fileUri = Uri.parse(filename); } try { DocumentFile file = DocumentFile.fromTreeUri(context, fileUri); // If only case change, OS adds '()' as suffix, to avoid that we need to rename to a temporary name first if (newFile.equalsIgnoreCase(file.getName())) { file.renameTo(newFile + "_temp"); } if (file.renameTo(newFile)) { String name = file.getName(); docId = DocumentsContract.getDocumentId(file.getUri()); callback.success(srcUri + SEPARATOR + docId); return; } callback.error("Unable to rename: " + filename); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } private void delete(String filename, CallbackContext callback) { final ContentResolver contentResolver = context.getContentResolver(); cordova .getThreadPool() .execute( new Runnable() { public void run() { Uri fileUri = Uri.parse(filename); try { boolean fileDeleted = DocumentsContract.deleteDocument( contentResolver, fileUri ); if (fileDeleted) { callback.success(filename); } else { callback.error("Unable to delete file " + filename); } } catch (FileNotFoundException e) { callback.error(e.toString()); } } } ); } private void move(String src, String dest, final CallbackContext callback) { final ContentResolver contentResolver = this.context.getContentResolver(); final String splittedStr[] = src.split(SEPARATOR, 2); final String rootUri = splittedStr[0]; final String srcId = splittedStr[1]; final String destId = dest.split(SEPARATOR, 2)[1]; cordova .getThreadPool() .execute( new Runnable() { @Override public void run() { try { Uri newUri = copy(rootUri, srcId, destId); if (newUri == null) callback.error("Unable to copy " + src); else { DocumentsContract.deleteDocument( contentResolver, getUri(rootUri, srcId) ); callback.success( rootUri + SEPARATOR + DocumentsContract.getDocumentId(newUri) ); } } catch (Exception e) { callback.error(e.toString()); } } } ); } private void copy(String src, String dest, final CallbackContext callback) { final String splittedStr[] = src.split(SEPARATOR, 2); final String srcUri = splittedStr[0]; final String srcId = splittedStr[1]; final String destId = dest.split(SEPARATOR, 2)[1]; cordova .getThreadPool() .execute( new Runnable() { @Override public void run() { try { Uri newUri = copy(srcUri, srcId, destId); if (newUri == null) { callback.error("Unable to copy " + src); } else { callback.success( srcUri + SEPARATOR + DocumentsContract.getDocumentId(newUri) ); } } catch (Exception e) { callback.error(e.toString()); } } } ); } private Uri copy(String root, String srcId, String destId) throws IOException, FileNotFoundException { Uri srcUri = getUri(root, srcId); Uri destUri = getUri(root, destId); DocumentFile src = getFile(srcUri); DocumentFile dest = getFile(destUri); ContentResolver contentResolver = context.getContentResolver(); if (src.isFile()) { Uri newUri = copyFile(src, dest); if (newUri == null) return null; else return newUri; } else { destUri = DocumentsContract.createDocument( contentResolver, destUri, Document.MIME_TYPE_DIR, src.getName() ); destId = DocumentsContract.getDocumentId(destUri); Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( Uri.parse(root), srcId ); Cursor c = contentResolver.query( childrenUri, new String[] { Document.COLUMN_DOCUMENT_ID }, null, null, null ); if (c != null) { while (c.moveToNext()) { String docId = c.getString(0); copy(root, docId, destId); } c.close(); return destUri; } else { DocumentsContract.deleteDocument(contentResolver, destUri); return null; } } } private Uri copyFile(DocumentFile src, DocumentFile dest) throws IOException, FileNotFoundException { ContentResolver contentResolver = this.context.getContentResolver(); Uri newFileUri = DocumentsContract.createDocument( contentResolver, dest.getUri(), src.getType(), src.getName() ); DocumentFile newFile = getFile(newFileUri); InputStream is = contentResolver.openInputStream(src.getUri()); OutputStream os = contentResolver.openOutputStream(newFile.getUri(), "rwt"); if (is == null || os == null) { DocumentsContract.deleteDocument(contentResolver, newFileUri); return null; } IOUtils.copy(is, os); is.close(); os.close(); if (src.length() == newFile.length()) return newFile.getUri(); else { DocumentsContract.deleteDocument(contentResolver, newFileUri); return null; } } private void listDir(String src, String parentId, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { Uri srcUri = Uri.parse(src); ContentResolver contentResolver = context.getContentResolver(); String parentDocId = parentId; if (parentDocId == null) { parentDocId = DocumentsContract.getTreeDocumentId(srcUri); } Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( srcUri, parentDocId ); JSONArray result = new JSONArray(); Cursor cursor = null; try { cursor = contentResolver.query( childrenUri, new String[] { Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, }, null, null, null ); } catch ( NullPointerException | SecurityException | IllegalArgumentException | Error e ) { Log.d("sdCard", "lsDir: " + src); Log.e("sdCard", "lsDir", e); callback.error("Cannot read directory."); return; } if (cursor == null) { callback.error("Cannot read directory."); return; } try { while (cursor.moveToNext()) { JSONObject fileData = new JSONObject(); String docId = cursor.getString(0); String name = cursor.getString(1); String mime = cursor.getString(2); boolean isDirectory = isDirectory(mime); fileData.put("name", name); fileData.put("mime", mime); fileData.put("isDirectory", isDirectory); fileData.put("isFile", !isDirectory); fileData.put("uri", src + SEPARATOR + docId); // TODO: Deprecate in future fileData.put("url", src + SEPARATOR + docId); result.put(fileData); } callback.success(result); } catch (JSONException e) { callback.error(e.toString()); } finally { if (cursor != null) { try { cursor.close(); } catch (RuntimeException re) { throw re; } catch (Exception ignore) { // ignore exception } } } } } ); } private boolean isDirectory(String mimeType) { return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType); } private void getStats(String filename, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { String fileUri = formatUri(filename); try { DocumentFile file = getFile(fileUri); JSONObject result = new JSONObject(); result.put("exists", file.exists()); result.put("canRead", file.canRead()); result.put("canWrite", canWrite(file.getUri())); result.put("name", file.getName()); result.put("length", file.length()); result.put("type", file.getType()); result.put("isFile", file.isFile()); result.put("isDirectory", file.isDirectory()); result.put("isVirtual", file.isVirtual()); result.put("lastModified", file.lastModified()); result.put("url", file.getUri().toString()); callback.success(result); } catch (Exception e) { callback.error(e.getMessage()); } } } ); } private Uri getUri(String src, String docId) { Uri srcUri = Uri.parse(src); String srcId = DocumentsContract.getTreeDocumentId(srcUri); srcUri = DocumentsContract.buildDocumentUriUsingTree(srcUri, srcId); return DocumentsContract.buildDocumentUriUsingTree(srcUri, docId); } private void exists(String path, CallbackContext callback) { DocumentFile file = DocumentFile.fromSingleUri(context, Uri.parse(path)); if (file == null) { callback.error("Unable to get file"); } else { if (file.exists()) { callback.success("TRUE"); } else { callback.success("FALSE"); } } } private void error(String err, CallbackContext callback) { callback.error("ERROR: " + err); } private void getPath(String uriString, String src, CallbackContext callback) { try { DocumentFile file = geRelativeDocumentFile(uriString, src); if (file == null) { callback.error("Unable to get file"); } else { Uri uri = file.getUri(); String path = uri.getPath(); if (path != null) { callback.success(uri.toString()); } else { callback.error("Unable to get path"); } } } catch (Exception e) { callback.error(e.getMessage()); } } private DocumentFile geRelativeDocumentFile(String uri, String filename) { List paths = new ArrayList(); DocumentFile file = null; file = DocumentFile.fromTreeUri(context, Uri.parse(uri)); if (!canWrite(file.getUri())) { throw new RuntimeException("Cannot write file"); } paths.addAll(Arrays.asList(filename.split("/"))); while (paths.size() > 0) { String path = paths.remove(0); filename = TextUtils.join("/", paths); if (!path.equals("")) { file = file.findFile(path); if (file == null) return null; } } return file; } private DocumentFile getFile(Uri uri) { return getFile(uri.toString()); } private DocumentFile getFile(String filePath) { Uri fileUri = Uri.parse(filePath); DocumentFile documentFile = null; if (filePath.matches("file:///(.*)")) { File file = new File(fileUri.getPath()); documentFile = DocumentFile.fromFile(file); } else { documentFile = DocumentFile.fromSingleUri(context, fileUri); } return documentFile; } private void takePermission(Uri uri) { contentResolver = context.getContentResolver(); contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION ); } public boolean canWrite(Uri uri) { boolean canWrite = false; try { // if the file is not writable this throws a SecurityException OutputStream os = context .getContentResolver() .openOutputStream(uri, "wa"); if (os == null) return false; os.close(); // we don't actually want to write anything, so we close immediately canWrite = true; } catch ( SecurityException | IllegalArgumentException | IOException ignored ) { // if we don't have write-permission or the file doesn't exist, canWrite can stay on false } return canWrite; } } class MyFileObserver extends FileObserver { private CallbackContext listener; private static final int mask = (FileObserver.DELETE_SELF | FileObserver.MODIFY | FileObserver.MOVE_SELF); public MyFileObserver(String path, CallbackContext listener) { super(path, mask); this.listener = listener; Log.d("MyFileObserver", "MyFileObserver: " + path); } public MyFileObserver(File file, CallbackContext listener) { super(file, mask); this.listener = listener; Log.d("MyFileObserver", "MyFileObserver: " + file.getAbsolutePath()); } @Override public void onEvent(int event, String path) { Log.d("MyFileObserver", "onEvent: " + event); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); listener.sendPluginResult(result); } public void startObserving() { startWatching(); } public void stopObserving() { stopWatching(); } } ================================================ FILE: src/plugins/sdcard/www/plugin.js ================================================ module.exports = { copy: function (srcPathname, destPathname, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'copy', [srcPathname, destPathname]); }, createDir: function (pathname, dir, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'create directory', [pathname, dir]); }, createFile: function (pathname, file, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'create file', [pathname, file]); }, delete: function (pathname, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'delete', [pathname]); }, exists: function (pathName, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'exists', [pathName]); }, formatUri: function (pathName, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'format uri', [pathName]); }, getPath: function (uri, filename, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'get path', [uri, filename]); }, getStorageAccessPermission: function (uuid, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'storage permission', [uuid]); }, listStorages: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'list volumes', []); }, listDir: function (src, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'list directory', [src]); }, move: function (srcPathname, destPathname, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'move', [srcPathname, destPathname]); }, openDocumentFile: function (onSuccess, onFail, mimeType) { cordova.exec(onSuccess, onFail, 'SDcard', 'open document file', mimeType ? [mimeType] : []); }, getImage: function (onSuccess, onFail, mimeType) { cordova.exec(onSuccess, onFail, 'SDcard', 'get image', mimeType ? [mimeType] : []); }, rename: function (pathname, newFilename, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'rename', [pathname, newFilename]); }, read: function (filename, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'read', [filename]); }, write: function (filename, content, onSuccess, onFail) { var _isBuffer = content instanceof ArrayBuffer; cordova.exec(onSuccess, onFail, 'SDcard', 'write', [filename, content, _isBuffer]); }, stats: function (filename, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'stats', [filename]); }, watchFile: function (filename, listener, onFail) { var id = parseInt(Date.now() + Math.random() * 1000000) + ''; cordova.exec(listener, onFail, 'SDcard', 'watch file', [filename, id]); return { unwatch: function () { cordova.exec(null, null, 'SDcard', 'unwatch file', [id]); } }; }, listEncodings: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'SDcard', 'list encodings', []); } }; ================================================ FILE: src/plugins/server/index.d.ts ================================================ interface Server{ stop(onSuccess: () => void, onError: (error: any) => void): void; send(id: string, data: any, onSuccess: () => void, onError: (error: any) => void): void; port: number; } declare var CreateServer: (port: number, onSuccess: (msg: any) => void, onError: (err: any) => void) => Server; ================================================ FILE: src/plugins/server/package.json ================================================ { "name": "cordova-plugin-server", "version": "1.0.0", "main": "index.js", "license": "MIT" } ================================================ FILE: src/plugins/server/plugin.xml ================================================ Webserver for Cordova Apps HTTP server, webserver ================================================ FILE: src/plugins/server/src/android/com/foxdebug/server/NanoHTTPDWebserver.java ================================================ package com.foxdebug.server; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.util.Log; import androidx.documentfile.provider.DocumentFile; import fi.iki.elonen.NanoHTTPD; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.URLConnection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.UUID; import org.apache.cordova.CallbackContext; import org.apache.cordova.PluginResult; import org.json.JSONException; import org.json.JSONObject; public class NanoHTTPDWebserver extends NanoHTTPD { public HashMap responses; public CallbackContext onRequestCallbackContext; Context context; public NanoHTTPDWebserver(int port, Context context) { super(port); this.context = context; this.responses = new HashMap(); } private String getBodyText(IHTTPSession session) { Map files = new HashMap(); Method method = session.getMethod(); if (Method.PUT.equals(method) || Method.POST.equals(method)) { try { session.parseBody(files); } catch (IOException ioe) { return "{}"; } catch (ResponseException re) { return "{}"; } } // get the POST body return files.get("postData"); } /** * Create a request object *

          * [ "requestId": requestUUID, " body": request.jsonObject ?? "", " headers": * request.headers, " method": request.method, " path": request.url.path, " * query": request.url.query ?? "" ] * * @param session * @return */ private JSONObject createJSONRequest(String requestId, IHTTPSession session) throws JSONException { JSONObject jsonRequest = new JSONObject(); jsonRequest.put("requestId", requestId); jsonRequest.put("body", this.getBodyText(session)); jsonRequest.put("headers", session.getHeaders()); jsonRequest.put("method", session.getMethod()); jsonRequest.put("path", session.getUri()); jsonRequest.put("query", session.getQueryParameterString()); return jsonRequest; } private String getContentType(JSONObject responseObject) throws JSONException { if ( responseObject.has("headers") && responseObject.getJSONObject("headers").has("Content-Type") ) { return responseObject.getJSONObject("headers").getString("Content-Type"); } else { return "text/plain"; } } private Response newFixedFileResponse(DocumentFile file, String mime) throws FileNotFoundException, IOException { Response res; res = newFixedLengthResponse( Response.Status.OK, mime, getInputStream(file), (int) file.length() ); res.addHeader("Accept-Ranges", "bytes"); return res; } Response serveFile( Map header, DocumentFile file, String mime ) { Response res; try { // Calculate etag long fileLen = file.length(); String path = file.getUri().toString(); String etag = Integer.toHexString( (path + file.lastModified() + "" + fileLen).hashCode() ); // Support (simple) skipping: long startFrom = 0; long endAt = -1; String range = header.get("range"); if (range != null) { if (range.startsWith("bytes=")) { range = range.substring("bytes=".length()); int minus = range.indexOf('-'); try { if (minus > 0) { startFrom = Long.parseLong(range.substring(0, minus)); endAt = Long.parseLong(range.substring(minus + 1)); } } catch (NumberFormatException error) { Log.w("NanoHTTPDWebserver", "Invalid range header: " + range, error); } } } // get if-range header. If present, it must match etag or else we // should ignore the range request String ifRange = header.get("if-range"); boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange)); String ifNoneMatch = header.get("if-none-match"); boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag)); if ( headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen ) { // range request that matches current etag // and the startFrom of the range is satisfiable if (headerIfNoneMatchPresentAndMatching) { // range request that matches current etag // and the startFrom of the range is satisfiable // would return range from file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else { if (endAt < 0) { endAt = fileLen - 1; } long newLen = endAt - startFrom + 1; if (newLen < 0) { newLen = 0; } InputStream is = getInputStream(file); is.skip(startFrom); res = newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, mime, is, newLen ); res.addHeader("Accept-Ranges", "bytes"); res.addHeader("Content-Length", "" + newLen); String contentRange = "bytes " + startFrom + "-" + endAt + "/" + fileLen; res.addHeader("Content-Range", contentRange); res.addHeader("ETag", etag); } } else { if ( headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen ) { // return the size of the file // 4xx responses are not trumped by if-none-match res = newFixedLengthResponse( Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "" ); res.addHeader("Content-Range", "bytes */" + fileLen); res.addHeader("ETag", etag); } else if (range == null && headerIfNoneMatchPresentAndMatching) { // full-file-fetch request // would return entire file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else if ( !headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching ) { // range request that doesn't match current etag // would return entire (different) file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else { // supply the file res = newFixedFileResponse(file, mime); res.addHeader("Content-Length", "" + fileLen); res.addHeader("ETag", etag); } } } catch (Exception e) { Log.d("ServeFileError", e.getMessage()); String type = e.getClass().getName(); if (type.equals("FileNotFoundException")) res = newFixedLengthResponse( Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, e.getMessage() ); else res = newFixedLengthResponse( Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, e.getMessage() ); } return res; } @Override public Response serve(IHTTPSession session) { String requestUUID = UUID.randomUUID().toString(); PluginResult pluginResult = null; try { pluginResult = new PluginResult( PluginResult.Status.OK, this.createJSONRequest(requestUUID, session) ); } catch (JSONException e) { e.printStackTrace(); } pluginResult.setKeepCallback(true); this.onRequestCallbackContext.sendPluginResult(pluginResult); while (!this.responses.containsKey(requestUUID)) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } JSONObject responseObject = (JSONObject) this.responses.get(requestUUID); Response response = null; if (responseObject.has("path")) { try { String path = responseObject.getString("path"); DocumentFile file = getFile(path); String mimeType = URLConnection.guessContentTypeFromName(path); Response res = serveFile(session.getHeaders(), file, mimeType); JSONObject headers = getJSONObject(responseObject, "headers"); // JSONObject headers = responseObject.getJSONObject("headers"); if (headers != null) { Iterator keys = headers.keys(); while (keys.hasNext()) { String key = (String) keys.next(); res.addHeader( key, responseObject.getJSONObject("headers").getString(key) ); } } return res; } catch (JSONException e) { e.printStackTrace(); } return response; } else { try { response = newFixedLengthResponse( Response.Status.lookup(responseObject.getInt("status")), getContentType(responseObject), responseObject.getString("body") ); Iterator keys = responseObject.getJSONObject("headers").keys(); while (keys.hasNext()) { String key = (String) keys.next(); response.addHeader( key, responseObject.getJSONObject("headers").getString(key) ); } } catch (JSONException e) { e.printStackTrace(); } return response; } } private DocumentFile getFile(String filePath) { Uri fileUri = Uri.parse(filePath); DocumentFile documentFile = null; if (filePath.matches("file:///(.*)")) { File file = new File(fileUri.getPath()); documentFile = DocumentFile.fromFile(file); } else { documentFile = DocumentFile.fromSingleUri(this.context, Uri.parse(filePath)); } return documentFile; } private InputStream getInputStream(DocumentFile file) throws FileNotFoundException, IOException { Uri uri = file.getUri(); ContentResolver contentResolver = context.getContentResolver(); return contentResolver.openInputStream(uri); } private JSONObject getJSONObject(JSONObject ob, String key) { JSONObject jsonObject = null; try { jsonObject = ob.getJSONObject(key); } catch (JSONException e) { Log.w("NanoHTTPDWebserver", "Missing or invalid JSON object for key: " + key, e); } return jsonObject; } } ================================================ FILE: src/plugins/server/src/android/com/foxdebug/server/Server.java ================================================ package com.foxdebug.server; import java.io.IOException; import java.util.HashMap; import org.apache.cordova.*; import org.json.JSONArray; import org.json.JSONException; public class Server extends CordovaPlugin { public HashMap servers; @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); servers = new HashMap(); } @Override public boolean execute( String action, JSONArray args, CallbackContext callbackContext ) throws JSONException { if ("start".equals(action)) { try { this.start(args, callbackContext); } catch (IOException e) { e.printStackTrace(); } return true; } if ("setOnRequestHandler".equals(action)) { this.setOnRequestHandler(args, callbackContext); return true; } if ("stop".equals(action)) { this.stop(args, callbackContext); return true; } if ("send".equals(action)) { this.send(args, callbackContext); return true; } return false; // Returning false results in a "MethodNotFound" error. } /** * Starts the server * * @param args * @param callbackContext */ private void start(JSONArray args, CallbackContext callbackContext) throws JSONException, IOException { Integer port = 8080; if (args.length() == 1) { port = args.getInt(0); } NanoHTTPDWebserver server = servers.get(port); if (server != null) { callbackContext.success("Server started on port " + port); return; } try { server = new NanoHTTPDWebserver(port, cordova.getContext()); server.start(); servers.put(port, server); callbackContext.success("Server started on port " + port); } catch (Exception e) { callbackContext.sendPluginResult( new PluginResult(PluginResult.Status.ERROR, e.getMessage()) ); } } private void setOnRequestHandler( JSONArray args, CallbackContext callbackContext ) throws JSONException { Integer port = args.getInt(0); NanoHTTPDWebserver server = servers.get(port); if (server == null) { callbackContext.error("Server not started on port " + port); return; } server.onRequestCallbackContext = callbackContext; } /** * Stops the server * * @param args * @param callbackContext */ private void stop(JSONArray args, CallbackContext callbackContext) throws JSONException { Integer port = args.getInt(0); NanoHTTPDWebserver server = servers.get(port); if (server == null) { callbackContext.error("Server not started on port " + port); return; } server.stop(); servers.remove(port); callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); } /** * Will be called if the js context sends an response to the webserver * * @param args {UUID: {...}} * @param callbackContext * @throws JSONException */ private void send(JSONArray args, CallbackContext callbackContext) throws JSONException { Integer port = args.getInt(0); NanoHTTPDWebserver server = this.servers.get(port); if (server == null) { callbackContext.sendPluginResult( new PluginResult(PluginResult.Status.ERROR, "Server not running") ); return; } server.responses.put(args.getString(1), args.get(2)); callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); } } ================================================ FILE: src/plugins/server/www/server.js ================================================ module.exports = function (port, onRequest, onError) { cordova.exec(onRequest, onError, 'Server', 'start', [port]); return { stop: function (onSuccess, onError) { onSuccess = onSuccess || function () { }; onError = onError || console.error.bind(console); cordova.exec(onSuccess, onError, 'Server', 'stop', [port]); }, send: function (req_id, data, onSuccess, onError) { onSuccess = onSuccess || function () { }; onError = onError || console.error.bind(console); cordova.exec(onSuccess, onError, 'Server', 'send', [port, req_id, data]); }, setOnRequestHandler: function (onRequest, onError) { onError = onError || console.error.bind(console); cordova.exec(onRequest, onError, 'Server', 'setOnRequestHandler', [port]); }, port: port } } ================================================ FILE: src/plugins/sftp/LICENSE.md ================================================ - GoldRaccoon is under [original author's license](https://github.com/albertodebortoli/GoldRaccoon/blob/master/LICENSE.markdown) - ftp4j is under [LGPL](http://opensource.org/licenses/LGPL-2.1) - All other codes (writen by me) are under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) ================================================ FILE: src/plugins/sftp/README.md ================================================ # cordova-plugin-sftp ## Description This cordova plugin is created to use sftp (client) in Cordova apps. Support **Android** platform for now. You can do the following things: - Execute a command - Download a file (with percent info) - Upload a file (with percent info) - Connect using private key or password ## Installation ```sh $ cordova plugin add cordova-plugin-sftp $ cordova prepare ``` ================================================ FILE: src/plugins/sftp/index.d.ts ================================================ interface Stats { canRead: boolean; canWrite: boolean; exists: boolean; //indicates if file can be found on device storage isDirectory: boolean; isFile: boolean; isVirtual: boolean; lastModified: number; length: number; name: string; type: string; uri: string; } interface ExecResult{ code: Number; result: String; } interface Sftp { /** * Executes command on ssh-server * @param command * @param onSucess * @param onFail */ exec(command: String, onSucess: (res: ExecResult)=>void, onFail: (err: any) => void): void; /** * Connects to SFTP server * @param host Hostname of the server * @param port port numer * @param username Username * @param password Password or private key file to authenticate the server * @param onSuccess Callback function on success returns url of copied file/dir * @param onFail Callback function on error returns error object */ connectUsingPassoword(host: String, port: Number, username: String, password: String, onSuccess: () => void, onFail: (err: any) => void): void; /** * Connects to SFTP server * @param host Hostname of the server * @param port port numer * @param username Username * @param keyFile Password or private key file to authenticate the server * @param passphrase Passphrase for keyfile * @param onSuccess Callback function on success returns url of copied file/dir * @param onFail Callback function on error returns error object */ connectUsingKeyFile(host: String, port: Number, username: String, keyFile: String, passphrase: String, onSuccess: () => void, onFail: (err: any) => void): void; /** * Gets file from the server. * @param filename * @param localFilename copy/shadow of remote file. * @param onSuccess * @param onFail */ getFile(filename: String, localFilename: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Uploaded the file to server * @param filename * @param localFilename copy/shadow of remote file. * @param onSuccess * @param onFail */ putFile(filename: String, localFilename: String, onSuccess: (url: String) => void, onFail: (err: any) => void): void; /** * Closes the connection * @param onSuccess * @param onFail */ close(onSuccess: () => void, onFail: (err: any) => void): void; /** * Gets wether server is connected or not. * @param onSuccess * @param onFail */ isConnected(onSuccess: (connectionId: String) => void, onFail: (err: any) => void): void; } declare var sftp: Sftp; ================================================ FILE: src/plugins/sftp/package.json ================================================ { "name": "cordova-plugin-sftp", "version": "1.1.1", "description": "This cordova plugin is created to use sftp (client) in web/js.", "cordova": { "id": "cordova-plugin-sftp", "platforms": [ "android" ] }, "keywords": [ "cordova", "sftp", "cordova-android" ], "author": "pax", "license": "Apache-2.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" } } ================================================ FILE: src/plugins/sftp/plugin.xml ================================================ Sftp Cordova Sftp Plugin MIT cordova,ftp https://github.com/foxdebug/cordova-plugin-sftp.git ================================================ FILE: src/plugins/sftp/src/com/foxdebug/sftp/Sftp.java ================================================ package com.foxdebug.sftp; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.util.Log; import androidx.documentfile.provider.DocumentFile; import com.sshtools.client.SshClient; import com.sshtools.client.SshClient.SshClientBuilder; import com.sshtools.client.sftp.SftpClient; import com.sshtools.client.sftp.SftpClient.SftpClientBuilder; import com.sshtools.client.sftp.SftpFile; import com.sshtools.client.sftp.TransferCancelledException; import com.sshtools.common.permissions.PermissionDeniedException; import com.sshtools.common.publickey.InvalidPassphraseException; import com.sshtools.common.publickey.SshKeyUtils; import com.sshtools.common.sftp.SftpFileAttributes; import com.sshtools.common.sftp.SftpStatusException; import com.sshtools.common.ssh.SshException; import com.sshtools.common.ssh.components.SshKeyPair; import com.sshtools.common.ssh.components.jce.JCEProvider; import com.sshtools.common.util.FileUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.lang.SecurityException; import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.channels.UnresolvedAddressException; import java.nio.charset.StandardCharsets; import java.security.Security; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Arrays; import org.bouncycastle.jce.provider.BouncyCastleProvider; public class Sftp extends CordovaPlugin { private static final String TAG = "SFTP"; private SshClient ssh; private SftpClient sftp; private Context context; private Activity activity; private String connectionID; public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); context = cordova.getContext(); activity = cordova.getActivity(); System.setProperty("maverick.log.nothread", "true"); } public boolean execute( String action, JSONArray args, CallbackContext callback ) { try { Method method = getClass() .getDeclaredMethod(action, JSONArray.class, CallbackContext.class); if (method != null) { method.invoke(this, args, callback); return true; } } catch (NoSuchMethodException e) { callback.error("Method not found: " + action); return false; } catch (SecurityException e) { callback.error("Security exception: " + e.getMessage()); return false; } catch (Exception e) { callback.error("Exception: " + e.getMessage()); return false; } return false; } public void connectUsingPassword(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String host = args.optString(0); int port = args.optInt(1); String username = args.optString(2); String password = args.optString(3); JCEProvider.enableBouncyCastle(true); Log.d( TAG, "Connecting to " + host + ":" + port + " as " + username ); ssh = SshClientBuilder.create() .withHostname(host) .withPort(port) .withUsername(username) .withPassword(password) .build(); if (ssh.isConnected()) { connectionID = username + "@" + host; try { sftp = SftpClientBuilder.create().withClient(ssh).build(); } catch (IOException | SshException e) { ssh.close(); callback.error( "Failed to initialize SFTP subsystem: " + errMessage(e) ); Log.e(TAG, "Failed to initialize SFTP subsystem", e); return; } try { sftp.getSubsystemChannel().setCharsetEncoding("UTF-8"); } catch (UnsupportedEncodingException | SshException e) { // Fallback to default encoding if UTF-8 fails Log.w( TAG, "Failed to set UTF-8 encoding, falling back to default", e ); } callback.success(); Log.d(TAG, "Connected successfully to " + connectionID); return; } callback.error("Failed to establish SSH connection"); } catch (UnresolvedAddressException e) { callback.error("Cannot resolve host address"); Log.e(TAG, "Cannot resolve host address", e); } catch (PermissionDeniedException e) { callback.error("Authentication failed: " + e.getMessage()); Log.e(TAG, "Authentication failed", e); } catch (SshException e) { callback.error("SSH error: " + errMessage(e)); Log.e(TAG, "SSH error", e); } catch (IOException e) { callback.error("I/O error: " + errMessage(e)); Log.e(TAG, "I/O error", e); } catch (Exception e) { callback.error("Unexpected error: " + errMessage(e)); Log.e(TAG, "Unexpected error", e); } } } ); } public void connectUsingKeyFile(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String host = args.optString(0); int port = args.optInt(1); String username = args.optString(2); String keyFile = args.optString(3); String passphrase = args.optString(4); DocumentFile file = DocumentFile.fromSingleUri( context, Uri.parse(keyFile) ); Uri uri = file.getUri(); ContentResolver contentResolver = context.getContentResolver(); InputStream in = contentResolver.openInputStream(uri); // for `appDataDirectory`, Ref: https://developer.android.com/reference/android/content/Context#getExternalFilesDir(java.lang.String) // the absolute path to application-specific directory. May return *null* if shared storage is not currently available. File appDataDirectory = context.getExternalFilesDir(null); if (appDataDirectory != null) { com.sshtools.common.logger.Log.getDefaultContext().enableFile(com.sshtools.common.logger.Log.Level.DEBUG, new File(appDataDirectory,"synergy.log")); } // JCEProvider.enableBouncyCastle(false); Log.i(TAG, "All Available Security Providers (Security.getProviders() : " + Arrays.toString(Security.getProviders())); Log.i(TAG, "All Available Security Providers for ED25519 (Security.getProviders(\"KeyPairGenerator.Ed25519\"\") : " + Arrays.toString(Security.getProviders("KeyPairGenerator.Ed25519"))); Log.i(TAG, "BC Security Provider Name (`Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)`) : " + Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)); Security.removeProvider("BC"); Security.insertProviderAt(new BouncyCastleProvider(), 1); Log.i(TAG, "(After Inserting BC) All Available Security Providers (Security.getProviders() : " + Arrays.toString(Security.getProviders())); Log.i(TAG, "(After Inserting BC) All Available Security Providers for ED25519 (Security.getProviders(\"KeyPairGenerator.Ed25519\"\") : " + Arrays.toString(Security.getProviders("KeyPairGenerator.Ed25519"))); Log.i(TAG, "(After Inserting BC) BC Security Provider Name (`Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)`) : " + Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)); SshKeyPair keyPair = null; try { keyPair = SshKeyUtils.getPrivateKey(in, passphrase); } catch (InvalidPassphraseException e) { callback.error("Invalid passphrase for key file"); Log.e(TAG, "Invalid passphrase for key file", e); return; } catch (IOException e) { callback.error("Could not read key file: " + errMessage(e)); Log.e(TAG, "Could not read key file", e); return; } ssh = SshClientBuilder.create() .withHostname(host) .withPort(port) .withUsername(username) .withIdentities(keyPair) .build(); if (ssh.isConnected()) { connectionID = username + "@" + host; try { sftp = SftpClientBuilder.create().withClient(ssh).build(); } catch (IOException | SshException e) { ssh.close(); callback.error( "Failed to initialize SFTP subsystem: " + errMessage(e) ); Log.e(TAG, "Failed to initialize SFTP subsystem", e); return; } try { sftp.getSubsystemChannel().setCharsetEncoding("UTF-8"); } catch (UnsupportedEncodingException | SshException e) { // Fallback to default encoding if UTF-8 fails Log.w( TAG, "Failed to set UTF-8 encoding, falling back to default", e ); } callback.success(); Log.d(TAG, "Connected successfully to " + connectionID); return; } callback.error("Failed to establish SSH connection"); } catch (UnresolvedAddressException e) { callback.error("Cannot resolve host address"); Log.e(TAG, "Cannot resolve host address", e); } catch (PermissionDeniedException e) { callback.error("Authentication failed: " + e.getMessage()); Log.e(TAG, "Authentication failed", e); } catch (SshException e) { callback.error("SSH error: " + errMessage(e)); Log.e(TAG, "SSH error", e); } catch (IOException e) { callback.error("I/O error: " + errMessage(e)); Log.e(TAG, "I/O error", e); } catch (SecurityException e) { callback.error("Security error: " + errMessage(e)); Log.e(TAG, "Security error", e); } catch (Exception e) { callback.error("Unexpected error: " + errMessage(e)); Log.e(TAG, "Unexpected error", e); } } } ); } public void exec(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String command = args.optString(0); if (ssh != null) { JSONObject res = new JSONObject(); StringBuffer buffer = new StringBuffer(); int code = ssh.executeCommandWithResult(command, buffer); String result = buffer.toString(); res.put("code", code); res.put("result", result); callback.success(res); return; } callback.error("Not connected"); } catch (IOException | JSONException e) { callback.error(errMessage(e)); } } } ); } public void getFile(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String filename = args.optString(0); String localFilename = args.optString(1); if (ssh != null && sftp != null) { URI uri = new URI(localFilename); DocumentFile file = DocumentFile.fromSingleUri( context, Uri.parse(localFilename) ); Uri fileUri = file.getUri(); ContentResolver contentResolver = context.getContentResolver(); try ( InputStream inputStream = sftp.getInputStream(filename); java.io.OutputStream outputStream = contentResolver.openOutputStream(fileUri, "wt") ) { byte[] buffer = new byte[32768]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } outputStream.flush(); callback.success(); return; } catch (SftpStatusException e) { callback.error("SFTP transfer error: " + errMessage(e)); return; } } Log.d("getFile", "ssh or sftp is null"); callback.error("Not connected"); } catch ( IOException | URISyntaxException | SecurityException | SshException e ) { Log.e("getFile", "Error downloading file", e); callback.error("File transfer error: " + errMessage(e)); } } } ); } public void putFile(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String remoteFilename = args.optString(0); String localFilename = args.optString(1); if (ssh == null || sftp == null) { callback.error("Not connected"); return; } if (remoteFilename == null || remoteFilename.isEmpty()) { callback.error("Remote filename is required"); return; } if (localFilename == null || localFilename.isEmpty()) { callback.error("Local filename is required"); return; } File localFile; try { URI uri = new URI(localFilename); localFile = new File(uri); } catch (URISyntaxException e) { callback.error("Invalid local URI: " + errMessage(e)); return; } if (!localFile.exists() || !localFile.canRead()) { callback.error("Local file does not exist or is not readable"); return; } try { sftp.put(localFile.getAbsolutePath(), remoteFilename); callback.success("File uploaded successfully"); } catch (IOException e) { callback.error("Error uploading file: " + errMessage(e)); } } catch (Exception e) { callback.error(errMessage(e)); } } } ); } public void lsDir(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String path = args.optString(0); if (ssh != null && sftp != null) { JSONArray files = new JSONArray(); for (SftpFile file : sftp.ls(path)) { String filename = file.getFilename(); if (filename.equals(".") || filename.equals("..")) { continue; } SftpFileAttributes fileAttributes = file.attributes(); JSONObject fileInfo = new JSONObject(); fileInfo.put("name", filename); fileInfo.put("exists", true); if (fileAttributes != null) { String permissions = fileAttributes.toPermissionsString(); boolean canRead = permissions.charAt(1) == 'r'; boolean canWrite = permissions.charAt(2) == 'w'; fileInfo.put("canRead", canRead); fileInfo.put("canWrite", canWrite); fileInfo.put("permissions", permissions); fileInfo.put("length", fileAttributes.size()); fileInfo.put("url", file.getAbsolutePath()); fileInfo.put( "lastModified", fileAttributes.lastModifiedTime() ); if (permissions.charAt(0) == 'l') { fileInfo.put("isLink", true); try { String linkTarget = sftp.getSymbolicLinkTarget( file.getAbsolutePath() ); fileInfo.put("linkTarget", linkTarget); SftpFileAttributes linkAttributes = sftp.stat( linkTarget ); fileInfo.put("isFile", linkAttributes.isFile()); fileInfo.put( "isDirectory", linkAttributes.isDirectory() ); } catch (SftpStatusException | SshException e) { // Handle broken symlink fileInfo.put("isFile", false); fileInfo.put("isDirectory", false); fileInfo.put("isLink", false); } } else { fileInfo.put("isLink", false); fileInfo.put("isDirectory", fileAttributes.isDirectory()); fileInfo.put("isFile", fileAttributes.isFile()); } } files.put(fileInfo); } callback.success(files); return; } callback.error("Not connected"); } catch (SftpStatusException | JSONException | SshException e) { callback.error(errMessage(e)); } } } ); } public void stat(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String path = sanitizePath(args.optString(0)); if (ssh != null && sftp != null) { URI uri = new URI(path); JSONObject fileStat = new JSONObject(); try { SftpFileAttributes fileAttributes = sftp.stat(uri.getPath()); if (fileAttributes != null) { String permissions = fileAttributes.toPermissionsString(); boolean canRead = permissions.charAt(1) == 'r'; boolean canWrite = permissions.charAt(2) == 'w'; fileStat.put("exists", true); fileStat.put("canRead", canRead); fileStat.put("canWrite", canWrite); fileStat.put("isLink", fileAttributes.isLink()); fileStat.put("isDirectory", fileAttributes.isDirectory()); fileStat.put("isFile", fileAttributes.isFile()); fileStat.put("length", fileAttributes.size()); fileStat.put( "permissions", fileAttributes.toPermissionsString() ); fileStat.put( "lastModified", fileAttributes.lastModifiedTime() ); String[] pathSegments = uri.getPath().split("/"); String filename = pathSegments[pathSegments.length - 1]; fileStat.put("name", filename); fileStat.put("url", uri.getPath()); if (permissions.charAt(0) == 'l') { fileStat.put("isLink", true); try { String linkTarget = sftp.getSymbolicLinkTarget( uri.getPath() ); fileStat.put("linkTarget", linkTarget); SftpFileAttributes linkAttributes = sftp.stat( linkTarget ); fileStat.put("isFile", linkAttributes.isFile()); fileStat.put( "isDirectory", linkAttributes.isDirectory() ); } catch (SftpStatusException | SshException e) { // Handle broken symlink fileStat.put("isFile", false); fileStat.put("isDirectory", false); fileStat.put("isLink", false); fileStat.put("exists", false); } } else { fileStat.put("isLink", false); fileStat.put("isDirectory", fileAttributes.isDirectory()); fileStat.put("isFile", fileAttributes.isFile()); } } } catch (SftpStatusException e) { fileStat.put("exists", false); fileStat.put("url", uri.getPath()); } callback.success(fileStat); return; } callback.error("Not connected"); } catch (URISyntaxException | JSONException | SshException e) { callback.error(errMessage(e)); } } } ); } public void mkdir(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String path = args.optString(0); if (ssh != null && sftp != null) { sftp.mkdir(path); callback.success(); return; } callback.error("Not connected"); } catch (SftpStatusException | SshException e) { callback.error(errMessage(e)); } } } ); } public void rm(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String path = args.optString(0); boolean force = args.optBoolean(1, false); boolean recurse = args.optBoolean(2, false); if (ssh != null && sftp != null) { sftp.rm(path, force, recurse); callback.success(); return; } callback.error("Not connected"); } catch (SftpStatusException | SshException e) { callback.error(errMessage(e)); } } } ); } public void createFile(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String path = args.optString(0); String content = args.optString(1, ""); if (ssh != null && sftp != null) { try { SftpFileAttributes attrs = sftp.stat(path); if (attrs != null && attrs.isFile()) { callback.error("File already exists"); return; } } catch (SftpStatusException e) { // File doesn't exist, continue with creation } java.io.ByteArrayInputStream inputStream; if (content.isEmpty()) { inputStream = new java.io.ByteArrayInputStream(new byte[0]); } else { inputStream = new java.io.ByteArrayInputStream( content.getBytes(StandardCharsets.UTF_8) ); } sftp.put(inputStream, path); callback.success(); return; } callback.error("Not connected"); } catch ( SftpStatusException | SshException | TransferCancelledException e ) { callback.error(errMessage(e)); } } } ); } public void rename(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { String oldpath = args.optString(0); String newpath = args.optString(1); if (ssh != null && sftp != null) { sftp.rename(oldpath, newpath); callback.success(); return; } callback.error("Not connected"); } catch (SftpStatusException | SshException e) { callback.error(errMessage(e)); } } } ); } public void pwd(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { if (ssh != null && sftp != null) { String pwd = sftp.pwd(); callback.success(pwd); return; } callback.error("Not connected"); } catch (SftpStatusException | SshException e) { callback.error(errMessage(e)); } } } ); } private String sanitizePath(String path) { try { String decodedPath = URLDecoder.decode( path, StandardCharsets.UTF_8.toString() ); String encodedPath = URLEncoder.encode( decodedPath, StandardCharsets.UTF_8.toString() ) .replace("+", "%20") // Replace + with %20 for spaces .replace("%2F", "/") // Preserve forward slashes .replace("%5C", "\\"); // Preserve backslashes if needed return encodedPath; } catch (UnsupportedEncodingException e) { return path; // Return original if encoding fails } } public void close(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { try { if (ssh != null) { ssh.close(); sftp.quit(); callback.success(); return; } callback.error("Not connected"); } catch (IOException | SshException e) { callback.error(errMessage(e)); } } } ); } public void isConnected(JSONArray args, CallbackContext callback) { cordova .getThreadPool() .execute( new Runnable() { public void run() { if ( ssh != null && ssh.isConnected() && sftp != null && !sftp.isClosed() ) { callback.success(connectionID); return; } callback.success(0); } } ); } public String errMessage(Exception e) { String res = e.getMessage(); if (res == null || res.equals("")) { return e.toString(); } return res; } } ================================================ FILE: src/plugins/sftp/www/sftp.js ================================================ module.exports = { exec: function (command, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'exec', [command]); }, connectUsingPassword: function (host, port, username, password, onSuccess, onFail) { if (typeof port != 'number') { throw new Error('Port must be number'); } port = Number.parseInt(port); cordova.exec(onSuccess, onFail, 'Sftp', 'connectUsingPassword', [host, port, username, password]); }, connectUsingKeyFile: function (host, port, username, keyFile, passphrase, onSuccess, onFail) { if (typeof port != 'number') { throw new Error('Port must be number'); } port = Number.parseInt(port); cordova.exec(onSuccess, onFail, 'Sftp', 'connectUsingKeyFile', [host, port, username, keyFile, passphrase]); }, getFile: function (filename, localFilename, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'getFile', [filename, localFilename]); }, putFile: function (filename, localFilename, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'putFile', [filename, localFilename]); }, lsDir: function (path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'lsDir', [path]); }, stat: function (path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'stat', [path]); }, mkdir: function (path, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'mkdir', [path]); }, rm: function (path, force, recurse, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'rm', [path, force, recurse]); }, createFile: function (path, content, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'createFile', [path, content]); }, rename: function (oldpath, newpath, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'rename', [oldpath, newpath]); }, pwd: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'pwd', []); }, close: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'close', []); }, isConnected: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'Sftp', 'isConnected', []); } }; ================================================ FILE: src/plugins/system/android/com/foxdebug/system/RewardPassManager.java ================================================ package com.foxdebug.system; import android.content.Context; import android.util.Log; import com.foxdebug.acode.rk.auth.EncryptedPreferenceManager; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Random; import org.json.JSONException; import org.json.JSONObject; public class RewardPassManager { private static final String TAG = "SystemRewardPass"; private static final String ADS_PREFS_FILENAME = "ads"; private static final String KEY_REWARD_STATE = "reward_state"; private static final long ONE_HOUR_MS = 60L * 60L * 1000L; private static final long MAX_ACTIVE_PASS_MS = 10L * ONE_HOUR_MS; private static final int MAX_REDEMPTIONS_PER_DAY = 3; private final EncryptedPreferenceManager adsPrefManager; private final Random random = new Random(); public RewardPassManager(Context context) { this.adsPrefManager = new EncryptedPreferenceManager(context, ADS_PREFS_FILENAME); } public String getRewardStatus() throws JSONException { JSONObject state = syncRewardState(loadRewardState()); JSONObject status = buildRewardStatus(state); if (status.optBoolean("hasPendingExpiryNotice")) { state.put("expiryNoticePendingUntil", 0L); } saveRewardState(state); return status.toString(); } public String redeemReward(String offerId) throws JSONException { JSONObject state = syncRewardState(loadRewardState()); int redemptionsToday = state.optInt("redemptionsToday", 0); long now = java.lang.System.currentTimeMillis(); long adFreeUntil = state.optLong("adFreeUntil", 0L); long remainingMs = Math.max(0L, adFreeUntil - now); if (redemptionsToday >= MAX_REDEMPTIONS_PER_DAY) { throw new JSONException( "Daily limit reached. You can redeem up to " + MAX_REDEMPTIONS_PER_DAY + " rewards per day." ); } if (remainingMs >= MAX_ACTIVE_PASS_MS) { throw new JSONException("You already have the maximum 10 hours of ad-free time active."); } long grantedDurationMs = resolveRewardDuration(offerId); long baseTime = Math.max(now, adFreeUntil); long newAdFreeUntil = Math.min(baseTime + grantedDurationMs, now + MAX_ACTIVE_PASS_MS); long appliedDurationMs = Math.max(0L, newAdFreeUntil - baseTime); state.put("adFreeUntil", newAdFreeUntil); state.put("lastExpiredRewardUntil", 0L); state.put("expiryNoticePendingUntil", 0L); state.put("redemptionDay", getTodayKey()); state.put("redemptionsToday", redemptionsToday + 1); saveRewardState(state); JSONObject status = buildRewardStatus(state); status.put("grantedDurationMs", grantedDurationMs); status.put("appliedDurationMs", appliedDurationMs); status.put("offerId", offerId); return status.toString(); } private JSONObject loadRewardState() { String raw = adsPrefManager.getString(KEY_REWARD_STATE, ""); if (raw == null || raw.isEmpty()) { return defaultRewardState(); } try { return mergeRewardState(new JSONObject(raw)); } catch (JSONException error) { Log.w(TAG, "Failed to parse reward state, resetting.", error); return defaultRewardState(); } } private JSONObject defaultRewardState() { JSONObject state = new JSONObject(); try { state.put("adFreeUntil", 0L); state.put("lastExpiredRewardUntil", 0L); state.put("expiryNoticePendingUntil", 0L); state.put("redemptionDay", getTodayKey()); state.put("redemptionsToday", 0); } catch (JSONException ignored) { } return state; } private JSONObject mergeRewardState(JSONObject parsed) { JSONObject state = defaultRewardState(); try { state.put("adFreeUntil", parsed.optLong("adFreeUntil", 0L)); state.put("lastExpiredRewardUntil", parsed.optLong("lastExpiredRewardUntil", 0L)); state.put("expiryNoticePendingUntil", parsed.optLong("expiryNoticePendingUntil", 0L)); state.put("redemptionDay", parsed.optString("redemptionDay", getTodayKey())); state.put("redemptionsToday", parsed.optInt("redemptionsToday", 0)); } catch (JSONException ignored) { } return state; } private void saveRewardState(JSONObject state) { adsPrefManager.setString(KEY_REWARD_STATE, state.toString()); } private JSONObject syncRewardState(JSONObject state) throws JSONException { String todayKey = getTodayKey(); if (!todayKey.equals(state.optString("redemptionDay", todayKey))) { state.put("redemptionDay", todayKey); state.put("redemptionsToday", 0); } long adFreeUntil = state.optLong("adFreeUntil", 0L); long now = java.lang.System.currentTimeMillis(); if (adFreeUntil > 0L && adFreeUntil <= now) { if (state.optLong("expiryNoticePendingUntil", 0L) != adFreeUntil) { state.put("expiryNoticePendingUntil", adFreeUntil); } state.put("lastExpiredRewardUntil", adFreeUntil); state.put("adFreeUntil", 0L); } return state; } private JSONObject buildRewardStatus(JSONObject state) throws JSONException { long now = java.lang.System.currentTimeMillis(); long adFreeUntil = state.optLong("adFreeUntil", 0L); int redemptionsToday = state.optInt("redemptionsToday", 0); long remainingMs = Math.max(0L, adFreeUntil - now); int remainingRedemptions = Math.max(0, MAX_REDEMPTIONS_PER_DAY - redemptionsToday); JSONObject status = new JSONObject(); status.put("adFreeUntil", adFreeUntil); status.put("lastExpiredRewardUntil", state.optLong("lastExpiredRewardUntil", 0L)); status.put("isActive", adFreeUntil > now); status.put("remainingMs", remainingMs); status.put("redemptionsToday", redemptionsToday); status.put("remainingRedemptions", remainingRedemptions); status.put("maxRedemptionsPerDay", MAX_REDEMPTIONS_PER_DAY); status.put("maxActivePassMs", MAX_ACTIVE_PASS_MS); status.put("hasPendingExpiryNotice", state.optLong("expiryNoticePendingUntil", 0L) > 0L); status.put("expiryNoticePendingUntil", state.optLong("expiryNoticePendingUntil", 0L)); boolean canRedeem = remainingRedemptions > 0 && remainingMs < MAX_ACTIVE_PASS_MS; status.put("canRedeem", canRedeem); status.put("redeemDisabledReason", getRedeemDisabledReason(remainingRedemptions, remainingMs)); return status; } private String getRedeemDisabledReason(int remainingRedemptions, long remainingMs) { if (remainingRedemptions <= 0) { return "Daily limit reached. You can redeem up to " + MAX_REDEMPTIONS_PER_DAY + " rewards per day."; } if (remainingMs >= MAX_ACTIVE_PASS_MS) { return "You already have the maximum 10 hours of ad-free time active."; } return ""; } private long resolveRewardDuration(String offerId) throws JSONException { if ("quick".equals(offerId)) { return ONE_HOUR_MS; } if ("focus".equals(offerId)) { int selectedHours = 4 + random.nextInt(3); return selectedHours * ONE_HOUR_MS; } throw new JSONException("Unknown reward offer."); } private String getTodayKey() { return new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date()); } } ================================================ FILE: src/plugins/system/android/com/foxdebug/system/SoftInputAssist.java ================================================ package com.foxdebug.system; import android.app.Activity; import android.view.View; import java.util.List; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsAnimationCompat; public class SoftInputAssist { private boolean animationRunning = false; public SoftInputAssist(Activity activity) { View contentView = activity.findViewById(android.R.id.content); ViewCompat.setOnApplyWindowInsetsListener(contentView, (v, insets) -> { if (!animationRunning) { Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); Insets nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars()); int keyboardHeight = Math.max(0, ime.bottom - nav.bottom); v.setPadding(0, 0, 0, keyboardHeight); } return insets; }); ViewCompat.setWindowInsetsAnimationCallback( contentView, new WindowInsetsAnimationCompat.Callback( WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE ) { @Override public void onPrepare(WindowInsetsAnimationCompat animation) { animationRunning = true; } @Override public void onEnd(WindowInsetsAnimationCompat animation) { animationRunning = false; } @Override public WindowInsetsCompat onProgress( WindowInsetsCompat insets, List runningAnimations) { Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime()); Insets nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars()); int keyboardHeight = Math.max(0, ime.bottom - nav.bottom); contentView.setPadding(contentView.getPaddingLeft(), contentView.getPaddingTop(), contentView.getPaddingRight(), keyboardHeight); return insets; } } ); } } ================================================ FILE: src/plugins/system/android/com/foxdebug/system/System.java ================================================ package com.foxdebug.system; import static android.os.Build.VERSION.SDK_INT; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.io.IOException; import android.app.Activity; import android.app.PendingIntent; import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.*; import android.content.pm.*; import java.util.*; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.ImageDecoder; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.provider.Settings.Global; import android.util.Base64; import android.view.View; import android.view.Window; import android.view.WindowInsetsController; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; import androidx.core.content.FileProvider; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import com.foxdebug.system.Ui.Theme; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaWebView; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import android.content.Context; import android.net.Uri; import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import android.webkit.MimeTypeMap; // DocumentFile import (AndroidX library) import androidx.documentfile.provider.DocumentFile; // Java I/O imports import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.io.BufferedReader; import java.io.InputStreamReader; import android.os.Build; import android.os.Environment; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.provider.DocumentsContract; import android.provider.Settings; import androidx.core.content.ContextCompat; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.Canvas; import android.util.TypedValue; import android.provider.MediaStore; import android.graphics.Bitmap; import android.os.Build; import android.graphics.ImageDecoder; import java.security.MessageDigest; import java.security.MessageDigest; public class System extends CordovaPlugin { private static final String TAG = "SystemPlugin"; private CallbackContext requestPermissionCallback; private Activity activity; private Context context; private int REQ_PERMISSIONS = 1; private int REQ_PERMISSION = 2; private int systemBarColor = 0xFF000000; private Theme theme; private CallbackContext intentHandler; private CordovaWebView webView; private String fileProviderAuthority; private RewardPassManager rewardPassManager; public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.context = cordova.getContext(); this.activity = cordova.getActivity(); this.webView = webView; this.rewardPassManager = new RewardPassManager(this.context); this.activity.runOnUiThread( new Runnable() { @Override public void run() { setNativeContextMenuDisabled(false); } } ); // Set up global exception handler Thread.setDefaultUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); String stackTrace = sw.toString(); String errorMsg = String.format( "Uncaught Exception: %s\nStack trace: %s", ex.getMessage(), stackTrace ); sendLogToJavaScript("error", errorMsg); // rethrow to the default handler Thread.getDefaultUncaughtExceptionHandler() .uncaughtException(thread, ex); } } ); } public boolean execute( String action, final JSONArray args, final CallbackContext callbackContext ) throws JSONException { final String arg1 = args.optString(0); final String arg2 = args.optString(1); final String arg3 = args.optString(2); final String arg4 = args.optString(3); final String arg5 = args.optString(4); final String arg6 = args.optString(5); switch (action) { case "get-webkit-info": case "file-action": case "checksumText": case "is-powersave-mode": case "get-app-info": case "add-shortcut": case "remove-shortcut": case "pin-shortcut": case "get-android-version": case "request-permissions": case "request-permission": case "has-permission": case "open-in-browser": case "launch-app": case "get-global-setting": case "get-available-encodings": case "decode": case "encode": case "copyToUri": case "compare-file-text": case "compare-texts": case "pin-file-shortcut": break; case "get-configuration": getConfiguration(callbackContext); return true; case "set-input-type": setInputType(arg1); callbackContext.success(); return true; case "set-native-context-menu-disabled": this.cordova.getActivity() .runOnUiThread( new Runnable() { @Override public void run() { setNativeContextMenuDisabled(Boolean.parseBoolean(arg1)); callbackContext.success(); } } ); return true; case "get-cordova-intent": getCordovaIntent(callbackContext); return true; case "set-intent-handler": setIntentHandler(callbackContext); return true; case "set-ui-theme": this.cordova.getActivity() .runOnUiThread( new Runnable() { public void run() { setUiTheme(arg1, args.optJSONObject(1), callbackContext); } } ); return true; case "clear-cache": this.cordova.getActivity() .runOnUiThread( new Runnable() { public void run() { clearCache(callbackContext); } } ); return true; case "fileExists": callbackContext.success(fileExists(args.getString(0), args.getString(1)) ? 1 : 0); return true; case "createSymlink": boolean success = createSymlink(args.getString(0), args.getString(1)); callbackContext.success(success ? 1 : 0); return true; case "getNativeLibraryPath": callbackContext.success(getNativeLibraryPath()); return true; case "getFilesDir": callbackContext.success(getFilesDir()); return true; case "getRewardStatus": callbackContext.success(rewardPassManager.getRewardStatus()); return true; case "redeemReward": callbackContext.success(rewardPassManager.redeemReward(args.getString(0))); return true; case "getParentPath": callbackContext.success(getParentPath(args.getString(0))); return true; case "listChildren": callbackContext.success(listChildren(args.getString(0))); return true; case "writeText": { try { String filePath = args.getString(0); String content = args.getString(1); Files.write(Paths.get(filePath), Collections.singleton(content), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); callbackContext.success("File written successfully"); } catch (Exception e) { callbackContext.error("Failed to write file: " + e.getMessage()); } return true; } case "getArch": String arch; if (android.os.Build.VERSION.SDK_INT >= 21) { arch = android.os.Build.SUPPORTED_ABIS[0]; } else { arch = android.os.Build.CPU_ABI; } callbackContext.success(arch); return true; case "requestStorageManager": if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { try { Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); intent.setData(Uri.parse("package:" + context.getPackageName())); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); callbackContext.success("true"); } catch (Exception e) { // Fallback to general settings if specific one fails Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); callbackContext.success("true"); } } else { callbackContext.success("false"); // Not needed on Android < 11 } return true; case "hasGrantedStorageManager": boolean granted; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { granted = Environment.isExternalStorageManager(); } else { // Fallback for Android 10 and below granted = ContextCompat.checkSelfPermission( context, Manifest.permission.READ_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_GRANTED; } callbackContext.success(String.valueOf(granted)); return true; case "isManageExternalStorageDeclared": PackageManager pm = context.getPackageManager(); try { PackageInfo info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); String[] permissions = info.requestedPermissions; String isDeclared = "false"; if (permissions != null) { for (String perm: permissions) { if (perm.equals("android.permission.MANAGE_EXTERNAL_STORAGE")) { isDeclared = "true"; break; } } } callbackContext.success(isDeclared); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); callbackContext.error(e.toString()); } return true; case "mkdirs": if (new File(args.getString(0)).mkdirs()) { callbackContext.success(); } else { callbackContext.error("mkdirs failed"); } return true; case "deleteFile": if (new File(args.getString(0)).delete()) { callbackContext.success(); } else { callbackContext.error("delete failed"); } return true; case "setExec": if (new File(args.getString(0)).setExecutable(Boolean.parseBoolean(args.getString(1)))) { callbackContext.success(); } else { callbackContext.error("set exec failed"); } return true; default: return false; } cordova .getThreadPool() .execute( new Runnable() { public void run() { switch (action) { case "copyToUri": try { //srcUri is a file Uri srcUri = Uri.parse(args.getString(0)); //destUri is a directory Uri destUri = Uri.parse(args.getString(1)); //create a file named this into the dest Directory and copy the srcUri into it String fileName = args.getString(2); InputStream in = null; OutputStream out = null; try { // Open input stream from the source URI if ("file".equalsIgnoreCase(srcUri.getScheme())) { File file = new File(srcUri.getPath()); in = new FileInputStream(file); } else { in = context.getContentResolver().openInputStream(srcUri); } // Create the destination file using DocumentFile for better URI handling DocumentFile destFile = null; if ("file".equalsIgnoreCase(destUri.getScheme())) { // Handle file:// scheme using DocumentFile File destDir = new File(destUri.getPath()); if (!destDir.exists()) { destDir.mkdirs(); // Create directory if it doesn't exist } DocumentFile destDocDir = DocumentFile.fromFile(destDir); // Check if file already exists and delete it DocumentFile existingFile = destDocDir.findFile(fileName); if (existingFile != null && existingFile.exists()) { existingFile.delete(); } // Create new file String mimeType = getMimeTypeFromExtension(fileName); destFile = destDocDir.createFile(mimeType, fileName); } else { // Handle content:// scheme using DocumentFile DocumentFile destDocDir = DocumentFile.fromTreeUri(context, destUri); if (destDocDir == null || !destDocDir.exists() || !destDocDir.isDirectory()) { callbackContext.error("Destination directory does not exist or is not accessible"); return; } // Check if file already exists and delete it DocumentFile existingFile = destDocDir.findFile(fileName); if (existingFile != null && existingFile.exists()) { existingFile.delete(); } // Create new file String mimeType = getMimeTypeFromExtension(fileName); destFile = destDocDir.createFile(mimeType, fileName); } if (destFile == null || !destFile.exists()) { callbackContext.error("Failed to create destination file"); return; } // Open output stream to the created file out = context.getContentResolver().openOutputStream(destFile.getUri()); if ( in == null || out == null) { callbackContext.error("uri streams are null"); return; } // Copy stream byte[] buffer = new byte[8192]; int len; while ((len = in .read(buffer)) > 0) { out.write(buffer, 0, len); } out.flush(); callbackContext.success(); } catch (IOException e) { e.printStackTrace(); callbackContext.error(e.toString()); } finally { try { if ( in != null) in .close(); if (out != null) out.close(); } catch (IOException e) { e.printStackTrace(); callbackContext.error(e.toString()); } } } catch (Exception e) { e.printStackTrace(); callbackContext.error(e.toString()); } break; case "get-webkit-info": getWebkitInfo(callbackContext); break; case "file-action": fileAction(arg1, arg2, arg3, arg4, callbackContext); break; case "is-powersave-mode": isPowerSaveMode(callbackContext); break; case "get-app-info": getAppInfo(callbackContext); break; case "pin-file-shortcut": pinFileShortcut(args.optJSONObject(0), callbackContext); break; case "add-shortcut": addShortcut( arg1, arg2, arg3, arg4, arg5, arg6, callbackContext ); break; case "remove-shortcut": removeShortcut(arg1, callbackContext); break; case "pin-shortcut": pinShortcut(arg1, callbackContext); break; case "get-android-version": getAndroidVersion(callbackContext); break; case "request-permissions": requestPermissions(args.optJSONArray(0), callbackContext); break; case "request-permission": requestPermission(arg1, callbackContext); break; case "has-permission": hasPermission(arg1, callbackContext); break; case "open-in-browser": openInBrowser(arg1, callbackContext); break; case "launch-app": launchApp(arg1, arg2, args.optJSONObject(2), callbackContext); break; case "get-global-setting": getGlobalSetting(arg1, callbackContext); break; case "get-available-encodings": getAvailableEncodings(callbackContext); break; case "decode": decode(arg1, arg2, callbackContext); break; case "encode": encode(arg1, arg2, callbackContext); break; case "compare-file-text": compareFileText(arg1, arg2, arg3, callbackContext); break; case "compare-texts": compareTexts(arg1, arg2, callbackContext); break; case "checksumText": cordova.getThreadPool().execute(() -> { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(args.getString(0).getBytes("UTF-8")); StringBuilder hexString = new StringBuilder(); for (byte b : hash) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } callbackContext.success(hexString.toString()); } catch (Exception e) { callbackContext.error(e.getMessage()); } }); break; default: break; } } } ); return true; } private void sendLogToJavaScript(String level, String message) { final String js = "window.log('" + level + "', " + JSONObject.quote(message) + ");"; cordova .getActivity() .runOnUiThread( new Runnable() { @Override public void run() { webView.loadUrl("javascript:" + js); } } ); } // Helper method to determine MIME type using Android's built-in MimeTypeMap private String getMimeTypeFromExtension(String fileName) { String extension = ""; int lastDotIndex = fileName.lastIndexOf('.'); if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) { extension = fileName.substring(lastDotIndex + 1).toLowerCase(); } String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); return mimeType != null ? mimeType : "application/octet-stream"; } private void getConfiguration(CallbackContext callback) { try { JSONObject result = new JSONObject(); Configuration config = context.getResources().getConfiguration(); InputMethodManager imm = (InputMethodManager) context.getSystemService( Context.INPUT_METHOD_SERVICE ); Method method = InputMethodManager.class.getMethod("getInputMethodWindowVisibleHeight"); result.put("isAcceptingText", imm.isAcceptingText()); result.put("keyboardHeight", method.invoke(imm)); result.put("locale", config.locale.toString()); result.put("fontScale", config.fontScale); result.put("keyboard", config.keyboard); result.put("keyboardHidden", config.keyboardHidden); result.put("hardKeyboardHidden", config.hardKeyboardHidden); result.put("navigationHidden", config.navigationHidden); result.put("navigation", config.navigation); result.put("orientation", config.orientation); callback.success(result); } catch ( JSONException | NoSuchMethodException | IllegalAccessException | InvocationTargetException error ) { callback.error(error.toString()); } } private void decode( String content, // base64 encoded string String charSetName, CallbackContext callback ) { try { byte[] bytes = Base64.decode(content, Base64.DEFAULT); if (Charset.isSupported(charSetName) == false) { callback.error("Charset not supported: " + charSetName); return; } Charset charSet = Charset.forName(charSetName); CharBuffer charBuffer = charSet.decode(ByteBuffer.wrap(bytes)); String result = String.valueOf(charBuffer); callback.success(result); } catch (Exception e) { callback.error(e.toString()); } } private void encode( String content, // string to encode String charSetName, CallbackContext callback ) { try { if (Charset.isSupported(charSetName) == false) { callback.error("Charset not supported: " + charSetName); return; } Charset charSet = Charset.forName(charSetName); ByteBuffer byteBuffer = charSet.encode(content); byte[] bytes = new byte[byteBuffer.remaining()]; byteBuffer.get(bytes); callback.success(bytes); } catch (Exception e) { callback.error(e.toString()); } } /** * Compares file content with provided text. * This method runs in a background thread to avoid blocking the UI. * * @param fileUri The URI of the file to read (file:// or content://) * @param encoding The character encoding to use when reading the file * @param currentText The text to compare against the file content * @param callback Returns 1 if texts are different, 0 if same */ private void compareFileText( String fileUri, String encoding, String currentText, CallbackContext callback ) { try { if (fileUri == null || fileUri.isEmpty()) { callback.error("File URI is required"); return; } if (encoding == null || encoding.isEmpty()) { encoding = "UTF-8"; } if (!Charset.isSupported(encoding)) { callback.error("Charset not supported: " + encoding); return; } Uri uri = Uri.parse(fileUri); Charset charset = Charset.forName(encoding); String fileContent; // Handle file:// URIs if ("file".equalsIgnoreCase(uri.getScheme())) { File file = new File(uri.getPath()); // Validate file if (!file.exists()) { callback.error("File does not exist"); return; } if (!file.isFile()) { callback.error("Path is not a file"); return; } if (!file.canRead()) { callback.error("File is not readable"); return; } Path path = file.toPath(); fileContent = new String(Files.readAllBytes(path), charset); } else if ("content".equalsIgnoreCase(uri.getScheme())) { // Handle content:// URIs (including SAF tree URIs) InputStream inputStream = null; try { String uriString = fileUri; Uri resolvedUri = uri; // Check if this is a SAF tree URI with :: separator if (uriString.contains("::")) { try { // Split into tree URI and document ID String[] parts = uriString.split("::", 2); String treeUriStr = parts[0]; String docId = parts[1]; // Build document URI directly from tree URI and document ID Uri treeUri = Uri.parse(treeUriStr); resolvedUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, docId); } catch (Exception e) { callback.error("SAF_FALLBACK: Invalid SAF URI format - " + e.getMessage()); return; } } // Try to open the resolved URI inputStream = context.getContentResolver().openInputStream(resolvedUri); if (inputStream == null) { callback.error("Cannot open file"); return; } StringBuilder sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(inputStream, charset))) { char[] buffer = new char[8192]; int charsRead; while ((charsRead = reader.read(buffer)) != -1) { sb.append(buffer, 0, charsRead); } } fileContent = sb.toString(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException closeError) { Log.w(TAG, "Failed to close input stream while reading file.", closeError); } } } } else { callback.error("Unsupported URI scheme: " + uri.getScheme()); return; } // check length first if (fileContent.length() != currentText.length()) { callback.success(1); // Changed return; } // Full comparison if (fileContent.equals(currentText)) { callback.success(0); // Not changed } else { callback.success(1); // Changed } } catch (Exception e) { callback.error(e.toString()); } } /** * Compares two text strings. * This method runs in a background thread to avoid blocking the UI * for large string comparisons. * * @param text1 First text to compare * @param text2 Second text to compare * @param callback Returns 1 if texts are different, 0 if same */ private void compareTexts( String text1, String text2, CallbackContext callback ) { try { if (text1 == null) text1 = ""; if (text2 == null) text2 = ""; // check length first if (text1.length() != text2.length()) { callback.success(1); // Changed return; } // Full comparison if (text1.equals(text2)) { callback.success(0); // Not changed } else { callback.success(1); // Changed } } catch (Exception e) { callback.error(e.toString()); } } private void getAvailableEncodings(CallbackContext callback) { try { Map < String, Charset > charsets = Charset.availableCharsets(); JSONObject result = new JSONObject(); for (Map.Entry < String, Charset > entry: charsets.entrySet()) { JSONObject obj = new JSONObject(); Charset charset = entry.getValue(); obj.put("label", charset.displayName()); obj.put("aliases", new JSONArray(charset.aliases())); obj.put("name", charset.name()); result.put(charset.name(), obj); } callback.success(result); } catch (Exception e) { callback.error(e.toString()); } } private void requestPermissions(JSONArray arr, CallbackContext callback) { try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { int[] res = new int[arr.length()]; for (int i = 0; i < res.length; ++i) { res[i] = 1; } callback.success(1); return; } String[] permissions = checkPermissions(arr); if (permissions.length > 0) { requestPermissionCallback = callback; cordova.requestPermissions(this, REQ_PERMISSIONS, permissions); return; } callback.success(new JSONArray()); } catch (Exception e) { callback.error(e.toString()); } } private void requestPermission(String permission, CallbackContext callback) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { callback.success(1); return; } if (permission != null || !permission.equals("")) { if (!cordova.hasPermission(permission)) { requestPermissionCallback = callback; cordova.requestPermission(this, REQ_PERMISSION, permission); return; } callback.success(1); return; } callback.error("No permission passed to request."); } private void hasPermission(String permission, CallbackContext callback) { if (permission != null || !permission.equals("")) { int res = 0; if (cordova.hasPermission(permission)) { res = 1; } callback.success(res); return; } callback.error("No permission passed to check."); } public boolean fileExists(String path, String countSymlinks) { boolean followSymlinks = !Boolean.parseBoolean(countSymlinks); File file = new File(path); // Android < O does not implement File#toPath(), fall back to legacy checks if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (!file.exists()) return false; if (followSymlinks) { try { // If canonical and absolute paths differ, it's a symlink return file.getCanonicalPath().equals(file.getAbsolutePath()); } catch (IOException ignored) { return false; } } return true; } Path p = file.toPath(); try { if (followSymlinks) { return Files.exists(p) && !Files.isSymbolicLink(p); } else { return Files.exists(p, LinkOption.NOFOLLOW_LINKS); } } catch (Exception e) { return false; } } public boolean createSymlink(String target, String linkPath) { try { Process process = Runtime.getRuntime().exec(new String[] { "ln", "-s", target, linkPath }); return process.waitFor() == 0; } catch (Exception e) { return false; } } public String getNativeLibraryPath() { ApplicationInfo appInfo = context.getApplicationInfo(); return appInfo.nativeLibraryDir; } public String getFilesDir() { return context.getFilesDir().getAbsolutePath(); } public String getParentPath(String path) { File file = new File(path); File parent = file.getParentFile(); return parent != null ? parent.getAbsolutePath() : null; } public JSONArray listChildren(String path) throws JSONException { File dir = new File(path); JSONArray result = new JSONArray(); if (dir.exists() && dir.isDirectory()) { File[] files = dir.listFiles(); if (files != null) { for (File file: files) { result.put(file.getAbsolutePath()); } } } return result; } public void onRequestPermissionResult( int code, String[] permissions, int[] resCodes ) { if (requestPermissionCallback == null) return; if (code == REQ_PERMISSIONS) { JSONArray resAr = new JSONArray(); for (int res: resCodes) { if (res == PackageManager.PERMISSION_DENIED) { resAr.put(0); } resAr.put(1); } requestPermissionCallback.success(resAr); requestPermissionCallback = null; return; } if ( resCodes.length >= 1 && resCodes[0] == PackageManager.PERMISSION_DENIED ) { requestPermissionCallback.success(0); requestPermissionCallback = null; return; } requestPermissionCallback.success(1); requestPermissionCallback = null; return; } private String[] checkPermissions(JSONArray arr) throws Exception { List < String > list = new ArrayList < String > (); for (int i = 0; i < arr.length(); i++) { try { String permission = arr.getString(i); if (permission == null || permission.equals("")) { throw new Exception("Permission cannot be null or empty"); } if (!cordova.hasPermission(permission)) { list.add(permission); } } catch (JSONException e) { Log.w(TAG, "Invalid permission entry at index " + i, e); } } String[] res = new String[list.size()]; return list.toArray(res); } private void getAndroidVersion(CallbackContext callback) { callback.success(Build.VERSION.SDK_INT); } private void getWebkitInfo(CallbackContext callback) { PackageInfo info = null; JSONObject res = new JSONObject(); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { info = WebView.getCurrentWebViewPackage(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Class webViewFactory = Class.forName("android.webkit.WebViewFactory"); Method method = webViewFactory.getMethod("getLoadedPackageInfo"); info = (PackageInfo) method.invoke(null); } else { PackageManager packageManager = activity.getPackageManager(); try { info = packageManager.getPackageInfo("com.google.android.webview", 0); } catch (PackageManager.NameNotFoundException e) { callback.error("Package not found"); } return; } res.put("packageName", info.packageName); res.put("versionName", info.versionName); res.put("versionCode", info.versionCode); callback.success(res); } catch ( JSONException | InvocationTargetException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException e ) { callback.error( "Cannot determine current WebView engine. (" + e.getMessage() + ")" ); return; } } private void isPowerSaveMode(CallbackContext callback) { PowerManager powerManager = (PowerManager) context.getSystemService( Context.POWER_SERVICE ); boolean powerSaveMode = powerManager.isPowerSaveMode(); callback.success(powerSaveMode ? 1 : 0); } private void pinFileShortcut(JSONObject shortcutJson, CallbackContext callback) { if (shortcutJson == null) { callback.error("Invalid shortcut data"); return; } String id = shortcutJson.optString("id", ""); String label = shortcutJson.optString("label", ""); String description = shortcutJson.optString("description", label); String iconSrc = shortcutJson.optString("icon", ""); String uriString = shortcutJson.optString("uri", ""); if (id.isEmpty() || label.isEmpty() || uriString.isEmpty()) { callback.error("Missing required shortcut fields"); return; } if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { callback.error("Pin shortcut not supported on this launcher"); return; } try { Uri dataUri = Uri.parse(uriString); String packageName = context.getPackageName(); PackageManager pm = context.getPackageManager(); Intent launchIntent = pm.getLaunchIntentForPackage(packageName); if (launchIntent == null) { callback.error("Launch intent not found for package: " + packageName); return; } ComponentName componentName = launchIntent.getComponent(); if (componentName == null) { callback.error("ComponentName is null"); return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setComponent(componentName); intent.setData(dataUri); intent.putExtra("acodeFileUri", uriString); IconCompat icon; if (iconSrc != null && !iconSrc.isEmpty()) { try { Uri iconUri = Uri.parse(iconSrc); Bitmap bitmap; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // API 28+ ImageDecoder.Source source = ImageDecoder.createSource( context.getContentResolver(), iconUri ); bitmap = ImageDecoder.decodeBitmap(source); } else { // Below API 28 bitmap = MediaStore.Images.Media.getBitmap( context.getContentResolver(), iconUri ); } icon = IconCompat.createWithBitmap(bitmap); } catch (Exception e) { icon = getFileShortcutIcon(label); } } else { icon = getFileShortcutIcon(label); } ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, id) .setShortLabel(label) .setLongLabel( description != null && !description.isEmpty() ? description : label ) .setIcon(icon) .setIntent(intent) .build(); ShortcutManagerCompat.pushDynamicShortcut(context, shortcut); boolean requested = ShortcutManagerCompat.requestPinShortcut( context, shortcut, null ); if (!requested) { callback.error("Failed to request pin shortcut"); return; } callback.success(); } catch (Exception e) { callback.error(e.toString()); } } private IconCompat getFileShortcutIcon(String filename) { Bitmap fallback = createFileShortcutBitmap(filename); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return IconCompat.createWithAdaptiveBitmap(fallback); } return IconCompat.createWithBitmap(fallback); } private Bitmap createFileShortcutBitmap(String filename) { final float baseSizeDp = 72f; float sizePx = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, baseSizeDp, context.getResources().getDisplayMetrics() ); if (sizePx <= 0) { sizePx = baseSizeDp; } int size = Math.round(sizePx); Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG); int backgroundColor = pickShortcutColor(filename); paint.setColor(backgroundColor); float radius = size * 0.24f; RectF bounds = new RectF(0, 0, size, size); canvas.drawRoundRect(bounds, radius, radius, paint); paint.setColor(Color.WHITE); paint.setTextAlign(Paint.Align.CENTER); paint.setTypeface(Typeface.create("sans-serif-medium", Typeface.BOLD)); String label = getShortcutLabel(filename); float textLength = Math.max(1, label.length()); float factor = textLength > 4 ? 0.22f : textLength > 3 ? 0.26f : 0.34f; paint.setTextSize(size * factor); Paint.FontMetrics metrics = paint.getFontMetrics(); float baseline = (size - metrics.bottom - metrics.top) / 2f; canvas.drawText(label, size / 2f, baseline, paint); return bitmap; } private String getFileExtension(String filename) { if (filename == null) return ""; int dot = filename.lastIndexOf('.'); if (dot < 0 || dot == filename.length() - 1) return ""; return filename.substring(dot + 1).toLowerCase(Locale.getDefault()); } private String getShortcutLabel(String filename) { String ext = getFileExtension(filename); if (!ext.isEmpty()) { switch (ext) { case "js": case "jsx": return "JS"; case "ts": case "tsx": return "TS"; case "md": case "markdown": return "MD"; case "json": return "JSON"; case "html": case "htm": return "HTML"; case "css": return "CSS"; case "java": return "JAVA"; case "kt": case "kts": return "KOT"; case "py": return "PY"; case "rb": return "RB"; case "c": return "C"; case "cpp": case "cc": case "cxx": return "CPP"; case "h": case "hpp": return "HDR"; case "go": return "GO"; case "rs": return "RS"; case "php": return "PHP"; case "xml": return "XML"; case "yml": case "yaml": return "YML"; case "txt": return "TXT"; case "sh": case "bash": return "SH"; default: String label = ext.replaceAll("[^A-Za-z0-9]", ""); if (label.isEmpty()) label = ext; if (label.length() > 4) { label = label.substring(0, 4); } return label.toUpperCase(Locale.getDefault()); } } if (filename != null && !filename.trim().isEmpty()) { String cleaned = filename.replaceAll("[^A-Za-z0-9]", ""); if (!cleaned.isEmpty()) { if (cleaned.length() > 3) cleaned = cleaned.substring(0, 3); return cleaned.toUpperCase(Locale.getDefault()); } return filename.substring(0, 1).toUpperCase(Locale.getDefault()); } return "FILE"; } private int pickShortcutColor(String filename) { String ext = getFileExtension(filename); switch (ext) { case "js": case "jsx": return 0xFFF7DF1E; case "ts": case "tsx": return 0xFF3178C6; case "md": case "markdown": return 0xFF546E7A; case "json": return 0xFF4CAF50; case "html": case "htm": return 0xFFF4511E; case "css": return 0xFF2962FF; case "java": return 0xFFEC6F2D; case "kt": case "kts": return 0xFF7F52FF; case "py": return 0xFF306998; case "rb": return 0xFFCC342D; case "c": return 0xFF546E7A; case "cpp": case "cc": case "cxx": return 0xFF00599C; case "h": case "hpp": return 0xFF8D6E63; case "go": return 0xFF00ADD8; case "rs": return 0xFFB7410E; case "php": return 0xFF8892BF; case "xml": return 0xFF5C6BC0; case "yml": case "yaml": return 0xFF757575; case "txt": return 0xFF546E7A; case "sh": case "bash": return 0xFF388E3C; default: final int[] colors = new int[] { 0xFF1E88E5, 0xFF6D4C41, 0xFF00897B, 0xFF8E24AA, 0xFF3949AB, 0xFF039BE5, 0xFFD81B60, 0xFF43A047 }; String key = ext.isEmpty() ? (filename == null ? "file" : filename) : ext; int hash = Math.abs(key.hashCode()); return colors[hash % colors.length]; } } private void fileAction( String fileURI, String filename, String action, String mimeType, CallbackContext callback ) { Activity activity = this.activity; Context context = this.context; Uri uri = this.getContentProviderUri(fileURI, filename); if (uri == null) { callback.error("Unable to access file for action " + action); return; } try { Intent intent = new Intent(action); if (mimeType.equals("")) { mimeType = "text/plain"; } mimeType = resolveMimeType(mimeType, uri, filename); String clipLabel = null; if (filename != null && !filename.isEmpty()) { clipLabel = new File(filename).getName(); } if (clipLabel == null || clipLabel.isEmpty()) { clipLabel = uri.getLastPathSegment(); } if (clipLabel == null || clipLabel.isEmpty()) { clipLabel = "shared-file"; } if (action.equals(Intent.ACTION_SEND)) { intent.setType(mimeType); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setClipData( ClipData.newUri( context.getContentResolver(), clipLabel, uri ) ); intent.putExtra(Intent.EXTRA_STREAM, uri); intent.putExtra(Intent.EXTRA_TITLE, clipLabel); intent.putExtra(Intent.EXTRA_SUBJECT, clipLabel); if (filename != null && !filename.isEmpty()) { intent.putExtra(Intent.EXTRA_TEXT, filename); } } else { int flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION; if (action.equals(Intent.ACTION_EDIT)) { flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; } intent.setFlags(flags); intent.setDataAndType(uri, mimeType); intent.setClipData( ClipData.newUri( context.getContentResolver(), clipLabel, uri ) ); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); if (!clipLabel.equals("shared-file")) { intent.putExtra(Intent.EXTRA_TITLE, clipLabel); } if (action.equals(Intent.ACTION_EDIT)) { intent.putExtra(Intent.EXTRA_STREAM, uri); } } int permissionFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; if (action.equals(Intent.ACTION_EDIT)) { permissionFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; } grantUriPermissions(intent, uri, permissionFlags); if (action.equals(Intent.ACTION_SEND)) { Intent chooserIntent = Intent.createChooser(intent, null); chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); activity.startActivity(chooserIntent); } else if (action.equals(Intent.ACTION_EDIT) || action.equals(Intent.ACTION_VIEW)) { Intent chooserIntent = Intent.createChooser(intent, null); chooserIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); if (action.equals(Intent.ACTION_EDIT)) { chooserIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } activity.startActivity(chooserIntent); } else { activity.startActivity(intent); } callback.success(uri.toString()); } catch (Exception e) { callback.error(e.getMessage()); } } private void getAppInfo(CallbackContext callback) { JSONObject res = new JSONObject(); try { PackageManager pm = activity.getPackageManager(); PackageInfo pInfo = pm.getPackageInfo(context.getPackageName(), 0); ApplicationInfo appInfo = context.getApplicationInfo(); int isDebuggable = appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE; res.put("firstInstallTime", pInfo.firstInstallTime); res.put("lastUpdateTime", pInfo.lastUpdateTime); res.put("label", appInfo.loadLabel(pm).toString()); res.put("packageName", pInfo.packageName); res.put("versionName", pInfo.versionName); res.put("versionCode", pInfo.getLongVersionCode()); res.put("isDebuggable", isDebuggable); callback.success(res); } catch (JSONException e) { callback.error(e.getMessage()); } catch (Exception e) { callback.error(e.getMessage()); } } private void openInBrowser(String src, CallbackContext callback) { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(src)); activity.startActivity(browserIntent); } private void launchApp( String appId, String className, JSONObject extras, CallbackContext callback ) { if (appId == null || appId.equals("")) { callback.error("No package name provided."); return; } if (className == null || className.equals("")) { callback.error("No activity class name provided."); return; } try { Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.setPackage(appId); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(appId, className); if (extras != null) { Iterator keys = extras.keys(); while (keys.hasNext()) { String key = keys.next(); Object value = extras.get(key); if (value instanceof Integer) { intent.putExtra(key, (Integer) value); } else if (value instanceof Boolean) { intent.putExtra(key, (Boolean) value); } else if (value instanceof Double) { intent.putExtra(key, (Double) value); } else if (value instanceof Long) { intent.putExtra(key, (Long) value); } else if (value instanceof String) { intent.putExtra(key, (String) value); } else { intent.putExtra(key, value.toString()); } } } activity.startActivity(intent); callback.success("Launched " + appId); } catch (Exception e) { callback.error(e.toString()); } } private void addShortcut( String id, String label, String description, String iconSrc, String action, String data, CallbackContext callback ) { try { Intent intent; ImageDecoder.Source imgSrc; Bitmap bitmap; IconCompat icon; imgSrc = ImageDecoder.createSource( context.getContentResolver(), Uri.parse(iconSrc) ); bitmap = ImageDecoder.decodeBitmap(imgSrc); icon = IconCompat.createWithBitmap(bitmap); intent = activity .getPackageManager() .getLaunchIntentForPackage(activity.getPackageName()); intent.putExtra("action", action); intent.putExtra("data", data); ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, id) .setShortLabel(label) .setLongLabel(description) .setIcon(icon) .setIntent(intent) .build(); ShortcutManagerCompat.pushDynamicShortcut(context, shortcut); callback.success(); } catch (Exception e) { callback.error(e.toString()); } } private void pinShortcut(String id, CallbackContext callback) { ShortcutManager shortcutManager = context.getSystemService( ShortcutManager.class ); if (shortcutManager.isRequestPinShortcutSupported()) { ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder( context, id ).build(); Intent pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo); PendingIntent successCallback = PendingIntent.getBroadcast( context, 0, pinnedShortcutCallbackIntent, PendingIntent.FLAG_IMMUTABLE ); shortcutManager.requestPinShortcut( pinShortcutInfo, successCallback.getIntentSender() ); callback.success(); return; } callback.error("Not supported"); } private void removeShortcut(String id, CallbackContext callback) { try { List < String > list = new ArrayList < String > (); list.add(id); ShortcutManagerCompat.removeDynamicShortcuts(context, list); callback.success(); } catch (Exception e) { callback.error(e.toString()); } } private void setUiTheme( final String systemBarColor, final JSONObject scheme, final CallbackContext callback ) { try { this.systemBarColor = Color.parseColor(systemBarColor); this.theme = new Theme(scheme); preferences.set("BackgroundColor", this.systemBarColor); webView.getPluginManager().postMessage("updateSystemBars", null); applySystemBarTheme(); callback.success(); } catch (IllegalArgumentException e) { callback.error("Invalid color: " + systemBarColor); } catch (Exception e) { callback.error(e.toString()); } } private void applySystemBarTheme() { final Window window = activity.getWindow(); final View decorView = window.getDecorView(); // Keep Cordova's BackgroundColor flow for API 36+, but also apply the // window colors directly so OEM variants do not leave stale system-bar // colors behind after a theme switch. window.clearFlags(0x04000000 | 0x08000000); // FLAG_TRANSLUCENT_STATUS | FLAG_TRANSLUCENT_NAVIGATION window.addFlags(0x80000000); // FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { window.setNavigationBarContrastEnforced(false); window.setStatusBarContrastEnforced(false); } decorView.setBackgroundColor(this.systemBarColor); View rootView = activity.findViewById(android.R.id.content); if (rootView != null) { rootView.setBackgroundColor(this.systemBarColor); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { window.setStatusBarColor(this.systemBarColor); window.setNavigationBarColor(this.systemBarColor); } setStatusBarStyle(window); setNavigationBarStyle(window); } private void setStatusBarStyle(final Window window) { String themeType = theme.getType(); View decorView = window.getDecorView(); int uiOptions; int lightStatusBar; if (SDK_INT <= 30) { uiOptions = getDeprecatedSystemUiVisibility(decorView); lightStatusBar = deprecatedFlagUiLightStatusBar(); if (themeType.equals("light")) { setDeprecatedSystemUiVisibility(decorView, uiOptions | lightStatusBar); return; } setDeprecatedSystemUiVisibility(decorView, uiOptions & ~lightStatusBar); return; } uiOptions = Objects.requireNonNull(decorView.getWindowInsetsController()).getSystemBarsAppearance(); lightStatusBar = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; if (themeType.equals("light")) { decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions | lightStatusBar, lightStatusBar); return; } decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions & ~lightStatusBar, lightStatusBar); } private void setNavigationBarStyle(final Window window) { String themeType = theme.getType(); View decorView = window.getDecorView(); int uiOptions; int lightNavigationBar; if (SDK_INT <= 30) { uiOptions = getDeprecatedSystemUiVisibility(decorView); lightNavigationBar = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; if (themeType.equals("light")) { setDeprecatedSystemUiVisibility(decorView, uiOptions | lightNavigationBar); return; } setDeprecatedSystemUiVisibility(decorView, uiOptions & ~lightNavigationBar); return; } uiOptions = Objects.requireNonNull(decorView.getWindowInsetsController()).getSystemBarsAppearance(); lightNavigationBar = WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; if (themeType.equals("light")) { decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions | lightNavigationBar, lightNavigationBar); return; } decorView.getWindowInsetsController().setSystemBarsAppearance(uiOptions & ~lightNavigationBar, lightNavigationBar); } private int deprecatedFlagUiLightStatusBar() { return View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; } private int getDeprecatedSystemUiVisibility(View decorView) { return decorView.getSystemUiVisibility(); } private void setDeprecatedSystemUiVisibility(View decorView, int visibility) { decorView.setSystemUiVisibility(visibility); } private void getCordovaIntent(CallbackContext callback) { Intent intent = activity.getIntent(); callback.sendPluginResult( new PluginResult(PluginResult.Status.OK, getIntentJson(intent)) ); } private void setIntentHandler(CallbackContext callback) { intentHandler = callback; PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); callback.sendPluginResult(result); } @Override public void onNewIntent(Intent intent) { if (intentHandler != null) { PluginResult result = new PluginResult( PluginResult.Status.OK, getIntentJson(intent) ); result.setKeepCallback(true); intentHandler.sendPluginResult(result); } } private JSONObject getIntentJson(Intent intent) { JSONObject json = new JSONObject(); try { json.put("action", intent.getAction()); json.put("data", intent.getDataString()); json.put("type", intent.getType()); json.put("package", intent.getPackage()); json.put("extras", getExtrasJson(intent.getExtras())); } catch (JSONException e) { e.printStackTrace(); } return json; } private JSONObject getExtrasJson(Bundle extras) { JSONObject json = new JSONObject(); if (extras != null) { for (String key: extras.keySet()) { try { Object value = extras.get(key); if (value instanceof String) { json.put(key, (String) value); } else if (value instanceof Integer) { json.put(key, (Integer) value); } else if (value instanceof Long) { json.put(key, (Long) value); } else if (value instanceof Double) { json.put(key, (Double) value); } else if (value instanceof Float) { json.put(key, (Float) value); } else if (value instanceof Boolean) { json.put(key, (Boolean) value); } else if (value instanceof Bundle) { json.put(key, getExtrasJson((Bundle) value)); } else { json.put(key, value.toString()); } } catch (JSONException e) { e.printStackTrace(); } } } return json; } private Uri getContentProviderUri(String fileUri) { return this.getContentProviderUri(fileUri, ""); } private Uri getContentProviderUri(String fileUri, String filename) { if (fileUri == null || fileUri.isEmpty()) { return null; } Uri uri = Uri.parse(fileUri); if (uri == null) { return null; } if ("file".equalsIgnoreCase(uri.getScheme())) { File originalFile = new File(uri.getPath()); if (!originalFile.exists()) { Log.e("System", "File does not exist for URI: " + fileUri); return null; } String authority = getFileProviderAuthority(); if (authority == null) { Log.e("System", "No FileProvider authority available."); return null; } try { return FileProvider.getUriForFile(context, authority, originalFile); } catch (IllegalArgumentException | SecurityException ex) { try { File cacheCopy = ensureShareableCopy(originalFile, filename); return FileProvider.getUriForFile(context, authority, cacheCopy); } catch (Exception copyError) { Log.e("System", "Failed to expose file via FileProvider", copyError); return null; } } } return uri; } private File ensureShareableCopy(File source, String displayName) throws IOException { File cacheRoot = new File(context.getCacheDir(), "shared"); if (!cacheRoot.exists() && !cacheRoot.mkdirs()) { throw new IOException("Unable to create shared cache directory"); } if (displayName != null && !displayName.isEmpty()) { displayName = new File(displayName).getName(); } if (displayName == null || displayName.isEmpty()) { displayName = source.getName(); } if (displayName == null || displayName.isEmpty()) { displayName = "shared-file"; } File target = new File(cacheRoot, displayName); target = ensureUniqueFile(target); copyFile(source, target); return target; } private File ensureUniqueFile(File target) { if (!target.exists()) { return target; } String name = target.getName(); String prefix = name; String suffix = ""; int dotIndex = name.lastIndexOf('.'); if (dotIndex > 0) { prefix = name.substring(0, dotIndex); suffix = name.substring(dotIndex); } int index = 1; File candidate = target; while (candidate.exists()) { candidate = new File(target.getParentFile(), prefix + "-" + index + suffix); index++; } return candidate; } private void copyFile(File source, File destination) throws IOException { try ( InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination) ) { byte[] buffer = new byte[8192]; int length; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } out.flush(); } } private void grantUriPermissions(Intent intent, Uri uri, int flags) { if (uri == null) return; PackageManager pm = context.getPackageManager(); List resInfoList = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo: resInfoList) { String packageName = resolveInfo.activityInfo.packageName; context.grantUriPermission(packageName, uri, flags); } } private String resolveMimeType(String currentMime, Uri uri, String filename) { if (currentMime != null && !currentMime.isEmpty() && !currentMime.equals("*/*")) { return currentMime; } String mime = null; if (uri != null) { mime = context.getContentResolver().getType(uri); } if ((mime == null || mime.isEmpty()) && filename != null) { mime = getMimeTypeFromExtension(filename); } if ((mime == null || mime.isEmpty()) && uri != null) { String path = uri.getPath(); if (path != null) { mime = getMimeTypeFromExtension(path); } } return (mime != null && !mime.isEmpty()) ? mime : "*/*"; } private String getFileProviderAuthority() { if (fileProviderAuthority != null && !fileProviderAuthority.isEmpty()) { return fileProviderAuthority; } try { PackageManager pm = context.getPackageManager(); PackageInfo packageInfo = pm.getPackageInfo( context.getPackageName(), PackageManager.GET_PROVIDERS ); if (packageInfo.providers != null) { for (ProviderInfo providerInfo: packageInfo.providers) { if ( providerInfo != null && providerInfo.name != null && providerInfo.name.equals(FileProvider.class.getName()) ) { fileProviderAuthority = providerInfo.authority; break; } } } } catch (PackageManager.NameNotFoundException error) { Log.w(TAG, "Unable to inspect package providers for FileProvider authority.", error); } if (fileProviderAuthority == null || fileProviderAuthority.isEmpty()) { fileProviderAuthority = context.getPackageName() + ".provider"; } return fileProviderAuthority; } private boolean isPackageInstalled( String packageName, PackageManager packageManager, CallbackContext callback ) { try { packageManager.getPackageInfo(packageName, 0); return true; } catch (PackageManager.NameNotFoundException e) { return false; } } private void getGlobalSetting(String setting, CallbackContext callback) { int value = (int) Global.getFloat( context.getContentResolver(), setting, -1 ); callback.success(value); } private void clearCache(CallbackContext callback) { webView.clearCache(true); callback.success("Cache cleared"); } private void setInputType(String type) { int mode = -1; if (type.equals("NO_SUGGESTIONS")) { mode = 0; } else if (type.equals("NO_SUGGESTIONS_AGGRESSIVE")) { mode = 1; } webView.setInputType(mode); } private void setNativeContextMenuDisabled(boolean disabled) { if (webView == null) { return; } webView.setNativeContextMenuDisabled(disabled); } } ================================================ FILE: src/plugins/system/android/com/foxdebug/system/Ui.java ================================================ package com.foxdebug.system; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.util.Log; import android.util.TypedValue; import org.json.JSONObject; public class Ui { public static class Icons { public static final String LOGO = "\uE922"; public static final String TUNE = "\uE927"; public static final String EXIT = "\uE902"; public static final String REFRESH = "\uE91B"; public static final String TERMINAL = "\uE923"; public static final String NO_CACHE = "\uE901"; public static final String MORE_VERT = "\uE91A"; public static final String OPEN_IN_BROWSER = "\uE91f"; public static final String PHONE_APPLE = "\uE928"; public static final String PHONE_ANDROID = "\uE90E"; public static final String TABLET_ANDROID = "\uE90F"; public static final String TABLET_APPLE = "\uE92A"; public static final String DESKTOP = "\uE90A"; public static final String DEVICES = "\uE907"; public static final String LAPTOP = "\uE90D"; public static final String TV = "\uE929"; private static Paint paint; private static int size = 24; private static int color = Color.parseColor("#FFFFFF"); private static final String FONT_PATH = "font/icon.ttf"; public static Bitmap get( Context context, String code, int size, int color ) { if (paint == null) { paint = new Paint(); paint.setAntiAlias(true); paint.setTypeface( Typeface.createFromAsset(context.getAssets(), FONT_PATH) ); paint.setTextAlign(Paint.Align.CENTER); } paint.setTextSize(size); paint.setColor(color); float baseline = -paint.ascent(); int width = (int) paint.measureText(code, 0, code.length()); int height = (int) (baseline + paint.descent()); Bitmap bitmap = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 ); Canvas canvas = new Canvas(bitmap); canvas.drawText(code, width / 2, baseline, paint); return bitmap; } public static Bitmap get( Context context, String code, int size, String color ) { int intColor = Color.parseColor(color); return get(context, code, size, intColor); } public static Bitmap get(Context context, String code) { return get(context, code, size, color); } public static Bitmap get(Context context, String code, String color) { int intColor = Color.parseColor(color); return get(context, code, size, intColor); } public static Bitmap get(Context context, String code, int color) { return get(context, code, size, color); } public static void setSize(int size) { Icons.size = size; } public static void setColor(int color) { Icons.color = color; } } public static class Theme { private JSONObject theme; public Theme(JSONObject theme) { this.theme = theme; } public int get(String color) { return get(color, "#000000"); } public int get(String color, String fallback) { String hex = theme.optString(color, fallback); return Color.parseColor(hex); } public String getType() { return theme.optString("type", "light"); } } public static int dpToPixels(Context context, int dipValue) { int value = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, (float) dipValue, context.getResources().getDisplayMetrics() ); return value; } } ================================================ FILE: src/plugins/system/package.json ================================================ { "name": "cordova-plugin-system", "version": "1.0.3", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: src/plugins/system/plugin.xml ================================================ cordova-plugin-system Utility methods for Android. Apache 2.0 cordova,plugin,system ================================================ FILE: src/plugins/system/readme.md ================================================ # Util plugin for cordova apps Using this plugin, cordova apps can: - Enable/disable full screen - Share file - Get webview information - Send email - Clear cache ## Installation ================================================ FILE: src/plugins/system/res/android/file_provider.xml ================================================ ================================================ FILE: src/plugins/system/system.d.ts ================================================ interface Info { versionName: string; packageName: string; versionCode: number; } interface AppInfo extends Info { label: string; firstInstallTime: number; lastUpdateTime: number; } interface ShortCut { id: string; label: string; description: string; icon: string; action: string; data: string; } interface FileShortcut { id: string; label: string; description?: string; icon?: string; uri: string; } interface Intent { action: string; data: string; type: string; package: string; extras: { [key: string]: any; }; } interface RewardStatus { adFreeUntil: number; lastExpiredRewardUntil: number; isActive: boolean; remainingMs: number; redemptionsToday: number; remainingRedemptions: number; maxRedemptionsPerDay: number; maxActivePassMs: number; hasPendingExpiryNotice: boolean; expiryNoticePendingUntil: number; canRedeem: boolean; redeemDisabledReason: string; grantedDurationMs?: number; appliedDurationMs?: number; offerId?: string; } type FileAction = 'VIEW' | 'EDIT' | 'SEND' | 'RUN'; type OnFail = (err: string) => void; type OnSuccessBool = (res: boolean) => void; interface System { /** * Get information about current webview */ getWebviewInfo(onSuccess: (res: Info) => void, onFail: OnFail): void; /** * Checks if power saving mode is on * @param onSuccess * @param onFail */ isPowerSaveMode(onSuccess: OnSuccessBool, onFail: OnFail): void; /** * File action using Apps content provider * @param fileUri File uri * @param filename file name * @param action file name * @param onFail */ fileAction( fileUri: string, filename: string, action: FileAction, mimeType: string, onFail: OnFail, ): void; /** * File action using Apps content provider * @param fileUri File uri * @param filename file name * @param action file name */ fileAction( fileUri: string, filename: string, action: FileAction, mimeType: string, ): void; /** * File action using Apps content provider * @param fileUri File uri * @param action file name * @param onFail */ fileAction( fileUri: string, action: FileAction, mimeType: string, onFail: OnFail, ): void; /** * File action using Apps content provider * @param fileUri File uri * @param action file name */ fileAction(fileUri: string, action: FileAction, mimeType: string): void; /** * File action using Apps content provider * @param fileUri File uri * @param action file name */ fileAction(fileUri: string, action: FileAction, onFail: OnFail): void; /** * File action using Apps content provider * @param fileUri File uri * @param action file name */ fileAction(fileUri: string, action: FileAction): void; /** * Gets app information * @param onSuccess * @param onFail */ getAppInfo(onSuccess: (info: AppInfo) => void, onFail: OnFail): void; /** * Add shortcut to app context menu * @param shortCut * @param onSuccess * @param onFail */ addShortcut( shortCut: ShortCut, onSuccess: OnSuccessBool, onFail: OnFail, ): void; /** * Removes shortcut * @param id * @param onSuccess * @param onFail */ removeShortcut(id: string, onSuccess: OnSuccessBool, onFail: OnFail): void; /** * Pins a shortcut * @param id * @param onSuccess * @param onFail */ pinShortcut(id: string, onSuccess: OnSuccessBool, onFail: OnFail): void; /** * Pin a shortcut for a specific file to the home screen * @param shortcut Shortcut configuration * @param onSuccess * @param onFail */ pinFileShortcut( shortcut: FileShortcut, onSuccess: OnSuccessBool, onFail: OnFail, ): void; /** * Gets android version * @param onSuccess * @param onFail */ getAndroidVersion(onSuccess: (res: Number) => void, onFail: OnFail): void; /** * Open settings which lets user change app settings to manage all files * @param onSuccess * @param onFail */ manageAllFiles(onSuccess: OnSuccessBool, onFail: OnFail): void; /** * Opens settings to allow to grant the app permission manage all files on device * @param onSuccess * @param onFail */ isExternalStorageManager(onSuccess: OnSuccessBool, onFail: OnFail): void; /** * Requests user to grant the provided permissions * @param permissions constant value of the permission required @see https://developer.android.com/reference/android/Manifest.permission * @param onSuccess * @param onFail */ requestPermissions( permissions: string[], onSuccess: OnSuccessBool, onFail: OnFail, ): void; /** * Requests user to grant the provided permission * @param permission constant value of the permission required @see https://developer.android.com/reference/android/Manifest.permission * @param onSuccess * @param onFail */ requestPermission( permission: string, onSuccess: OnSuccessBool, onFail: OnFail, ): void; /** * Checks whether the app has provided permission * @param permission constant value of the permission required @see https://developer.android.com/reference/android/Manifest.permission * @param onSuccess * @param onFail */ hasPermission( permission: string, onSuccess: OnSuccessBool, onFail: OnFail, ): void; /** * Opens src in browser * @param src */ openInBrowser(src: string): void; /** * Launch an Android application activity. * * @param app Package name of the application (e.g. `com.example.app`) * @param className Fully qualified activity class name (e.g. `com.example.app.MainActivity`) * @param extras Optional key-value pairs passed as Android Intent extras * @param onSuccess Called when the activity launches successfully * @param onFail Called if launching the activity fails */ launchApp( app: string, className: string, extras?: Record, onSuccess?: OnSuccessBool, onFail?: OnFail, ): void; /** * Opens a link within the app * @param url Url to open * @param title Title of the page * @param showButtons Set to true to show buttons like console, open in browser, etc */ inAppBrowser(url: string, title: string, showButtons: boolean): void; /** * Sets the color of status bar and navigation bar * @param systemBarColor Color of status bar and navigation bar * @param theme Theme as object * @param onSuccess Callback on success * @param onFail Callback on fail */ setUiTheme( systemBarColor: string, theme: object, onSuccess: OnSuccessBool, onFail: OnFail, ): void; /** * Sets intent handler for the app * @param onSuccess * @param onFail */ setIntentHandler(onSuccess: (intent: Intent) => void, onFail: OnFail): void; /** * Gets the launch intent * @param onSuccess * @param onFail */ getCordovaIntent(onSuccess: (intent: Intent) => void, onFail: OnFail): void; getRewardStatus( onSuccess: (status: RewardStatus | string) => void, onFail: OnFail, ): void; redeemReward( offerId: string, onSuccess: (status: RewardStatus | string) => void, onFail: OnFail, ): void; /** * Enable/disable native WebView long-press context behavior. * Use this when rendering a custom editor context menu. * @param disabled * @param onSuccess * @param onFail */ setNativeContextMenuDisabled( disabled: boolean, onSuccess?: () => void, onFail?: OnFail, ): void; } interface Window{ system: System; } ================================================ FILE: src/plugins/system/utils/changeProvider.js ================================================ module.exports = { changeProvider(reset) { const fs = require('fs'); const path = require('path'); const androidManifest = path.resolve(__dirname, "../../../platforms/android/app/src/main/AndroidManifest.xml"); const configXML = path.resolve(__dirname, "../../../config.xml"); const repeatChar = (char, times) => { let res = ""; while (--times >= 0) res += char; return res; }; try { const fileData = fs.readFileSync(configXML, "utf8"); const manifest = fs.readFileSync(androidManifest, "utf8"); const ID = reset ? "com.foxdebug" : /widget id="([0-9a-zA-Z\.\-_]*)"/.exec(fileData)[1]; const newFileData = manifest.replace( /(android:authorities=")([0-9a-zA-Z\.\-_]*)(")/, `$1${reset ? "com.foxdebug" : ID}.provider$3` ); fs.writeFileSync(androidManifest, newFileData); const msg = "==== Changed provider to " + ID + ".provider ===="; console.log(""); console.log(repeatChar("=", msg.length)); console.log(msg); console.log(repeatChar("=", msg.length)); console.log(""); } catch (error) { console.error(error); process.exit(1); } } }; ================================================ FILE: src/plugins/system/utils/fixProvider.js ================================================ const { changeProvider } = require("./changeProvider"); changeProvider(); ================================================ FILE: src/plugins/system/utils/resetProvider.js ================================================ const { changeProvider } = require("./changeProvider"); changeProvider(true); ================================================ FILE: src/plugins/system/www/plugin.js ================================================ module.exports = { isManageExternalStorageDeclared: function (success, error) { cordova.exec(success, error, 'System', 'isManageExternalStorageDeclared', []); }, hasGrantedStorageManager: function (success, error) { cordova.exec(success, error, 'System', 'hasGrantedStorageManager', []); }, requestStorageManager: function (success, error) { cordova.exec(success, error, 'System', 'requestStorageManager', []); }, copyToUri: function (srcUri, destUri, fileName, success, error) { cordova.exec(success, error, 'System', 'copyToUri', [srcUri, destUri, fileName]); }, fileExists: function (path, countSymlinks, success, error) { cordova.exec(success, error, 'System', 'fileExists', [path, String(countSymlinks)]); }, createSymlink: function (target, linkPath, success, error) { cordova.exec(success, error, 'System', 'createSymlink', [target, linkPath]); }, writeText: function (path, content, success, error) { cordova.exec(success, error, 'System', 'writeText', [path, content]); }, deleteFile: function (path, success, error) { cordova.exec(success, error, 'System', 'deleteFile', [path]); }, setExec: function (path, executable, success, error) { cordova.exec(success, error, 'System', 'setExec', [path, String(executable)]); }, getNativeLibraryPath: function (success, error) { cordova.exec(success, error, 'System', 'getNativeLibraryPath', []); }, getFilesDir: function (success, error) { cordova.exec(success, error, 'System', 'getFilesDir', []); }, getRewardStatus: function (success, error) { cordova.exec(success, error, 'System', 'getRewardStatus', []); }, redeemReward: function (offerId, success, error) { cordova.exec(success, error, 'System', 'redeemReward', [offerId]); }, getParentPath: function (path, success, error) { cordova.exec(success, error, 'System', 'getParentPath', [path]); }, listChildren: function (path, success, error) { cordova.exec(success, error, 'System', 'listChildren', [path]); }, mkdirs: function (path, success, error) { cordova.exec(success, error, 'System', 'mkdirs', [path]); }, getArch: function (success, error) { cordova.exec(success, error, 'System', 'getArch', []); }, clearCache: function (success, fail) { return cordova.exec(success, fail, "System", "clearCache", []); }, getWebviewInfo: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'get-webkit-info', []); }, isPowerSaveMode: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'is-powersave-mode', []); }, fileAction: function (fileUri, filename, action, mimeType, onFail) { if (typeof action !== 'string') { onFail = action || function () { }; action = filename; filename = ''; } else if (typeof mimeType !== 'string') { onFail = mimeType || function () { }; mimeType = action; action = filename; filename = ''; } else if (typeof onFail !== 'function') { onFail = function () { }; } action = "android.intent.action." + action; cordova.exec(function () { }, onFail, 'System', 'file-action', [fileUri, filename, action, mimeType]); }, getAppInfo: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'get-app-info', []); }, addShortcut: function (shortcut, onSuccess, onFail) { var id, label, description, icon, data; id = shortcut.id; label = shortcut.label; description = shortcut.description; icon = shortcut.icon; data = shortcut.data; action = shortcut.action; cordova.exec(onSuccess, onFail, 'System', 'add-shortcut', [id, label, description, icon, action, data]); }, removeShortcut: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'remove-shortcut', [id]); }, pinShortcut: function (id, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'pin-shortcut', [id]); }, pinFileShortcut: function (shortcut, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'pin-file-shortcut', [shortcut]); }, manageAllFiles: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'manage-all-files', []); }, getAndroidVersion: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'get-android-version', []); }, isExternalStorageManager: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'is-external-storage-manager', []); }, requestPermission: function (permission, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'request-permission', [permission]); }, requestPermissions: function (permissions, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'request-permissions', [permissions]); }, hasPermission: function (permission, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'has-permission', [permission]); }, openInBrowser: function (src) { cordova.exec(null, null, 'System', 'open-in-browser', [src]); }, /** * Launch an Android application activity. * * @param {string} app - Package name of the application (e.g. `com.example.app`). * @param {string} className - Fully qualified activity class name (e.g. `com.example.app.MainActivity`). * @param {Object} [extras] - Optional key-value pairs passed as Intent extras. * @param {(message: string) => void} [onSuccess] - Callback invoked when the activity launches successfully. * @param {(error: any) => void} [onFail] - Callback invoked if launching the activity fails. * * @example * System.launchApp( * "com.example.app", * "com.example.app.MainActivity", * { * user: "example", * age: 20, * premium: true * }, * (msg) => console.log(msg), * (err) => console.error(err) * ); */ launchApp: function (app, className, extras, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'launch-app', [app, className, extras]); }, inAppBrowser: function (url, title, showButtons, disableCache) { var myInAppBrowser = { onOpenExternalBrowser: null, onError: null, }; cordova.exec(function (data) { if (typeof data !== 'string') { console.warn('System.inAppBrowser: invalid callback payload', data); return; } var separatorIndex = data.indexOf(':'); if (separatorIndex < 0) { console.warn('System.inAppBrowser: malformed callback payload', data); return; } var dataTag = data.slice(0, separatorIndex); var dataUrl = data.slice(separatorIndex + 1); if (dataTag === 'onOpenExternalBrowser') { if (typeof myInAppBrowser.onOpenExternalBrowser === 'function') { myInAppBrowser.onOpenExternalBrowser(dataUrl); } else { console.warn('System.inAppBrowser: onOpenExternalBrowser handler is not set'); } } }, function (err) { if (typeof myInAppBrowser.onError === 'function') { myInAppBrowser.onError(err); return; } console.warn('System.inAppBrowser error callback not handled', err); }, 'System', 'in-app-browser', [url, title, !!showButtons, disableCache]); return myInAppBrowser; }, setUiTheme: function (systemBarColor, theme, onSuccess, onFail) { const color = systemBarColor.toLowerCase(); if (color === '#ffffff' || color === '#ffffffff') { systemBarColor = '#fffffe'; } cordova.exec((out) => { window.statusbar.setBackgroundColor(systemBarColor); if (typeof onSuccess === "function") { onSuccess(out); } }, onFail, 'System', 'set-ui-theme', [systemBarColor, theme]); }, setIntentHandler: function (handler, onerror) { cordova.exec(handler, onerror, 'System', 'set-intent-handler', []); }, getCordovaIntent: function (onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'get-cordova-intent', []); }, setInputType: function (type, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'set-input-type', [type]); }, setNativeContextMenuDisabled: function (disabled, onSuccess, onFail) { cordova.exec( onSuccess, onFail, 'System', 'set-native-context-menu-disabled', [String(!!disabled)], ); }, getGlobalSetting: function (key, onSuccess, onFail) { cordova.exec(onSuccess, onFail, 'System', 'get-global-setting', [key]); }, /** * Compare file content with provided text in a background thread. * @param {string} fileUri - The URI of the file to read * @param {string} encoding - The character encoding to use * @param {string} currentText - The text to compare against * @returns {Promise} - Resolves to true if content differs, false if same */ compareFileText: function (fileUri, encoding, currentText) { return new Promise((resolve, reject) => { cordova.exec( function(result) { resolve(result === 1); }, reject, 'System', 'compare-file-text', [fileUri, encoding, currentText] ); }); }, /** * Compare two text strings in a background thread. * @param {string} text1 - First text to compare * @param {string} text2 - Second text to compare * @returns {Promise} - Resolves to true if texts differ, false if same */ compareTexts: function (text1, text2) { return new Promise((resolve, reject) => { cordova.exec( function(result) { resolve(result === 1); }, reject, 'System', 'compare-texts', [text1, text2] ); }); } }; ================================================ FILE: src/plugins/terminal/package.json ================================================ { "name": "com.foxdebug.acode.rk.exec.terminal", "version": "1.0.0", "description": "Terminal stuff", "cordova": { "id": "com.foxdebug.acode.rk.exec.terminal", "platforms": [ "android" ] }, "keywords": [ "ecosystem:cordova", "cordova-android" ], "author": "@RohitKushvaha01", "license": "MIT" } ================================================ FILE: src/plugins/terminal/plugin.xml ================================================ Terminal ================================================ FILE: src/plugins/terminal/scripts/init-alpine.sh ================================================ export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/share/bin:/usr/share/sbin:/usr/local/bin:/usr/local/sbin:/system/bin:/system/xbin:$PREFIX/local/bin export PS1="\[\e[38;5;46m\]\u\[\033[39m\]@localhost \[\033[39m\]\w \[\033[0m\]\\$ " export HOME=/home export TERM=xterm-256color required_packages="bash command-not-found tzdata wget" missing_packages="" for pkg in $required_packages; do if ! apk info -e "$pkg" >/dev/null 2>&1; then missing_packages="$missing_packages $pkg" fi done if [ -n "$missing_packages" ]; then echo -e "\e[34;1m[*] \e[0mInstalling important packages\e[0m" apk update && apk upgrade apk add $missing_packages if [ $? -eq 0 ]; then echo -e "\e[32;1m[+] \e[0mSuccessfully installed\e[0m" fi echo -e "\e[34m[*] \e[0mUse \e[32mapk\e[0m to install new packages\e[0m" fi if [ ! -f /linkerconfig/ld.config.txt ]; then mkdir -p /linkerconfig touch /linkerconfig/ld.config.txt fi if [ "$1" = "--installing" ]; then echo "Configuring timezone..." if [ -n "$ANDROID_TZ" ] && [ -f "/usr/share/zoneinfo/$ANDROID_TZ" ]; then ln -sf "/usr/share/zoneinfo/$ANDROID_TZ" /etc/localtime echo "$ANDROID_TZ" > /etc/timezone echo "Timezone set to: $ANDROID_TZ" else echo "Failed to detect timezone" fi mkdir -p "$PREFIX/.configured" echo "Installation completed." exit 0 fi if [ "$#" -eq 0 ]; then echo "$$" > "$PREFIX/pid" chmod +x "$PREFIX/axs" if [ ! -e "$PREFIX/alpine/etc/acode_motd" ]; then cat < "$PREFIX/alpine/etc/acode_motd" Welcome to Alpine Linux in Acode! Working with packages: - Search: apk search - Install: apk add - Uninstall: apk del - Upgrade: apk update && apk upgrade EOF fi # Create acode CLI tool if [ ! -e "$PREFIX/alpine/usr/local/bin/acode" ]; then mkdir -p "$PREFIX/alpine/usr/local/bin" cat <<'ACODE_CLI' > "$PREFIX/alpine/usr/local/bin/acode" #!/bin/bash # acode - Open files/folders in Acode editor # Uses OSC escape sequences to communicate with the Acode terminal usage() { echo "Usage: acode [file/folder...]" echo "" echo "Open files or folders in Acode editor." echo "" echo "Examples:" echo " acode file.txt # Open a file" echo " acode . # Open current folder" echo " acode ~/project # Open a folder" echo " acode -h, --help # Show this help" } get_abs_path() { local path="$1" local abs_path="" if command -v realpath >/dev/null 2>&1; then abs_path=$(realpath -- "$path" 2>/dev/null) fi if [[ -z "$abs_path" ]]; then if [[ -d "$path" ]]; then abs_path=$(cd -- "$path" 2>/dev/null && pwd -P) elif [[ -e "$path" ]]; then local dir_name file_name dir_name=$(dirname -- "$path") file_name=$(basename -- "$path") abs_path="$(cd -- "$dir_name" 2>/dev/null && pwd -P)/$file_name" elif [[ "$path" == /* ]]; then abs_path="$path" else abs_path="$PWD/$path" fi fi echo "$abs_path" } open_in_acode() { local path=$(get_abs_path "$1") local type="file" [[ -d "$path" ]] && type="folder" # Send OSC 7777 escape sequence: \e]7777;cmd;type;path\a # The terminal component will intercept and handle this printf '\e]7777;open;%s;%s\a' "$type" "$path" } if [[ $# -eq 0 ]]; then open_in_acode "." exit 0 fi for arg in "$@"; do case "$arg" in -h|--help) usage exit 0 ;; *) if [[ -e "$arg" ]]; then open_in_acode "$arg" else echo "Error: '$arg' does not exist" >&2 exit 1 fi ;; esac done ACODE_CLI chmod +x "$PREFIX/alpine/usr/local/bin/acode" fi # Create initrc if it doesn't exist #initrc runs in bash so we can use bash features if [ ! -e "$PREFIX/alpine/initrc" ]; then cat <<'EOF' > "$PREFIX/alpine/initrc" # Source rc files if they exist if [ -f "/etc/profile" ]; then source "/etc/profile" fi # Environment setup export PATH=$PATH:/bin:/sbin:/usr/bin:/usr/sbin:/usr/share/bin:/usr/share/sbin:/usr/local/bin:/usr/local/sbin export HOME=/home export TERM=xterm-256color SHELL=/bin/bash export PIP_BREAK_SYSTEM_PACKAGES=1 # Default prompt with fish-style path shortening (~/p/s/components) # To use custom prompts (Starship, Oh My Posh, etc.), just init them in ~/.bashrc: # eval "$(starship init bash)" _shorten_path() { local path="$PWD" if [[ "$HOME" != "/" && "$path" == "$HOME" ]]; then echo "~" return elif [[ "$HOME" != "/" && "$path" == "$HOME/"* ]]; then path="~${path#$HOME}" fi [[ "$path" == "~" ]] && echo "~" && return local parts result="" IFS='/' read -ra parts <<< "$path" local len=${#parts[@]} for ((i=0; i/dev/null | awk -F'-[0-9]' '{print $1}' | head -n 1) if [ -n "$pkg" ]; then echo -e "The program '$cmd' is not installed.\nInstall it by executing:\n ${green}apk add $pkg${reset}" >&2 else echo "The program '$cmd' is not installed and no package provides it." >&2 fi return 127 } EOF fi # Add PS1 only if not already present if ! grep -q 'PS1=' "$PREFIX/alpine/initrc"; then # Smart path shortening (fish-style: ~/p/s/components) echo 'PS1="\[\033[1;32m\]\u\[\033[0m\]@localhost \[\033[1;34m\]\$_PS1_PATH\[\033[0m\] \[\$([ \$_PS1_EXIT -ne 0 ] && echo \"\033[31m\")\]\$\[\033[0m\] "' >> "$PREFIX/alpine/initrc" # Simple prompt (uncomment below and comment above if you prefer full paths) # echo 'PS1="\[\033[1;32m\]\u\[\033[0m\]@localhost \[\033[1;34m\]\w\[\033[0m\] \$ "' >> "$PREFIX/alpine/initrc" fi chmod +x "$PREFIX/alpine/initrc" #actual source #everytime a terminal is started initrc will run "$PREFIX/axs" -c "bash --rcfile /initrc -i" else exec "$@" fi ================================================ FILE: src/plugins/terminal/scripts/init-sandbox.sh ================================================ export LD_LIBRARY_PATH=$PREFIX mkdir -p "$PREFIX/tmp" mkdir -p "$PREFIX/alpine/tmp" mkdir -p "$PREFIX/public" export PROOT_TMP_DIR=$PREFIX/tmp if [ "$FDROID" = "true" ]; then if [ -f "$PREFIX/libproot.so" ]; then export PROOT_LOADER="$PREFIX/libproot.so" fi if [ -f "$PREFIX/libproot32.so" ]; then export PROOT_LOADER32="$PREFIX/libproot32.so" fi export PROOT="$PREFIX/libproot-xed.so" chmod +x $PREFIX/* else if [ -f "$NATIVE_DIR/libproot.so" ]; then export PROOT_LOADER="$NATIVE_DIR/libproot.so" fi if [ -f "$NATIVE_DIR/libproot32.so" ]; then export PROOT_LOADER32="$NATIVE_DIR/libproot32.so" fi if [ -e "$PREFIX/libtalloc.so.2" ] || [ -L "$PREFIX/libtalloc.so.2" ]; then rm "$PREFIX/libtalloc.so.2" fi ln -s "$NATIVE_DIR/libtalloc.so" "$PREFIX/libtalloc.so.2" export PROOT="$NATIVE_DIR/libproot-xed.so" fi ARGS="--kill-on-exit" for system_mnt in /apex /odm /product /system /system_ext /vendor /linkerconfig/ld.config.txt /linkerconfig/com.android.art/ld.config.txt /plat_property_contexts /property_contexts; do if [ -e "$system_mnt" ]; then system_mnt=$(realpath "$system_mnt") ARGS="$ARGS -b ${system_mnt}" fi done unset system_mnt ARGS="$ARGS -b /sdcard" ARGS="$ARGS -b /storage" ARGS="$ARGS -b /dev" ARGS="$ARGS -b /data" ARGS="$ARGS -b /dev/urandom:/dev/random" ARGS="$ARGS -b /proc" ARGS="$ARGS -b /sys" ARGS="$ARGS -b $PREFIX" ARGS="$ARGS -b $PREFIX/public:/public" ARGS="$ARGS -b $PREFIX/alpine/tmp:/dev/shm" if [ -e "/proc/self/fd" ]; then ARGS="$ARGS -b /proc/self/fd:/dev/fd" fi if [ -e "/proc/self/fd/0" ]; then ARGS="$ARGS -b /proc/self/fd/0:/dev/stdin" fi if [ -e "/proc/self/fd/1" ]; then ARGS="$ARGS -b /proc/self/fd/1:/dev/stdout" fi if [ -e "/proc/self/fd/2" ]; then ARGS="$ARGS -b /proc/self/fd/2:/dev/stderr" fi ARGS="$ARGS -r $PREFIX/alpine" ARGS="$ARGS -0" ARGS="$ARGS --link2symlink" ARGS="$ARGS --sysvipc" ARGS="$ARGS -L" $PROOT $ARGS /bin/sh $PREFIX/init-alpine.sh "$@" ================================================ FILE: src/plugins/terminal/scripts/rm-wrapper.sh ================================================ #!/bin/sh unlink_recursive() { path="$1" # Try to recurse into it as a directory first for entry in "$path"/* "$path"/.[!.]* "$path"/..?*; do case "$entry" in *'*'*|*'?'*) continue ;; esac unlink_recursive "$entry" done 2>/dev/null unlink "$path" 2>/dev/null || : } for target in "$@"; do unlink_recursive "$target" done # Run busybox rm, capture stderr, and filter out the "No such file or directory" message err="$(busybox rm "$@" 2>&1 >/dev/null)" # Print only real errors printf "%s\n" "$err" | grep -v "No such file or directory" ================================================ FILE: src/plugins/terminal/src/android/AlpineDocumentProvider.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Point; import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.DocumentsProvider; import android.util.Log; import android.webkit.MimeTypeMap; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Collections; import java.util.LinkedList; import java.util.Locale; import com.foxdebug.acode.R; import com.foxdebug.acode.rk.exec.terminal.*; public class AlpineDocumentProvider extends DocumentsProvider { private static final String ALL_MIME_TYPES = "*/*"; // The default columns to return information about a root if no specific // columns are requested in a query. private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{ DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES }; // The default columns to return information about a document if no specific // columns are requested in a query. private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{ DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE }; @Override public Cursor queryRoots(String[] projection) { File BASE_DIR = new File(getContext().getFilesDir(),"public"); if (!BASE_DIR.exists()) { BASE_DIR.mkdirs(); } MatrixCursor result = new MatrixCursor( projection != null ? projection : DEFAULT_ROOT_PROJECTION ); String applicationName = getApplicationLabel(); MatrixCursor.RowBuilder row = result.newRow(); row.add(DocumentsContract.Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR)); row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR)); row.add(DocumentsContract.Root.COLUMN_SUMMARY, null); row.add( DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE | DocumentsContract.Root.FLAG_SUPPORTS_SEARCH | DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD ); row.add(DocumentsContract.Root.COLUMN_TITLE, applicationName); row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES); row.add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace()); row.add(DocumentsContract.Root.COLUMN_ICON, resolveLauncherIcon()); return result; } @Override public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { MatrixCursor result = new MatrixCursor( projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION ); includeFile(result, documentId, null); return result; } @Override public Cursor queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder ) throws FileNotFoundException { MatrixCursor result = new MatrixCursor( projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION ); File parent = getFileForDocId(parentDocumentId); File[] files = parent.listFiles(); if (files != null) { for (File file : files) { includeFile(result, null, file); } } else { Log.e("DocumentsProvider", "Unable to list files in " + parentDocumentId); } return result; } @Override public ParcelFileDescriptor openDocument( String documentId, String mode, CancellationSignal signal ) throws FileNotFoundException { File file = getFileForDocId(documentId); int accessMode = ParcelFileDescriptor.parseMode(mode); return ParcelFileDescriptor.open(file, accessMode); } @Override public AssetFileDescriptor openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal ) throws FileNotFoundException { File file = getFileForDocId(documentId); ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); return new AssetFileDescriptor(pfd, 0, file.length()); } @Override public boolean onCreate() { return true; } @Override public String createDocument( String parentDocumentId, String mimeType, String displayName ) throws FileNotFoundException { File parent = getFileForDocId(parentDocumentId); File newFile = new File(parent, displayName); int noConflictId = 2; while (newFile.exists()) { newFile = new File(parent, displayName + " (" + noConflictId++ + ")"); } try { boolean succeeded; if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { succeeded = newFile.mkdir(); } else { succeeded = newFile.createNewFile(); } if (!succeeded) { throw new FileNotFoundException("Failed to create document with id " + newFile.getAbsolutePath()); } } catch (IOException e) { throw new FileNotFoundException("Failed to create document with id " + newFile.getAbsolutePath()); } return getDocIdForFile(newFile); } @Override public void deleteDocument(String documentId) throws FileNotFoundException { File file = getFileForDocId(documentId); if (!file.delete()) { throw new FileNotFoundException("Failed to delete document with id " + documentId); } } @Override public String renameDocument(String documentId, String displayName) throws FileNotFoundException { File file = getFileForDocId(documentId); File parent = file.getParentFile(); if (parent == null) { throw new FileNotFoundException("Failed to rename root document with id " + documentId); } if (displayName == null || displayName.trim().isEmpty()) { throw new FileNotFoundException("Failed to rename document with id " + documentId); } if (displayName.equals(file.getName())) { return documentId; } if (displayName.contains(File.separator)) { throw new FileNotFoundException("Invalid display name for rename: " + displayName); } File target = new File(parent, displayName); if (target.exists()) { throw new FileNotFoundException("Target already exists: " + target.getAbsolutePath()); } if (!file.renameTo(target)) { throw new FileNotFoundException("Failed to rename document with id " + documentId); } return getDocIdForFile(target); } @Override public String getDocumentType(String documentId) throws FileNotFoundException { File file = getFileForDocId(documentId); return getMimeType(file); } @Override public Cursor querySearchDocuments( String rootId, String query, String[] projection ) throws FileNotFoundException { File BASE_DIR = new File(getContext().getFilesDir(),"public"); if (!BASE_DIR.exists()) { BASE_DIR.mkdirs(); } MatrixCursor result = new MatrixCursor( projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION ); File parent = getFileForDocId(rootId); // This example implementation searches file names for the query and doesn't rank search // results, so we can stop as soon as we find a sufficient number of matches. Other // implementations might rank results and use other data about files, rather than the file // name, to produce a match. LinkedList pending = new LinkedList<>(); pending.add(parent); final int MAX_SEARCH_RESULTS = 50; while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) { File file = pending.removeFirst(); // Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search // through the whole SD card). boolean isInsideHome; try { isInsideHome = file.getCanonicalPath().startsWith(BASE_DIR.getCanonicalPath()); } catch (IOException e) { isInsideHome = true; } if (isInsideHome) { if (file.isDirectory()) { File[] files = file.listFiles(); if (files != null) { Collections.addAll(pending, files); } } else { if (file.getName().toLowerCase(Locale.getDefault()).contains(query)) { includeFile(result, null, file); } } } } return result; } @Override public boolean isChildDocument(String parentDocumentId, String documentId) { return documentId.startsWith(parentDocumentId); } /** * Add a representation of a file to a cursor. * * @param result the cursor to modify * @param docId the document ID representing the desired file (may be null if given file) * @param file the File object representing the desired file (may be null if given docID) */ private void includeFile(MatrixCursor result, String docId, File file) throws FileNotFoundException { if (docId == null) { docId = getDocIdForFile(file); } else { file = getFileForDocId(docId); } int flags = 0; if (file.isDirectory()) { if (file.canWrite()) { flags = flags | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE; } } else if (file.canWrite()) { flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_WRITE; } File parentFile = file.getParentFile(); if (parentFile != null && parentFile.canWrite()) { flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_DELETE; flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_RENAME; } String displayName = file.getName(); String mimeType = getMimeType(file); if (mimeType.startsWith("image/")) { flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL; } MatrixCursor.RowBuilder row = result.newRow(); row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, docId); row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, displayName); row.add(DocumentsContract.Document.COLUMN_SIZE, file.length()); row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType); row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified()); row.add(DocumentsContract.Document.COLUMN_FLAGS, flags); row.add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher); } public static boolean isDocumentProviderEnabled(Context context) { ComponentName componentName = new ComponentName(context, AlpineDocumentProvider.class); int state = context.getPackageManager().getComponentEnabledSetting(componentName); return state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; } public static void setDocumentProviderEnabled(Context context, boolean enabled) { if (isDocumentProviderEnabled(context) == enabled) { return; } ComponentName componentName = new ComponentName(context, AlpineDocumentProvider.class); int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; context.getPackageManager().setComponentEnabledSetting( componentName, newState, PackageManager.DONT_KILL_APP ); } /** * Get the document id given a file. This document id must be consistent across time as other * applications may save the ID and use it to reference documents later. * * The reverse of {@link #getFileForDocId}. */ private static String getDocIdForFile(File file) { return file.getAbsolutePath(); } /** * Get the file given a document id (the reverse of {@link #getDocIdForFile}). */ private static File getFileForDocId(String docId) throws FileNotFoundException { File f = new File(docId); if (!f.exists()) { throw new FileNotFoundException(f.getAbsolutePath() + " not found"); } return f; } private static String getMimeType(File file) { if (file.isDirectory()) { return DocumentsContract.Document.MIME_TYPE_DIR; } else { String name = file.getName(); int lastDot = name.lastIndexOf('.'); if (lastDot >= 0) { String extension = name.substring(lastDot + 1).toLowerCase(Locale.getDefault()); String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); if (mime != null) { return mime; } } return "application/octet-stream"; } } private int resolveLauncherIcon() { Context context = getContext(); if (context == null) return android.R.mipmap.sym_def_app_icon; int icon = context.getResources().getIdentifier("ic_launcher", "mipmap", context.getPackageName()); return icon != 0 ? icon : android.R.mipmap.sym_def_app_icon; } private String getApplicationLabel() { Context context = getContext(); if (context == null) return "Acode"; PackageManager pm = context.getPackageManager(); try { CharSequence label = pm.getApplicationLabel(context.getApplicationInfo()); return label != null ? label.toString() : "Acode"; } catch (Exception ignored) { return "Acode"; } } } ================================================ FILE: src/plugins/terminal/src/android/BackgroundExecutor.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import org.apache.cordova.*; import org.json.*; import java.io.*; import java.util.*; import java.util.concurrent.*; import com.foxdebug.acode.rk.exec.terminal.*; public class BackgroundExecutor extends CordovaPlugin { private final Map processes = new ConcurrentHashMap<>(); private final Map processInputs = new ConcurrentHashMap<>(); private final Map processCallbacks = new ConcurrentHashMap<>(); private ProcessManager processManager; @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.processManager = new ProcessManager(cordova.getContext()); } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { switch (action) { case "start": String pid = UUID.randomUUID().toString(); startProcess(pid, args.getString(0), args.getString(1).equals("true"), callbackContext); return true; case "write": writeToProcess(args.getString(0), args.getString(1), callbackContext); return true; case "stop": stopProcess(args.getString(0), callbackContext); return true; case "exec": exec(args.getString(0), args.getString(1).equals("true"), callbackContext); return true; case "isRunning": isProcessRunning(args.getString(0), callbackContext); return true; case "loadLibrary": loadLibrary(args.getString(0), callbackContext); return true; default: callbackContext.error("Unknown action: " + action); return false; } } private void exec(String cmd, boolean useAlpine, CallbackContext callbackContext) { cordova.getThreadPool().execute(() -> { try { ProcessManager.ExecResult result = processManager.executeCommand(cmd, useAlpine); if (result.isSuccess()) { callbackContext.success(result.stdout); } else { callbackContext.error(result.getErrorMessage()); } } catch (Exception e) { callbackContext.error("Exception: " + e.getMessage()); } }); } private void startProcess(String pid, String cmd, boolean useAlpine, CallbackContext callbackContext) { cordova.getThreadPool().execute(() -> { try { ProcessBuilder builder = processManager.createProcessBuilder(cmd, useAlpine); Process process = builder.start(); processes.put(pid, process); processInputs.put(pid, process.getOutputStream()); processCallbacks.put(pid, callbackContext); sendPluginResult(callbackContext, pid, true); // Stream stdout new Thread(() -> StreamHandler.streamOutput( process.getInputStream(), line -> sendPluginMessage(pid, "stdout:" + line) )).start(); // Stream stderr new Thread(() -> StreamHandler.streamOutput( process.getErrorStream(), line -> sendPluginMessage(pid, "stderr:" + line) )).start(); int exitCode = process.waitFor(); sendPluginMessage(pid, "exit:" + exitCode); cleanup(pid); } catch (Exception e) { callbackContext.error("Failed to start process: " + e.getMessage()); } }); } private void writeToProcess(String pid, String input, CallbackContext callbackContext) { try { OutputStream os = processInputs.get(pid); if (os != null) { StreamHandler.writeToStream(os, input); callbackContext.success("Written to process"); } else { callbackContext.error("Process not found or closed"); } } catch (IOException e) { callbackContext.error("Write error: " + e.getMessage()); } } private void stopProcess(String pid, CallbackContext callbackContext) { Process process = processes.get(pid); if (process != null) { ProcessUtils.killProcessTree(process); cleanup(pid); callbackContext.success("Process terminated"); } else { callbackContext.error("No such process"); } } private void isProcessRunning(String pid, CallbackContext callbackContext) { Process process = processes.get(pid); if (process != null) { String status = ProcessUtils.isAlive(process) ? "running" : "exited"; if (status.equals("exited")) cleanup(pid); callbackContext.success(status); } else { callbackContext.success("not_found"); } } private void loadLibrary(String path, CallbackContext callbackContext) { try { System.load(path); callbackContext.success("Library loaded successfully."); } catch (Exception e) { callbackContext.error("Failed to load library: " + e.getMessage()); } } private void sendPluginResult(CallbackContext ctx, String message, boolean keepCallback) { PluginResult result = new PluginResult(PluginResult.Status.OK, message); result.setKeepCallback(keepCallback); ctx.sendPluginResult(result); } private void sendPluginMessage(String pid, String message) { CallbackContext ctx = processCallbacks.get(pid); if (ctx != null) { sendPluginResult(ctx, message, true); } } private void cleanup(String pid) { processes.remove(pid); processInputs.remove(pid); processCallbacks.remove(pid); } } ================================================ FILE: src/plugins/terminal/src/android/Executor.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import org.apache.cordova.*; import org.json.*; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.util.Log; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import android.Manifest; import android.content.pm.PackageManager; import android.os.Build; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.app.Activity; import com.foxdebug.acode.rk.exec.terminal.*; import java.net.ServerSocket; public class Executor extends CordovaPlugin { private Messenger serviceMessenger; private boolean isServiceBound; private boolean isServiceBinding; // Track if binding is in progress private Context context; private Activity activity; private final Messenger handlerMessenger = new Messenger(new IncomingHandler()); private CountDownLatch serviceConnectedLatch; private final java.util.Map callbackContextMap = new java.util.concurrent.ConcurrentHashMap<>(); private static final int REQUEST_POST_NOTIFICATIONS = 1001; private void askNotificationPermission(Activity context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission( context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { } else if (ActivityCompat.shouldShowRequestPermissionRationale( context, Manifest.permission.POST_NOTIFICATIONS)) { ActivityCompat.requestPermissions( context, new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_POST_NOTIFICATIONS ); } else { ActivityCompat.requestPermissions( context, new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_POST_NOTIFICATIONS ); } } } @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.context = cordova.getContext(); this.activity = cordova.getActivity(); askNotificationPermission(activity); // Don't bind service immediately - wait until needed Log.d("Executor", "Plugin initialized - service will be started when needed"); } /** * Ensure service is bound and ready for communication * Returns true if service is ready, false if binding failed */ private boolean ensureServiceBound(CallbackContext callbackContext) { // If already bound, return immediately if (isServiceBound && serviceMessenger != null) { return true; } // If binding is already in progress, wait for it if (isServiceBinding) { try { if (serviceConnectedLatch != null && serviceConnectedLatch.await(10, TimeUnit.SECONDS)) { return isServiceBound; } else { callbackContext.error("Service binding timeout"); return false; } } catch (InterruptedException e) { callbackContext.error("Service binding interrupted: " + e.getMessage()); return false; } } // Start binding process Log.d("Executor", "Starting service binding..."); return bindServiceNow(callbackContext); } /** * Immediately bind to service */ private boolean bindServiceNow(CallbackContext callbackContext) { if (isServiceBinding) { return false; // Already binding } isServiceBinding = true; serviceConnectedLatch = new CountDownLatch(1); Intent intent = new Intent(context, TerminalService.class); // Start the service first context.startService(intent); // Then bind to it boolean bindResult = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); if (!bindResult) { Log.e("Executor", "Failed to bind to service"); isServiceBinding = false; callbackContext.error("Failed to bind to service"); return false; } // Wait for connection try { if (serviceConnectedLatch.await(10, TimeUnit.SECONDS)) { Log.d("Executor", "Service bound successfully"); return isServiceBound; } else { Log.e("Executor", "Service binding timeout"); callbackContext.error("Service binding timeout"); isServiceBinding = false; return false; } } catch (InterruptedException e) { Log.e("Executor", "Service binding interrupted: " + e.getMessage()); callbackContext.error("Service binding interrupted: " + e.getMessage()); isServiceBinding = false; return false; } } private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.d("Executor", "Service connected"); serviceMessenger = new Messenger(service); isServiceBound = true; isServiceBinding = false; if (serviceConnectedLatch != null) { serviceConnectedLatch.countDown(); } } @Override public void onServiceDisconnected(ComponentName name) { Log.w("Executor", "Service disconnected"); serviceMessenger = null; isServiceBound = false; isServiceBinding = false; serviceConnectedLatch = new CountDownLatch(1); } }; private class IncomingHandler extends Handler { @Override public void handleMessage(Message msg) { Bundle bundle = msg.getData(); String id = bundle.getString("id"); String action = bundle.getString("action"); String data = bundle.getString("data"); if (action.equals("exec_result")) { CallbackContext callbackContext = getCallbackContext(id); if (callbackContext != null) { if (bundle.getBoolean("isSuccess", false)) { callbackContext.success(data); } else { callbackContext.error(data); } cleanupCallback(id); } } else { String pid = id; CallbackContext callbackContext = getCallbackContext(pid); if (callbackContext != null) { switch (action) { case "stdout": case "stderr": PluginResult result = new PluginResult(PluginResult.Status.OK, action + ":" + data); result.setKeepCallback(true); callbackContext.sendPluginResult(result); break; case "exit": cleanupCallback(pid); callbackContext.success("exit:" + data); break; case "isRunning": callbackContext.success(data); cleanupCallback(pid); break; } } } } } @Override public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { // For actions that don't need the service, handle them directly if (action.equals("loadLibrary")) { try { System.load(args.getString(0)); callbackContext.success("Library loaded successfully."); } catch (Exception e) { callbackContext.error("Failed to load library: " + e.getMessage()); } return true; } if (action.equals("stopService")) { stopServiceNow(); callbackContext.success("Service stopped"); return true; } if (action.equals("moveToBackground")) { Intent intent = new Intent(context, TerminalService.class); intent.setAction(TerminalService.MOVE_TO_BACKGROUND); context.startService(intent); callbackContext.success("Service moved to background mode"); return true; } if (action.equals("moveToForeground")) { Intent intent = new Intent(context, TerminalService.class); intent.setAction(TerminalService.MOVE_TO_FOREGROUND); context.startService(intent); callbackContext.success("Service moved to foreground mode"); return true; } if (action.equals("spawn")) { try { JSONArray cmdArr = args.getJSONArray(0); String[] cmd = new String[cmdArr.length()]; for (int i = 0; i < cmdArr.length(); i++) { cmd[i] = cmdArr.getString(i); } int port; try (ServerSocket socket = new ServerSocket(0)) { port = socket.getLocalPort(); } ProcessServer server = new ProcessServer(port, cmd); server.startAndAwait(); // blocks until onStart() fires — server is listening before port is returned callbackContext.success(port); } catch (Exception e) { e.printStackTrace(); callbackContext.error("Failed to spawn process: " + e.getMessage()); } return true; } // For all other actions, ensure service is bound first if (!ensureServiceBound(callbackContext)) { // Error already sent by ensureServiceBound return false; } switch (action) { case "start": String cmdStart = args.getString(0); String pid = UUID.randomUUID().toString(); callbackContextMap.put(pid, callbackContext); startProcess(pid, cmdStart, args.getString(1)); return true; case "write": String pidWrite = args.getString(0); String input = args.getString(1); writeToProcess(pidWrite, input, callbackContext); return true; case "stop": String pidStop = args.getString(0); stopProcess(pidStop, callbackContext); return true; case "exec": String execId = UUID.randomUUID().toString(); callbackContextMap.put(execId, callbackContext); exec(execId, args.getString(0), args.getString(1)); return true; case "isRunning": String pidCheck = args.getString(0); callbackContextMap.put(pidCheck, callbackContext); isProcessRunning(pidCheck); return true; default: callbackContext.error("Unknown action: " + action); return false; } } private void stopServiceNow() { if (isServiceBound) { try { context.unbindService(serviceConnection); Log.d("Executor", "Service unbound"); } catch (IllegalArgumentException ignored) { // already unbound } isServiceBound = false; } isServiceBinding = false; Intent intent = new Intent(context, TerminalService.class); boolean stopped = context.stopService(intent); Log.d("Executor", "Service stop result: " + stopped); serviceMessenger = null; if (serviceConnectedLatch == null) { serviceConnectedLatch = new CountDownLatch(1); } } private void startProcess(String pid, String cmd, String alpine) { CallbackContext callbackContext = getCallbackContext(pid); if (callbackContext != null) { PluginResult result = new PluginResult(PluginResult.Status.OK, pid); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } Message msg = Message.obtain(null, TerminalService.MSG_START_PROCESS); msg.replyTo = handlerMessenger; Bundle bundle = new Bundle(); bundle.putString("id", pid); bundle.putString("cmd", cmd); bundle.putString("alpine", alpine); msg.setData(bundle); try { serviceMessenger.send(msg); } catch (RemoteException e) { CallbackContext errorContext = getCallbackContext(pid); if (errorContext != null) { errorContext.error("Failed to start process: " + e.getMessage()); cleanupCallback(pid); } } } private void exec(String execId, String cmd, String alpine) { Message msg = Message.obtain(null, TerminalService.MSG_EXEC); msg.replyTo = handlerMessenger; Bundle bundle = new Bundle(); bundle.putString("id", execId); bundle.putString("cmd", cmd); bundle.putString("alpine", alpine); msg.setData(bundle); try { serviceMessenger.send(msg); } catch (RemoteException e) { CallbackContext callbackContext = getCallbackContext(execId); if (callbackContext != null) { callbackContext.error("Failed to execute command: " + e.getMessage()); cleanupCallback(execId); } } } private void writeToProcess(String pid, String input, CallbackContext callbackContext) { Message msg = Message.obtain(null, TerminalService.MSG_WRITE_TO_PROCESS); Bundle bundle = new Bundle(); bundle.putString("id", pid); bundle.putString("input", input); msg.setData(bundle); try { serviceMessenger.send(msg); callbackContext.success("Written to process"); } catch (RemoteException e) { callbackContext.error("Write error: " + e.getMessage()); } } private void stopProcess(String pid, CallbackContext callbackContext) { Message msg = Message.obtain(null, TerminalService.MSG_STOP_PROCESS); Bundle bundle = new Bundle(); bundle.putString("id", pid); msg.setData(bundle); try { serviceMessenger.send(msg); callbackContext.success("Process terminated"); } catch (RemoteException e) { callbackContext.error("Stop error: " + e.getMessage()); } } private void isProcessRunning(String pid) { Message msg = Message.obtain(null, TerminalService.MSG_IS_RUNNING); msg.replyTo = handlerMessenger; Bundle bundle = new Bundle(); bundle.putString("id", pid); msg.setData(bundle); try { serviceMessenger.send(msg); } catch (RemoteException e) { CallbackContext callbackContext = getCallbackContext(pid); if (callbackContext != null) { callbackContext.error("Check running error: " + e.getMessage()); cleanupCallback(pid); } } } private CallbackContext getCallbackContext(String id) { return callbackContextMap.get(id); } private void cleanupCallback(String id) { callbackContextMap.remove(id); } @Override public void onDestroy() { super.onDestroy(); } } ================================================ FILE: src/plugins/terminal/src/android/ProcessManager.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import android.content.Context; import android.content.pm.PackageManager; import java.io.*; import java.util.Map; import java.util.TimeZone; import com.foxdebug.acode.rk.exec.terminal.*; public class ProcessManager { private final Context context; public ProcessManager(Context context) { this.context = context; } /** * Creates a ProcessBuilder with common environment setup */ public ProcessBuilder createProcessBuilder(String cmd, boolean useAlpine) { String xcmd = useAlpine ? "source $PREFIX/init-sandbox.sh " + cmd : cmd; ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd); setupEnvironment(builder.environment()); return builder; } /** * Sets up common environment variables */ private void setupEnvironment(Map env) { env.put("PREFIX", context.getFilesDir().getAbsolutePath()); env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir); TimeZone tz = TimeZone.getDefault(); env.put("ANDROID_TZ", tz.getID()); try { int target = context.getPackageManager() .getPackageInfo(context.getPackageName(), 0) .applicationInfo.targetSdkVersion; env.put("FDROID", String.valueOf(target <= 28)); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } } /** * Reads all output from a stream */ public static String readStream(InputStream stream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } return output.toString(); } /** * Executes a command and returns the result */ public ExecResult executeCommand(String cmd, boolean useAlpine) throws Exception { ProcessBuilder builder = createProcessBuilder(cmd, useAlpine); Process process = builder.start(); String stdout = readStream(process.getInputStream()); String stderr = readStream(process.getErrorStream()); int exitCode = process.waitFor(); return new ExecResult(exitCode, stdout.trim(), stderr.trim()); } /** * Result container for command execution */ public static class ExecResult { public final int exitCode; public final String stdout; public final String stderr; public ExecResult(int exitCode, String stdout, String stderr) { this.exitCode = exitCode; this.stdout = stdout; this.stderr = stderr; } public boolean isSuccess() { return exitCode == 0; } public String getErrorMessage() { if (!stderr.isEmpty()) { return stderr; } return "Command exited with code: " + exitCode; } } } ================================================ FILE: src/plugins/terminal/src/android/ProcessServer.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; class ProcessServer extends WebSocketServer { private final String[] cmd; private final CountDownLatch readyLatch = new CountDownLatch(1); private final AtomicReference startError = new AtomicReference<>(); private static final class ConnState { final Process process; final OutputStream stdin; ConnState(Process process, OutputStream stdin) { this.process = process; this.stdin = stdin; } } ProcessServer(int port, String[] cmd) { super(new InetSocketAddress("127.0.0.1", port)); this.cmd = cmd; } void startAndAwait() throws Exception { start(); readyLatch.await(); Exception err = startError.get(); if (err != null) throw err; } @Override public void onStart() { readyLatch.countDown(); } @Override public void onError(WebSocket conn, Exception ex) { if (conn == null) { // Bind/startup failure — unblock startAndAwait() so it can throw. startError.set(ex); readyLatch.countDown(); } // Per-connection errors: do nothing. onClose fires immediately after // for the same connection, which is the single place cleanup happens. } @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { try { Process process = new ProcessBuilder(cmd).redirectErrorStream(true).start(); InputStream stdout = process.getInputStream(); OutputStream stdin = process.getOutputStream(); conn.setAttachment(new ConnState(process, stdin)); new Thread(() -> { try { byte[] buf = new byte[8192]; int len; while ((len = stdout.read(buf)) != -1) { conn.send(ByteBuffer.wrap(buf, 0, len)); } } catch (Exception ignored) {} conn.close(1000, "process exited"); }).start(); } catch (Exception e) { conn.close(1011, "Failed to start process: " + e.getMessage()); } } @Override public void onMessage(WebSocket conn, ByteBuffer msg) { try { ConnState state = conn.getAttachment(); state.stdin.write(msg.array(), msg.position(), msg.remaining()); state.stdin.flush(); } catch (Exception ignored) {} } @Override public void onMessage(WebSocket conn, String message) { try { ConnState state = conn.getAttachment(); state.stdin.write(message.getBytes(StandardCharsets.UTF_8)); state.stdin.flush(); } catch (Exception ignored) {} } @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { try { ConnState state = conn.getAttachment(); if (state != null) state.process.destroy(); } catch (Exception ignored) {} // stop() calls w.join() on every worker thread. If called directly from // onClose (which runs on a WebSocketWorker thread), it deadlocks waiting // for itself to finish. A separate thread sidesteps that entirely. new Thread(() -> { try { stop(); } catch (Exception ignored) {} }).start(); } } ================================================ FILE: src/plugins/terminal/src/android/ProcessUtils.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import java.lang.reflect.Field; import android.util.Log; import com.foxdebug.acode.rk.exec.terminal.*; public class ProcessUtils { /** * Gets the PID of a process using reflection */ public static long getPid(Process process) { try { Field f = process.getClass().getDeclaredField("pid"); f.setAccessible(true); return f.getLong(process); } catch (Exception e) { return -1; } } /** * Checks if a process is still alive */ public static boolean isAlive(Process process) { try { process.exitValue(); return false; } catch(IllegalThreadStateException e) { return true; } } /** * Forcefully kills a process and its children */ public static void killProcessTree(Process process) { try { long pid = getPid(process); if (pid > 0) { Runtime.getRuntime().exec("kill -9 -" + pid); } } catch (Exception error) { Log.w("ProcessUtils", "Failed to kill process tree.", error); } process.destroy(); } } ================================================ FILE: src/plugins/terminal/src/android/StreamHandler.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import java.io.*; import com.foxdebug.acode.rk.exec.terminal.*; public class StreamHandler { public interface OutputListener { void onLine(String line); } /** * Streams output from an InputStream to a listener */ public static void streamOutput(InputStream inputStream, OutputListener listener) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = reader.readLine()) != null) { listener.onLine(line); } } catch (IOException ignored) { } } /** * Writes input to an OutputStream */ public static void writeToStream(OutputStream outputStream, String input) throws IOException { outputStream.write((input + "\n").getBytes()); outputStream.flush(); } } ================================================ FILE: src/plugins/terminal/src/android/TerminalService.java ================================================ package com.foxdebug.acode.rk.exec.terminal; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.PowerManager; import android.os.RemoteException; import androidx.core.app.NotificationCompat; import java.io.IOException; import java.io.OutputStream; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import com.foxdebug.acode.rk.exec.terminal.*; public class TerminalService extends Service { public static final int MSG_START_PROCESS = 1; public static final int MSG_WRITE_TO_PROCESS = 2; public static final int MSG_STOP_PROCESS = 3; public static final int MSG_IS_RUNNING = 4; public static final int MSG_EXEC = 5; public static final String CHANNEL_ID = "terminal_exec_channel"; public static final String ACTION_EXIT_SERVICE = "com.foxdebug.acode.ACTION_EXIT_SERVICE"; public static final String MOVE_TO_BACKGROUND = "com.foxdebug.acode.MOVE_TO_BACKGROUND"; public static final String MOVE_TO_FOREGROUND = "com.foxdebug.acode.MOVE_TO_FOREGROUND"; public static final String ACTION_TOGGLE_WAKE_LOCK = "com.foxdebug.acode.ACTION_TOGGLE_WAKE_LOCK"; public static boolean Default_Foreground = true; private final Map processes = new ConcurrentHashMap<>(); private final Map processInputs = new ConcurrentHashMap<>(); private final Map clientMessengers = new ConcurrentHashMap<>(); private final java.util.concurrent.ExecutorService threadPool = Executors.newCachedThreadPool(); private final Messenger serviceMessenger = new Messenger(new ServiceHandler()); private PowerManager.WakeLock wakeLock; private boolean isWakeLockHeld = false; private ProcessManager processManager; @Override public void onCreate() { super.onCreate(); processManager = new ProcessManager(this); if(Default_Foreground){ createNotificationChannel(); updateNotification(); } } @Override public IBinder onBind(Intent intent) { return serviceMessenger.getBinder(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { String action = intent.getAction(); if (ACTION_EXIT_SERVICE.equals(action)) { stopForeground(true); stopSelf(); return START_NOT_STICKY; } else if (ACTION_TOGGLE_WAKE_LOCK.equals(action)) { toggleWakeLock(); } else if(MOVE_TO_BACKGROUND.equals(action)){ Default_Foreground = false; stopForeground(true); } else if(MOVE_TO_FOREGROUND.equals(action)){ Default_Foreground = true; createNotificationChannel(); updateNotification(); } } return START_STICKY; } private class ServiceHandler extends Handler { @Override public void handleMessage(Message msg) { Bundle bundle = msg.getData(); String id = bundle.getString("id"); Messenger clientMessenger = msg.replyTo; switch (msg.what) { case MSG_START_PROCESS: String cmd = bundle.getString("cmd"); String alpine = bundle.getString("alpine"); clientMessengers.put(id, clientMessenger); startProcess(id, cmd, "true".equals(alpine)); break; case MSG_WRITE_TO_PROCESS: String input = bundle.getString("input"); writeToProcess(id, input); break; case MSG_STOP_PROCESS: stopProcess(id); break; case MSG_IS_RUNNING: isProcessRunning(id, clientMessenger); break; case MSG_EXEC: String execCmd = bundle.getString("cmd"); String execAlpine = bundle.getString("alpine"); clientMessengers.put(id, clientMessenger); exec(id, execCmd, "true".equals(execAlpine)); break; } } } private void toggleWakeLock() { if (isWakeLockHeld) { releaseWakeLock(); } else { acquireWakeLock(); } updateNotification(); } private void acquireWakeLock() { if (wakeLock == null) { PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AcodeTerminal:WakeLock"); } if (!isWakeLockHeld) { wakeLock.acquire(); isWakeLockHeld = true; } } private void releaseWakeLock() { if (wakeLock != null && isWakeLockHeld) { wakeLock.release(); isWakeLockHeld = false; } } private void startProcess(String pid, String cmd, boolean useAlpine) { threadPool.execute(() -> { try { ProcessBuilder builder = processManager.createProcessBuilder(cmd, useAlpine); Process process = builder.start(); processes.put(pid, process); processInputs.put(pid, process.getOutputStream()); // Stream stdout threadPool.execute(() -> StreamHandler.streamOutput(process.getInputStream(), line -> sendMessageToClient(pid, "stdout", line)) ); // Stream stderr threadPool.execute(() -> StreamHandler.streamOutput(process.getErrorStream(), line -> sendMessageToClient(pid, "stderr", line)) ); // Wait for process completion threadPool.execute(() -> { try { int exitCode = process.waitFor(); sendMessageToClient(pid, "exit", String.valueOf(exitCode)); cleanup(pid); } catch (InterruptedException e) { e.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); sendMessageToClient(pid, "stderr", "Failed to start process: " + e.getMessage()); sendMessageToClient(pid, "exit", "1"); cleanup(pid); } }); } private void exec(String execId, String cmd, boolean useAlpine) { threadPool.execute(() -> { try { ProcessManager.ExecResult result = processManager.executeCommand(cmd, useAlpine); if (result.isSuccess()) { sendExecResultToClient(execId, true, result.stdout); } else { sendExecResultToClient(execId, false, result.getErrorMessage()); } cleanup(execId); } catch (Exception e) { sendExecResultToClient(execId, false, "Exception: " + e.getMessage()); cleanup(execId); } }); } private void sendMessageToClient(String id, String action, String data) { Messenger clientMessenger = clientMessengers.get(id); if (clientMessenger != null) { try { Message msg = Message.obtain(); Bundle bundle = new Bundle(); bundle.putString("id", id); bundle.putString("action", action); bundle.putString("data", data); msg.setData(bundle); clientMessenger.send(msg); } catch (RemoteException e) { cleanup(id); } } } private void sendExecResultToClient(String id, boolean isSuccess, String data) { Messenger clientMessenger = clientMessengers.get(id); if (clientMessenger != null) { try { Message msg = Message.obtain(); Bundle bundle = new Bundle(); bundle.putString("id", id); bundle.putString("action", "exec_result"); bundle.putString("data", data); bundle.putBoolean("isSuccess", isSuccess); msg.setData(bundle); clientMessenger.send(msg); } catch (RemoteException e) { cleanup(id); } } } private void writeToProcess(String pid, String input) { try { OutputStream os = processInputs.get(pid); if (os != null) { StreamHandler.writeToStream(os, input); } } catch (IOException e) { e.printStackTrace(); } } private void stopProcess(String pid) { Process process = processes.get(pid); if (process != null) { ProcessUtils.killProcessTree(process); cleanup(pid); } } private void isProcessRunning(String pid, Messenger clientMessenger) { boolean running = processes.containsKey(pid) && ProcessUtils.isAlive(processes.get(pid)); try { Message reply = Message.obtain(); Bundle bundle = new Bundle(); bundle.putString("id", pid); bundle.putString("action", "isRunning"); bundle.putString("data", running ? "running" : "stopped"); reply.setData(bundle); clientMessenger.send(reply); } catch (RemoteException e) { // nothing else to do } } private void cleanup(String id) { processes.remove(id); processInputs.remove(id); clientMessengers.remove(id); } private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel serviceChannel = new NotificationChannel( CHANNEL_ID, "Terminal Executor Channel", NotificationManager.IMPORTANCE_LOW ); NotificationManager manager = getSystemService(NotificationManager.class); if (manager != null) { manager.createNotificationChannel(serviceChannel); } } } private void updateNotification() { Intent exitIntent = new Intent(this, TerminalService.class); exitIntent.setAction(ACTION_EXIT_SERVICE); PendingIntent exitPendingIntent = PendingIntent.getService(this, 0, exitIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); Intent wakeLockIntent = new Intent(this, TerminalService.class); wakeLockIntent.setAction(ACTION_TOGGLE_WAKE_LOCK); PendingIntent wakeLockPendingIntent = PendingIntent.getService(this, 1, wakeLockIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); String contentText = "Executor service" + (isWakeLockHeld ? " (wakelock held)" : ""); String wakeLockButtonText = isWakeLockHeld ? "Release Wake Lock" : "Acquire Wake Lock"; int notificationIcon = resolveDrawableId("ic_notification", "ic_launcher_foreground", "ic_launcher"); Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Acode Service") .setContentText(contentText) .setSmallIcon(notificationIcon) .setOngoing(true) .addAction(notificationIcon, wakeLockButtonText, wakeLockPendingIntent) .addAction(notificationIcon, "Exit", exitPendingIntent) .build(); startForeground(1, notification); } @Override public void onDestroy() { stopForeground(true); super.onDestroy(); releaseWakeLock(); for (Process process : processes.values()) { ProcessUtils.killProcessTree(process); } processes.clear(); processInputs.clear(); clientMessengers.clear(); threadPool.shutdown(); } private int resolveDrawableId(String... names) { for (String name : names) { int id = getResources().getIdentifier(name, "drawable", getPackageName()); if (id != 0) return id; } return android.R.drawable.sym_def_app_icon; } } ================================================ FILE: src/plugins/terminal/www/Executor.js ================================================ /** * @class Executor * @description * This class provides an interface to run shell commands from a Cordova app. * It supports real-time process streaming, writing input to running processes, * stopping them, and executing one-time commands. */ const exec = require('cordova/exec'); class Executor { constructor(BackgroundExecutor = false) { this.ExecutorType = BackgroundExecutor ? "BackgroundExecutor" : "Executor"; } /** * Spawns a process and exposes it as a raw WebSocket stream. * * @param {string[]} cmd - Command and arguments to execute (e.g. `["sh", "-c", "echo hi"]`). * @param {(ws: WebSocket) => void} callback - Called with the connected WebSocket once the * process is ready. Use `ws.send()` to write to stdin and `ws.onmessage` to read stdout. */ spawnStream(cmd, callback, onError) { exec((port) => { const ws = new WebSocket(`ws://127.0.0.1:${port}`); ws.binaryType = "arraybuffer"; ws.onopen = () => { callback(ws); }; ws.onerror = (e) => { if (onError) onError(e); }; }, (err) => { if (onError) onError(err); }, "Executor", "spawn", [cmd]); } /** * Starts a shell process and enables real-time streaming of stdout, stderr, and exit status. * * @param {string} command - The shell command to run (e.g., `"sh"`, `"ls -al"`). * @param {(type: 'stdout' | 'stderr' | 'exit', data: string) => void} onData - Callback that receives real-time output: * - `"stdout"`: Standard output line. * - `"stderr"`: Standard error line. * - `"exit"`: Exit code of the process. * @param {boolean} [alpine=false] - Whether to run the command inside the Alpine sandbox environment (`true`) or on Android directly (`false`). * @returns {Promise} Resolves with a unique process ID (UUID) used for future references like `write()` or `stop()`. * * @example * const executor = new Executor(); * executor.start('sh', (type, data) => { * //console.log(`[${type}] ${data}`); * }).then(uuid => { * executor.write(uuid, 'echo Hello World'); * executor.stop(uuid); * }); */ start(command, onData, alpine = false) { return new Promise((resolve, reject) => { let first = true; exec( async (message) => { //console.log(message); if (first) { first = false; await new Promise(resolve => setTimeout(resolve, 100)); // First message is always the process UUID resolve(message); } else { const match = message.match(/^([^:]+):(.*)$/); if (match) { const prefix = match[1]; // e.g. "stdout" const content = match[2]; // output onData(prefix, content); } else { onData("unknown", message); } } }, reject, this.ExecutorType, "start", [command, String(alpine)] ); }); } /** * Sends input to a running process's stdin. * * @param {string} uuid - The process ID returned by {@link Executor#start}. * @param {string} input - Input string to send (e.g., shell commands). * @returns {Promise} Resolves once the input is written. * * @example * executor.write(uuid, 'ls /sdcard'); */ write(uuid, input) { //console.log("write: " + input + " to " + uuid); return new Promise((resolve, reject) => { exec(resolve, reject, this.ExecutorType, "write", [uuid, input]); }); } /** * Moves the executor service to the background (stops foreground notification). * * @returns {Promise} Resolves when the service is moved to background. * * @example * executor.moveToBackground(); */ moveToBackground() { return new Promise((resolve, reject) => { exec(resolve, reject, this.ExecutorType, "moveToBackground", []); }); } /** * Moves the executor service to the foreground (shows notification). * * @returns {Promise} Resolves when the service is moved to foreground. * * @example * executor.moveToForeground(); */ moveToForeground() { return new Promise((resolve, reject) => { exec(resolve, reject, this.ExecutorType, "moveToForeground", []); }); } /** * Terminates a running process. * * @param {string} uuid - The process ID returned by {@link Executor#start}. * @returns {Promise} Resolves when the process has been stopped. * * @example * executor.stop(uuid); */ stop(uuid) { return new Promise((resolve, reject) => { exec(resolve, reject, this.ExecutorType, "stop", [uuid]); }); } /** * Checks if a process is still running. * * @param {string} uuid - The process ID returned by {@link Executor#start}. * @returns {Promise} Resolves `true` if the process is running, `false` otherwise. * * @example * const isAlive = await executor.isRunning(uuid); */ isRunning(uuid) { return new Promise((resolve, reject) => { exec( (result) => { resolve(result === "running"); }, reject, this.ExecutorType, "isRunning", [uuid] ); }); } /** * Stops the executor service completely. * * @returns {Promise} Resolves when the service has been stopped. * * Note: This does not gurantee that all running processes have been killed, but the service will no longer be active. Use with caution. * * @example * executor.stopService(); */ stopService() { return new Promise((resolve, reject) => { exec(resolve, reject, this.ExecutorType, "stopService", []); }); } /** * Executes a shell command once and waits for it to finish. * Unlike {@link Executor#start}, this does not stream output. * * @param {string} command - The shell command to execute. * @param {boolean} [alpine=false] - Whether to run the command in the Alpine sandbox (`true`) or Android environment (`false`). * @returns {Promise} Resolves with standard output on success, rejects with an error or standard error on failure. * * @example * executor.execute('ls -l') * .then(//console.log) * .catch(console.error); */ execute(command, alpine = false) { return new Promise((resolve, reject) => { exec(resolve, reject, this.ExecutorType, "exec", [command, String(alpine)]); }); } /** * Loads a native library from the specified path. * * @param {string} path - The path to the native library to load. * @returns {Promise} Resolves when the library has been loaded. * * @example * executor.loadLibrary('/path/to/library.so'); */ loadLibrary(path) { return new Promise((resolve, reject) => { exec(resolve, reject, this.ExecutorType, "loadLibrary", [path]); }); } } //backward compatibility const executorInstance = new Executor(); executorInstance.BackgroundExecutor = new Executor(true); module.exports = executorInstance; ================================================ FILE: src/plugins/terminal/www/Terminal.js ================================================ const Executor = require("./Executor"); const Terminal = { /** * Starts the AXS environment by writing init scripts and executing the sandbox. * @param {boolean} [installing=false] - Whether AXS is being started during installation. * @param {Function} [logger=console.log] - Function to log standard output. * @param {Function} [err_logger=console.error] - Function to log errors. * @returns {Promise} - Returns true if installation completes with exit code 0, void if not installing */ async startAxs(installing = false, logger = console.log, err_logger = console.error) { const filesDir = await new Promise((resolve, reject) => { system.getFilesDir(resolve, reject); }); if (installing) { return new Promise((resolve, reject) => { readAsset("init-alpine.sh", async (content) => { system.writeText(`${filesDir}/init-alpine.sh`, content, logger, err_logger); }); readAsset("rm-wrapper.sh", async (content) => { system.deleteFile(`${filesDir}/alpine/bin/rm`, logger, err_logger); system.writeText(`${filesDir}/alpine/bin/rm`, content, logger, err_logger); system.setExec(`${filesDir}/alpine/bin/rm`, true, logger, err_logger); }); readAsset("init-sandbox.sh", (content) => { system.writeText(`${filesDir}/init-sandbox.sh`, content, logger, err_logger); Executor.start("sh", (type, data) => { logger(`${type} ${data}`); // Check for exit code during installation if (type === "exit") { resolve(data === "0"); } }).then(async (uuid) => { await Executor.write(uuid, `source ${filesDir}/init-sandbox.sh ${installing ? "--installing" : ""}; exit`); }).catch((error) => { err_logger("Failed to start AXS:", error); resolve(false); }); }); }); } else { readAsset("rm-wrapper.sh", async (content) => { system.deleteFile(`${filesDir}/alpine/bin/rm`, logger, err_logger); system.writeText(`${filesDir}/alpine/bin/rm`, content, logger, err_logger); system.setExec(`${filesDir}/alpine/bin/rm`, true, logger, err_logger); }); readAsset("init-alpine.sh", async (content) => { system.writeText(`${filesDir}/init-alpine.sh`, content, logger, err_logger); }); readAsset("init-sandbox.sh", (content) => { system.writeText(`${filesDir}/init-sandbox.sh`, content, logger, err_logger); Executor.start("sh", (type, data) => { logger(`${type} ${data}`); }).then(async (uuid) => { await Executor.write(uuid, `source ${filesDir}/init-sandbox.sh ${installing ? "--installing" : ""}; exit`); }); }); } }, /** * Stops the AXS process by forcefully killing it. * @returns {Promise} */ async stopAxs() { await Executor.execute(`kill -KILL $(cat $PREFIX/pid)`); }, /** * Checks if the AXS process is currently running. * @returns {Promise} - `true` if AXS is running, `false` otherwise. */ async isAxsRunning() { const filesDir = await new Promise((resolve, reject) => { system.getFilesDir(resolve, reject); }); const pidExists = await new Promise((resolve, reject) => { system.fileExists(`${filesDir}/pid`, false, (result) => { resolve(result == 1); }, reject); }); if (!pidExists) return false; const result = await Executor.BackgroundExecutor.execute(`kill -0 $(cat $PREFIX/pid) 2>/dev/null && echo "true" || echo "false"`); return String(result).toLowerCase() === "true"; }, /** * Installs Alpine by downloading binaries and extracting the root filesystem. * Also sets up additional dependencies for F-Droid variant. * @param {Function} [logger=console.log] - Function to log standard output. * @param {Function} [err_logger=console.error] - Function to log errors. * @returns {Promise} - Returns true if installation completes with exit code 0 */ async install(logger = console.log, err_logger = console.error) { if (!(await this.isSupported())) return false; try { //cleanup before insatll await this.uninstall(); } catch (e) { //supress error } const filesDir = await new Promise((resolve, reject) => { system.getFilesDir(resolve, reject); }); const arch = await new Promise((resolve, reject) => { system.getArch(resolve, reject); }); try { let alpineUrl; let axsUrl; let prootUrl; let libTalloc; let libproot = null; let libproot32 = null; if (arch === "arm64-v8a") { libproot = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/arm64/libproot.so"; libproot32 = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/arm64/libproot32.so"; libTalloc = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/arm64/libtalloc.so"; prootUrl = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/arm64/libproot-xed.so"; axsUrl = `https://github.com/bajrangCoder/acodex_server/releases/latest/download/axs-musl-android-arm64`; alpineUrl = "https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/aarch64/alpine-minirootfs-3.21.0-aarch64.tar.gz"; } else if (arch === "armeabi-v7a") { libproot = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/arm32/libproot.so"; libTalloc = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/arm32/libtalloc.so"; prootUrl = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/arm32/libproot-xed.so"; axsUrl = `https://github.com/bajrangCoder/acodex_server/releases/latest/download/axs-musl-android-armv7`; alpineUrl = "https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/armhf/alpine-minirootfs-3.21.0-armhf.tar.gz"; } else if (arch === "x86_64") { libproot = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/x64/libproot.so"; libproot32 = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/x64/libproot32.so"; libTalloc = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/x64/libtalloc.so"; prootUrl = "https://raw.githubusercontent.com/Acode-Foundation/Acode/main/src/plugins/proot/libs/x64/libproot-xed.so"; axsUrl = `https://github.com/bajrangCoder/acodex_server/releases/latest/download/axs-musl-android-x86_64`; alpineUrl = "https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/x86_64/alpine-minirootfs-3.21.0-x86_64.tar.gz"; } else { throw new Error(`Unsupported architecture: ${arch}`); } logger("⬇️ Downloading sandbox filesystem..."); await new Promise((resolve, reject) => { cordova.plugin.http.downloadFile( alpineUrl, {}, {}, cordova.file.dataDirectory + "alpine.tar.gz", resolve, reject ); }); logger("⬇️ Downloading axs..."); await new Promise((resolve, reject) => { cordova.plugin.http.downloadFile( axsUrl, {}, {}, cordova.file.dataDirectory + "axs", resolve, reject ); }); const isFdroid = await Executor.execute("echo $FDROID"); if (isFdroid === "true") { logger("🐧 F-Droid flavor detected, downloading additional files..."); logger("⬇️ Downloading compatibility layer..."); await new Promise((resolve, reject) => { cordova.plugin.http.downloadFile( prootUrl, {}, {}, cordova.file.dataDirectory + "libproot-xed.so", resolve, reject ); }); logger("⬇️ Downloading supporting library..."); await new Promise((resolve, reject) => { cordova.plugin.http.downloadFile( libTalloc, {}, {}, cordova.file.dataDirectory + "libtalloc.so.2", resolve, reject ); }); if (libproot != null) { await new Promise((resolve, reject) => { cordova.plugin.http.downloadFile( libproot, {}, {}, cordova.file.dataDirectory + "libproot.so", resolve, reject ); }); } if (libproot32 != null) { await new Promise((resolve, reject) => { cordova.plugin.http.downloadFile( libproot32, {}, {}, cordova.file.dataDirectory + "libproot32.so", resolve, reject ); }); } } logger("✅ All downloads completed"); logger("📁 Setting up directories..."); await new Promise((resolve, reject) => { system.mkdirs(`${filesDir}/.downloaded`, resolve, reject); }); const alpineDir = `${filesDir}/alpine`; await new Promise((resolve, reject) => { system.mkdirs(alpineDir, resolve, reject); }); logger("📦 Extracting sandbox filesystem..."); await Executor.execute(`tar --no-same-owner -xf ${filesDir}/alpine.tar.gz -C ${alpineDir}`); logger("⚙️ Applying basic configuration..."); system.writeText(`${alpineDir}/etc/resolv.conf`, `nameserver 8.8.4.4 \nnameserver 8.8.8.8`); readAsset("rm-wrapper.sh", async (content) => { system.deleteFile(`${alpineDir}/bin/rm`, logger, err_logger); system.writeText(`${alpineDir}/bin/rm`, content, logger, err_logger); system.setExec(`${alpineDir}/bin/rm`, true, logger, err_logger); }); logger("✅ Extraction complete"); await new Promise((resolve, reject) => { system.mkdirs(`${filesDir}/.extracted`, resolve, reject); }); logger("⚙️ Updating sandbox enviroment..."); const installResult = await this.startAxs(true, logger, err_logger); return installResult; } catch (e) { err_logger("Installation failed:", e); console.error("Installation failed:", e); return false; } }, /** * Checks if alpine is already installed. * @returns {Promise} - Returns true if all required files and directories exist. */ isInstalled() { return new Promise(async (resolve, reject) => { const filesDir = await new Promise((resolve, reject) => { system.getFilesDir(resolve, reject); }); const alpineExists = await new Promise((resolve, reject) => { system.fileExists(`${filesDir}/alpine`, false, (result) => { resolve(result == 1); }, reject); }); const downloaded = alpineExists && await new Promise((resolve, reject) => { system.fileExists(`${filesDir}/.downloaded`, false, (result) => { resolve(result == 1); }, reject); }); const extracted = alpineExists && await new Promise((resolve, reject) => { system.fileExists(`${filesDir}/.extracted`, false, (result) => { resolve(result == 1); }, reject); }); const configured = alpineExists && await new Promise((resolve, reject) => { system.fileExists(`${filesDir}/.configured`, false, (result) => { resolve(result == 1); }, reject); }); resolve(alpineExists && downloaded && extracted && configured); }); }, /** * Checks if the current device architecture is supported. * @returns {Promise} - `true` if architecture is supported, otherwise `false`. */ isSupported() { return new Promise((resolve, reject) => { system.getArch((arch) => { resolve(["arm64-v8a", "armeabi-v7a", "x86_64"].includes(arch)); }, reject); }); }, /** * Creates a backup of the Alpine Linux installation * @async * @function backup * @description Creates a compressed tar archive of the Alpine installation * @returns {Promise} Promise that resolves to the file URI of the created backup file (aterm_backup.tar) * @throws {string} Rejects with "Alpine is not installed." if Alpine is not currently installed * @throws {string} Rejects with command output if backup creation fails * @example * try { * const backupPath = await backup(); * console.log(`Backup created at: ${backupPath}`); * } catch (error) { * console.error(`Backup failed: ${error}`); * } */ backup() { return new Promise(async (resolve, reject) => { if (!await this.isInstalled()) { reject("Alpine is not installed."); return; } const cmd = ` set -e INCLUDE_FILES="alpine .downloaded .extracted .configured axs" if [ "$FDROID" = "true" ]; then INCLUDE_FILES="$INCLUDE_FILES libtalloc.so.2 libproot-xed.so" fi EXCLUDE="--exclude=alpine/data --exclude=alpine/system --exclude=alpine/vendor --exclude=alpine/sdcard --exclude=alpine/storage --exclude=alpine/public --exclude=alpine/apex --exclude=alpine/odm --exclude=alpine/product --exclude=alpine/system_ext --exclude=alpine/linkerconfig --exclude=alpine/proc --exclude=alpine/sys --exclude=alpine/dev --exclude=alpine/run --exclude=alpine/tmp" tar -cf "$PREFIX/aterm_backup.tar" -C "$PREFIX" $EXCLUDE $INCLUDE_FILES echo "ok" `; const result = await Executor.execute(cmd); if (result === "ok") { resolve(cordova.file.dataDirectory + "aterm_backup.tar"); } else { reject(result); } }); }, /** * Restores Alpine Linux installation from a backup file * @async * @function restore * @description Restores the Alpine installation from a previously created backup file (aterm_backup.tar). * This function stops any running Alpine processes, removes existing installation files, and extracts * the backup to restore the previous state. The backup file must exist in the expected location. * @returns {Promise} Promise that resolves to "ok" when restoration completes successfully * @throws {string} Rejects with "Backup File does not exist" if aterm_backup.tar is not found * @throws {string} Rejects with command output if restoration fails * @example * try { * await restore(); * console.log("Alpine installation restored successfully"); * } catch (error) { * console.error(`Restore failed: ${error}`); * } */ restore() { return new Promise(async (resolve, reject) => { if (await this.isAxsRunning()) { await this.stopAxs(); } const cmd = ` set -e INCLUDE_FILES="$PREFIX/alpine $PREFIX/.downloaded $PREFIX/.extracted $PREFIX/.configured $PREFIX/axs" if [ "$FDROID" = "true" ]; then INCLUDE_FILES="$INCLUDE_FILES $PREFIX/libtalloc.so.2 $PREFIX/libproot-xed.so" fi for item in $INCLUDE_FILES; do rm -rf -- "$item" done tar -xf $PREFIX/aterm_backup.* -C "$PREFIX" echo "ok" `; const result = await Executor.execute(cmd); if (result === "ok") { resolve(result); } else { reject(result); } }); }, /** * Uninstalls the Alpine Linux installation * @async * @function uninstall * @description Completely removes the Alpine Linux installation from the device by deleting all * Alpine-related files and directories. This function stops any running Alpine processes before * removal. NOTE: This does not perform cleanup of $PREFIX * @returns {Promise} Promise that resolves to "ok" when uninstallation completes successfully * @throws {string} Rejects with command output if uninstallation fails * @example * try { * await uninstall(); * console.log("Alpine installation removed successfully"); * } catch (error) { * console.error(`Uninstall failed: ${error}`); * } */ uninstall() { return new Promise(async (resolve, reject) => { if (await this.isAxsRunning()) { await this.stopAxs(); } const cmd = ` set -e INCLUDE_FILES="$PREFIX/alpine $PREFIX/.downloaded $PREFIX/.extracted $PREFIX/.configured $PREFIX/axs" if [ "$FDROID" = "true" ]; then INCLUDE_FILES="$INCLUDE_FILES $PREFIX/libtalloc.so.2 $PREFIX/libproot-xed.so" fi for item in $INCLUDE_FILES; do rm -rf -- "$item" done echo "ok" `; const result = await Executor.execute(cmd); if (result === "ok") { resolve(result); } else { reject(result); } }); } }; function readAsset(assetPath, callback) { const assetUrl = "file:///android_asset/" + assetPath; window.resolveLocalFileSystemURL(assetUrl, fileEntry => { fileEntry.file(file => { const reader = new FileReader(); reader.onloadend = () => callback(reader.result); reader.readAsText(file); }, console.error); }, console.error); } module.exports = Terminal; ================================================ FILE: src/plugins/websocket/README.md ================================================ # Cordova Plugin: OkHttp WebSocket A Cordova plugin that uses [OkHttp](https://square.github.io/okhttp/) to provide WebSocket support in your Cordova app. It aims to mimic the [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) in JavaScript, with additional features. ## Features * ✅ WebSocket API-like interface * ✅ Event support: `onopen`, `onmessage`, `onerror`, `onclose` * ✅ `extensions` and `readyState` properties * ✅ `listClients()` to list active connections * ✅ Support for protocols * ✅ Support for Custom Headers. * ✅ Compatible with Cordova for Android --- ## Usage ### Import ```javascript const WebSocketPlugin = cordova.websocket; ``` ### Connect to WebSocket ```javascript WebSocketPlugin.connect("wss://example.com/socket", ["protocol1", "protocol2"], headers) .then(ws => { ws.onopen = (e) => console.log("Connected!", e); ws.onmessage = (e) => console.log("Message:", e.data); ws.onerror = (e) => console.error("Error:", e); ws.onclose = (e) => console.log("Closed:", e); ws.send("Hello from Cordova!"); ws.close(); }) .catch(err => console.error("WebSocket connection failed:", err)); ``` --- ## API Reference ### Methods * `WebSocketPlugin.connect(url, protocols, headers, binaryType)` * Connects to a WebSocket server. * `url`: The WebSocket server URL. * `protocols`: (Optional) An array of subprotocol strings. * `headers`: (Optional) Custom headers as key-value pairs. * `binaryType`: (Optional) Initial binary type setting. * **Returns:** A Promise that resolves to a `WebSocketInstance`. * `WebSocketPlugin.listClients()` * Lists all stored webSocket instance IDs. * **Returns:** `Promise` that resolves to an array of `instanceId` strings. * `WebSocketPlugin.send(instanceId, message, binary)` * Sends a message to the server using an instance ID. * `instanceId`: The ID of the WebSocket instance. * `message`: The message to send (string or ArrayBuffer/ArrayBufferView). * `binary`: (Optional) Whether to send the message as binary, accepts `boolean` * **Returns:** `Promise` that resolves when the message is sent. * `WebSocketPlugin.close(instanceId, code, reason)` * same as `WebSocketInstance.close(code, reason)` but needs `instanceId`. * **Returns:** `Promise` that resolves. ### WebSocketInstance Methods * `WebSocketInstance.send(message, binary)` * Sends a message to the server. * `message`: The message to send (string or ArrayBuffer/ArrayBufferView). * `binary`: (Optional) Whether to send the message as binary. accepts `boolean` * Throws an error if the connection is not open. * `WebSocketInstance.close(code, reason)` * Closes the connection. * `code`: (Optional) If unspecified, a close code for the connection is automatically set: to 1000 for a normal closure, or otherwise to [another standard value in the range 1001-1015](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1) that indicates the actual reason the connection was closed. * `reason`: A string providing a [custom WebSocket connection close reason](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6) (a concise human-readable prose explanation for the closure). The value must be no longer than 123 bytes (encoded in UTF-8). --- ### Properties of `WebSocketInstance` * `onopen`: Event listener for connection open. * `onmessage`: Event listener for messages received. * `onclose`: Event listener for connection close. * `onerror`: Event listener for errors. * `readyState`: (number) The state of the connection. * 0 (`CONNECTING`): Socket created, not yet open. * 1 (`OPEN`): Connection is open and ready. * 2 (`CLOSING`): Connection is closing. * 3 (`CLOSED`): Connection is closed or couldn't be opened. * `extensions`: (string) Extensions negotiated by the server. * `binaryType`: (string) Type of binary data to use ('arraybuffer' or '' (binary payload returned as strings.)). * `url`: (string) The WebSocket server URL. * `instanceId`: (string) Unique identifier for this WebSocket instance. ### Event Handling `WebSocketInstance` extends `EventTarget`, providing standard event handling methods: * `addEventListener(type, listener)`: Registers an event listener. * `removeEventListener(type, listener)`: Removes an event listener. * `dispatchEvent(event)`: Dispatches an event to the object. Example of using event listeners: ```javascript const ws = await WebSocketPlugin.connect("wss://example.com/socket"); // Using on* properties ws.onmessage = (event) => console.log("Message:", event.data); // Using addEventListener ws.addEventListener('message', (event) => console.log("Message:", event.data)); ``` ### Constants * `WebSocketInstance.CONNECTING`: 0 * `WebSocketInstance.OPEN`: 1 * `WebSocketInstance.CLOSING`: 2 * `WebSocketInstance.CLOSED`: 3 --- ## Notes * Only supported on Android (via OkHttp). * Make sure to handle connection lifecycle properly (close sockets when done). * `listClients()` is useful for debugging and management. --- ================================================ FILE: src/plugins/websocket/package.json ================================================ { "name": "cordova-plugin-websocket", "version": "0.0.1", "description": "This cordova plugin is created to use WebSocket (client) in web/js.", "cordova": { "id": "cordova-plugin-websocket", "platforms": [ "android" ] }, "keywords": [ "cordova", "websocket", "cordova-android", "ws" ], "author": "Acode-Foundation (created by UnschooledGamer)", "license": "Apache-2.0", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" } } ================================================ FILE: src/plugins/websocket/plugin.xml ================================================ cordova-plugin-websocket Cordova Websocket MIT cordova,ws,WebSocket ================================================ FILE: src/plugins/websocket/src/android/WebSocketInstance.java ================================================ package com.foxdebug.websocket; import android.util.Base64; import android.util.Log; import androidx.annotation.NonNull; import org.apache.cordova.*; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Iterator; import java.util.concurrent.TimeUnit; import okhttp3.*; import okio.ByteString; public class WebSocketInstance extends WebSocketListener { private static final String TAG = "WebSocketInstance"; private static final int DEFAULT_CLOSE_CODE = 1000; private static final String DEFAULT_CLOSE_REASON = "Normal closure"; private WebSocket webSocket; private CallbackContext callbackContext; private final CordovaInterface cordova; private final String instanceId; private String extensions = ""; private String protocol = ""; private String binaryType = ""; private int readyState = 0; // CONNECTING // okHttpMainClient parameter is used. To have a single main client(singleton), with per-websocket configuration using newBuilder method. public WebSocketInstance(String url, JSONArray protocols, JSONObject headers, String binaryType, OkHttpClient okHttpMainClient, CordovaInterface cordova, String instanceId) { this.cordova = cordova; this.instanceId = instanceId; this.binaryType = binaryType; OkHttpClient client = okHttpMainClient.newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .build(); Request.Builder requestBuilder = new Request.Builder().url(url); // custom headers support. if (headers != null) { Iterator keys = headers.keys(); while (keys.hasNext()) { String key = keys.next(); String value = headers.optString(key); requestBuilder.addHeader(key, value); } } // adds Sec-WebSocket-Protocol header if protocols is present. if (protocols != null) { StringBuilder protocolHeader = new StringBuilder(); for (int i = 0; i < protocols.length(); i++) { protocolHeader.append(protocols.optString(i)).append(","); } if (protocolHeader.length() > 0) { protocolHeader.setLength(protocolHeader.length() - 1); requestBuilder.addHeader("Sec-WebSocket-Protocol", protocolHeader.toString()); } } client.newWebSocket(requestBuilder.build(), this); } public void setCallback(CallbackContext callbackContext) { this.callbackContext = callbackContext; PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } public void send(String message, boolean isBinary) { if (this.webSocket != null) { Log.d(TAG, "websocket instanceId=" + this.instanceId + " received send(..., isBinary=" + isBinary + ") action call, sending message=" + message); if(isBinary) { this.sendBinary(message); return; } this.webSocket.send(message); } else { Log.d(TAG, "websocket instanceId=" + this.instanceId + " received send(..., isBinary=" + isBinary + ") ignoring... as webSocket is null (not present/connected)"); } } /** * Sends bytes as the data of a binary (type 0x2) message. * @param base64Data Binary Data received from JS bridge encoded as base64 String */ private void sendBinary(String base64Data) { byte[] data = Base64.decode(base64Data, Base64.DEFAULT); this.webSocket.send(ByteString.of(data)); } public String close(int code, String reason) { if (this.webSocket != null) { this.readyState = 2; // CLOSING try { boolean result = this.webSocket.close(code, reason); Log.d(TAG, "websocket instanceId=" + this.instanceId + " received close() action call, code=" + code + " reason=" + reason + " close method result: " + result); // if a graceful shutdown was already underway... // or if the web socket is already closed or canceled. do nothing. if(!result) { return null; } } catch (Exception e) { return e.getMessage(); } return null; } else { Log.d(TAG, "websocket instanceId=" + this.instanceId + " received close() action call, ignoring... as webSocket is null (not present)"); // TODO: finding a better way of telling it wasn't successful. return ""; } } public String close() { Log.d(TAG, "WebSocket instanceId=" + this.instanceId + " close() called with no arguments. Using defaults."); // Calls the more specific version with default values return close(DEFAULT_CLOSE_CODE, DEFAULT_CLOSE_REASON); } @Override public void onOpen(@NonNull WebSocket webSocket, Response response) { this.webSocket = webSocket; this.readyState = 1; // OPEN this.extensions = response.headers("Sec-WebSocket-Extensions").toString(); this.protocol = response.header("Sec-WebSocket-Protocol"); Log.i(TAG, "websocket instanceId=" + this.instanceId + " Opened" + "received extensions=" + this.extensions); sendEvent("open", null, false, false); } @Override public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) { Log.d(TAG, "websocket instanceId=" + this.instanceId + " Received message: " + text); sendEvent("message", text, false, false); } // This is called when the Websocket server sends a binary(type 0x2) message. @Override public void onMessage(@NonNull WebSocket webSocket, @NonNull ByteString bytes) { Log.d(TAG, "websocket instanceId=" + this.instanceId + " Received message(bytes/binary payload): " + bytes.toString()); try { if ("arraybuffer".equals(this.binaryType)) { String base64 = bytes.base64(); sendEvent("message", base64, true, false); } else { sendEvent("message", bytes.utf8(), true, true); } } catch (Exception e) { Log.e(TAG, "Error sending message", e); } } @Override public void onClosing(@NonNull WebSocket webSocket, int code, @NonNull String reason) { this.readyState = 2; // CLOSING Log.i(TAG, "websocket instanceId=" + this.instanceId + " is Closing code: " + code + " reason: " + reason); this.webSocket.close(code, reason); } @Override public void onClosed(@NonNull WebSocket webSocket, int code, @NonNull String reason) { this.readyState = 3; // CLOSED Log.i(TAG, "websocket instanceId=" + this.instanceId + " Closed code: " + code + " reason: " + reason); JSONObject closedEvent = new JSONObject(); try { closedEvent.put("code", code); closedEvent.put("reason", reason); } catch (JSONException e) { Log.e(TAG, "Error creating close event", e); } sendEvent("close", closedEvent.toString(), false, false); // remove instance after receiving close. WebSocketPlugin.removeInstance(this.instanceId); } @Override public void onFailure(@NonNull WebSocket webSocket, Throwable t, Response response) { this.readyState = 3; // CLOSED sendEvent("error", t.getMessage(), false, false); Log.e(TAG, "websocket instanceId=" + this.instanceId + " Error: " + t.getMessage()); } public void setBinaryType(String binaryType) { this.binaryType = binaryType; } private void sendEvent(String type, String data, boolean isBinary, boolean parseAsText) { if (callbackContext != null) { try { JSONObject event = new JSONObject(); event.put("type", type); event.put("extensions", this.extensions); event.put("readyState", this.readyState); event.put("isBinary", isBinary); event.put("parseAsText", parseAsText); if (data != null) event.put("data", data); Log.d(TAG, "sending event: " + type + " eventObj " + event.toString()); PluginResult result = new PluginResult(PluginResult.Status.OK, event); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } catch (Exception e) { Log.e(TAG, "Error sending event", e); } } } } ================================================ FILE: src/plugins/websocket/src/android/WebSocketPlugin.java ================================================ package com.foxdebug.websocket; import android.util.Log; import org.apache.cordova.*; import org.json.*; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import okhttp3.OkHttpClient; // TODO: plugin init & plugin destroy(closing okhttp clients) lifecycles. (✅) public class WebSocketPlugin extends CordovaPlugin { private static final ConcurrentHashMap instances = new ConcurrentHashMap<>(); public OkHttpClient okHttpMainClient = null; @Override protected void pluginInitialize() { this.okHttpMainClient = new OkHttpClient(); } @Override public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { cordova.getThreadPool().execute(new Runnable() { @Override public void run() { switch (action) { case "connect": String url = args.optString(0); JSONArray protocols = args.optJSONArray(1); JSONObject headers = args.optJSONObject(2); String binaryType = args.optString(3, null); String id = UUID.randomUUID().toString(); WebSocketInstance instance = new WebSocketInstance(url, protocols, headers, binaryType, okHttpMainClient, cordova, id); instances.put(id, instance); callbackContext.success(id); return; case "send": String instanceId = args.optString(0); String message = args.optString(1); boolean isBinary = args.optBoolean(2, false); WebSocketInstance inst = instances.get(instanceId); Log.d("WebSocketPlugin", "send called"); if (inst != null) { inst.send(message, isBinary); callbackContext.success(); } else { callbackContext.error("Invalid instance ID"); } return; case "close": instanceId = args.optString(0); // defaults code to 1000 & reason to "Normal closure" int code = args.optInt(1, 1000); String reason = args.optString(2, "Normal closure"); inst = instances.get(instanceId); if (inst != null) { String error = inst.close(code, reason); if(error == null) { callbackContext.success(); return; } else if(!error.isEmpty()) { // if error is empty means the websocket is not ready/open. callbackContext.error(error); return; } } else { callbackContext.error("Invalid instance ID"); } return; case "registerListener": instanceId = args.optString(0); inst = instances.get(instanceId); if (inst != null) { inst.setCallback(callbackContext); } else { callbackContext.error("Invalid instance ID"); } return; case "setBinaryType": instanceId = args.optString(0); String type = args.optString(1); inst = instances.get(instanceId); if (inst != null) { inst.setBinaryType(type); } else { Log.d("WebSocketPlugin", "setBinaryType called for instanceId=" + instanceId + " but It's not found. ignoring...."); } return; case "listClients": JSONArray clientIds = new JSONArray(); for (String clientId : instances.keySet()) { clientIds.put(clientId); } callbackContext.success(clientIds); return; default: return; } } }); return true; } @Override public void onDestroy() { // clear all. for (WebSocketInstance instance : instances.values()) { // Closing them gracefully. instance.close(); } instances.clear(); okHttpMainClient.dispatcher().executorService().shutdown(); Log.i("WebSocketPlugin", "cleaned up... on destroy"); } public static void removeInstance(String instanceId) { instances.remove(instanceId); } } ================================================ FILE: src/plugins/websocket/www/websocket.js ================================================ var exec = require('cordova/exec'); /** * Whether to log debug messages */ let DEBUG = false; const logIfDebug = (...args) => { console.log("DEBUG flag -> ", cordova.websocket.DEBUG) if (cordova.websocket.DEBUG) { console.log(...args); } }; class WebSocketInstance extends EventTarget { constructor(url, instanceId, binaryType) { super(); this.instanceId = instanceId; this.extensions = ''; this.readyState = WebSocketInstance.CONNECTING; this.onopen = null; this.onmessage = null; this.onclose = null; this.onerror = null; this.url = url; // NOTE: blob is not supported currently. this._binaryType = binaryType ? binaryType : ''; // empty as Default is string (Same Plugins might require this behavior) exec((event) => { logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Event from native:`, event); if (event.type === 'open') { this.readyState = WebSocketInstance.OPEN; this.extensions = event.extensions || ''; if (this.onopen) this.onopen(event); this.dispatchEvent(new Event('open')); } if (event.type === 'message') { let msgData = event.data; // parseAsText solely takes care of the state of binaryType, // sometimes, syncing binaryType to Java side might take longer. it's there to not wrongly pass normal string as base64. if (event.isBinary && this.binaryType === 'arraybuffer' && !event.parseAsText) { let binary = atob(msgData); let bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } msgData = bytes.buffer; } logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] msg Event:`, event, msgData); const msgEvent = new MessageEvent('message', { data: msgData }); Object.defineProperty(msgEvent, "binary", { enumerable: true, value: event.isBinary }) if (this.onmessage) this.onmessage(msgEvent); this.dispatchEvent(msgEvent); } if (event.type === 'close') { this.readyState = WebSocketInstance.CLOSED; const closeData = event && event.data ? event.data : {}; const closeEvent = new CloseEvent('close', { code: closeData.code, reason: closeData.reason, }); if (this.onclose) this.onclose(closeEvent); this.dispatchEvent(closeEvent); } if (event.type === 'error') { const errorMessage = event && event.data ? event.data : undefined; const errorEvent = new Event('error'); if (errorMessage !== undefined) { errorEvent.message = errorMessage; } if (this.onerror) this.onerror(errorEvent); this.dispatchEvent(errorEvent); } }, null, "WebSocketPlugin", "registerListener", [this.instanceId]); } get binaryType() { return this._binaryType || ''; } set binaryType(type) { // blob isn't supported but checked as browser compatibility, & it default to empty string if (type === 'blob' || type === 'arraybuffer' || type === '') { this._binaryType = type !== 'blob' ? type : ''; exec(null, null, "WebSocketPlugin", "setBinaryType", [this.instanceId, type]); } else { console.warn('Invalid binaryType, expected "blob" or "arraybuffer"'); } } send(message, binary) { if (this.readyState !== WebSocketInstance.OPEN) { throw new Error(`WebSocket is not open/connected`); } let finalMessage = null; if (message instanceof ArrayBuffer || ArrayBuffer.isView(message)) { const uint8Array = message instanceof ArrayBuffer ? new Uint8Array(message) : message; finalMessage = btoa(String.fromCharCode.apply(null, uint8Array)); // set to true as it's the data of a binary (type 0x2) message. binary = true; exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Sent message(binary payload):`, finalMessage), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Send error:`, err), "WebSocketPlugin", "send", [this.instanceId, finalMessage, binary]); } else if (typeof message === 'string') { finalMessage = message; // maybe a String to be sent as Binary (if it's true) if(binary) finalMessage = btoa(message) exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Sent message(binary=${binary}):`, finalMessage), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Send error:`, err), "WebSocketPlugin", "send", [this.instanceId, finalMessage, binary]); } else { throw new Error(`Unsupported message type: ${typeof message}`); } } /** * Closes the WebSocket connection. * * @param {number} code The status code explaining why the connection is being closed. * @param {string} reason A human-readable string explaining why the connection is being closed. */ close(code, reason) { this.readyState = WebSocketInstance.CLOSING; exec(() => logIfDebug(`[Cordova WebSocket - ID=${this.instanceId}] Close requested`, code, reason), (err) => console.error(`[Cordova WebSocket - ID=${this.instanceId}] Close error`, err), "WebSocketPlugin", "close", [this.instanceId, code, reason]); } } WebSocketInstance.CONNECTING = 0; WebSocketInstance.OPEN = 1; WebSocketInstance.CLOSING = 2; WebSocketInstance.CLOSED = 3; const connect = function(url, protocols = null, headers = null, binaryType) { return new Promise((resolve, reject) => { exec(instanceId => resolve(new WebSocketInstance(url, instanceId)), reject, "WebSocketPlugin", "connect", [url, protocols, binaryType, headers]); }); }; const listClients = function() { return new Promise((resolve, reject) => { exec(resolve, reject, "WebSocketPlugin", "listClients", []); }); }; /** Utility functions, in-case you lost the websocketInstance returned from the connect function */ const send = function(instanceId, message, binary) { return new Promise((resolve, reject) => { if (typeof message === 'string') { if(binary) message = btoa(message); exec(resolve, reject, "WebSocketPlugin", "send", [instanceId, message, binary]); } else if (message instanceof ArrayBuffer || ArrayBuffer.isView(message)) { const uint8Array = message instanceof ArrayBuffer ? new Uint8Array(message) : message; const base64Message = btoa(String.fromCharCode.apply(null, uint8Array)); exec(resolve, reject, "WebSocketPlugin", "send", [instanceId, base64Message, true]); } else { reject(`Unsupported message type: ${typeof message}`); } }); }; /** * Closes the WebSocket connection. * * @param {string} instanceId The ID of the WebSocketInstance to close. * @param {number} [code] (optional) The status code explaining why the connection is being closed. * @param {string} [reason] (optional) A human-readable string explaining why the connection is being closed. * * @returns {Promise} A promise that resolves when the close operation has completed. */ const close = function(instanceId, code, reason) { return new Promise((resolve, reject) => { exec(resolve, reject, "WebSocketPlugin", "close", [instanceId, code, reason]); }); }; module.exports = { connect, listClients, send, close, DEBUG }; ================================================ FILE: src/res/file-icons/style.css ================================================ @font-face { font-family: 'file-icons'; src: url('file-icons.ttf?ujkkfk') format('truetype'); font-weight: normal; font-style: normal; font-display: block; } .file { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'file-icons' !important; speak: never; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .file_type_default:before { content: "\e91a"; color: #c5c5c5; } .file_type_text:before { content: "\e985"; color: #8fa5c2; } .file_type_txt:before { content: "\e985"; color: #8fa5c2; } .file_type_image:before { content: "\e934"; color: #96e5cf; } .file_type_video:before { content: "\e991"; color: #e75749; } .file_type_astro:before { content: "\e9a4"; color: #fff; } .file_type_music:before { content: "\e950"; } .file_type_zip:before { content: "\e99b"; color: #a08a00; } .file_type_compressed:before { content: "\e99b"; color: #a08a00; } .file_type_android:before { content: "\e99e"; color: #3ddc84; } .file_type_smali:before { content: "\e99e"; color: #3ddc84; } .file_type_rhtml:before { content: "\e9a0"; color: #c00; } .file_type_forth:before { content: "\e9a1"; color: #35c0ed; } .file_type_frt:before { content: "\e9a1"; color: #35c0ed; } .file_type_fs:before { content: "\e9a1"; color: #35c0ed; } .file_type_ldr:before { content: "\e9a1"; color: #35c0ed; } .file_type_fth:before { content: "\e9a1"; color: #35c0ed; } .file_type_4th:before { content: "\e9a1"; color: #35c0ed; } .file_type_alda:before { content: "\e99f"; color: #35c0ed; } .file_type_abc:before { content: "\e9ea"; color: #00aeef; } .file_type_sap:before { content: "\e9eb"; color: #00aeef; } .file_type_abap:before { content: "\e9eb"; color: #00aeef; } .file_type_apache:before { content: "\e900"; color: #d22128; } .file_type_apache_conf:before { content: "\e900"; color: #d22128; } .file_type_htaccess:before { content: "\e900"; color: #d22128; } .file_type_htgroups:before { content: "\e900"; color: #d22128; } .file_type_conf:before { content: "\e900"; color: #d22128; } .file_type_htpasswd:before { content: "\e900"; color: #d22128; } .file_type_apex:before { content: "\e901"; color: #0072a0; } .file_type_cls:before { content: "\e901"; color: #0072a0; } .file_type_trigger:before { content: "\e901"; color: #0072a0; } .file_type_tgr:before { content: "\e901"; color: #0072a0; } .file_type_asciidoc:before { content: "\e902"; color: #e40046; } .file_type_adoc:before { content: "\e902"; color: #e40046; } .file_type_asl:before { content: "\e903"; color: #65bed9; } .file_type_dsl:before { content: "\e903"; color: #65bed9; } .file_type_assembly:before { content: "\e904"; color: #0000bf; } .file_type_a:before { content: "\e904"; color: #0000bf; } .file_type_asm:before { content: "\e904"; color: #0000bf; } .file_type_assembly_x86:before { content: "\e904"; color: #0000bf; } .file_type_autohotkey:before { content: "\e905"; color: #44a915; } .file_type_ahk:before { content: "\e905"; color: #44a915; } .file_type_babel:before { content: "\e906"; color: #f9dc3e; } .file_type_babelrc:before { content: "\e906"; color: #f9dc3e; } .file_type_bibtex:before { content: "\e907"; } .file_type_bib:before { content: "\e907"; } .file_type_blade:before { content: "\e908"; color: #ff2d20; } .file_type_php_laravel_blade:before { content: "\e908"; color: #ff2d20; } .file_type_c:before { content: "\e909"; color: #005f91; } .file_type_cabal:before { content: "\e90a"; color: #6bc9dd; } .file_type_haskell_cabal:before { content: "\e90a"; color: #6bc9dd; } .file_type_cheader:before { content: "\e90b"; color: #005f91; } .file_type_clojure:before { content: "\e90c"; color: #5881d8; } .file_type_clj:before { content: "\e90c"; color: #5881d8; } .file_type_clojurescript:before { content: "\e90d"; color: #5f7fbf; } .file_type_cljs:before { content: "\e90d"; color: #5f7fbf; } .file_type_cmake:before { content: "\e90e"; color: #064f8c; } .file_type_cobol:before { content: "\e90f"; color: #005ca5; } .file_type_cbl:before { content: "\e90f"; color: #005ca5; } .file_type_cob:before { content: "\e90f"; color: #005ca5; } .file_type_coffeescript:before { content: "\e910"; } .file_type_coffee:before { content: "\e910"; } .file_type_cf:before { content: "\e910"; } .file_type_cson:before { content: "\e910"; } .file_type_cakefile:before { content: "\e910"; } .file_type_coldfussion:before { content: "\e911"; color: #859aa5; } .file_type_cfm:before { content: "\e911"; color: #859aa5; } .file_type_cfc:before { content: "\e911"; color: #859aa5; } .file_type_cpp:before { content: "\e912"; color: #984c93; } .file_type_cc:before { content: "\e912"; color: #984c93; } .file_type_cxx:before { content: "\e912"; color: #984c93; } .file_type_ino:before { content: "\e912"; color: #984c93; } .file_type_cppheader:before { content: "\e913"; color: #984c93; } .file_type_hh:before { content: "\e913"; color: #984c93; } .file_type_hpp:before { content: "\e913"; color: #984c93; } .file_type_crystal:before { content: "\e914"; color: #c8c8c8; } .file_type_cr:before { content: "\e914"; color: #c8c8c8; } .file_type_csharp:before { content: "\e915"; color: #368832; } .file_type_cs:before { content: "\e915"; color: #368832; } .file_type_css:before { content: "\e916"; color: #1572b6; } .file_type_cssmap:before { content: "\e917"; color: #33a9dc; } .file_type_dartlang:before { content: "\e918"; color: #0175c2; } .file_type_dart:before { content: "\e918"; color: #0175c2; } .file_type_db:before { content: "\e919"; color: #c4c7ce; } .file_type_aql:before { content: "\e919"; color: #c4c7ce; } .file_type_diff:before { content: "\e91b"; color: #536653; } .file_type_patch:before { content: "\e91b"; color: #536653; } .file_type_django:before { content: "\e91c"; color: #44b78b; } .file_type_dlang:before { content: "\e91d"; color: #b03931; } .file_type_d:before { content: "\e91d"; color: #b03931; } .file_type_di:before { content: "\e91d"; color: #b03931; } .file_type_docker:before { content: "\e91e"; color: #00aada; } .file_type_dockerfile:before { content: "\e91e"; color: #00aada; } .file_type_docz:before { content: "\e91f"; color: #5f5846; } .file_type_drools:before { content: "\e920"; color: #0095dd; } .file_type_drl:before { content: "\e920"; color: #0095dd; } .file_type_ejs:before { content: "\e921"; color: #90a93a; } .file_type_elixir:before { content: "\e922"; color: #7c648f; } .file_type_ex:before { content: "\e922"; color: #7c648f; } .file_type_exs:before { content: "\e922"; color: #7c648f; } .file_type_html_elixir:before { content: "\e922"; color: #7c648f; } .file_type_elm:before { content: "\e923"; color: #8cd636; } .file_type_erlang:before { content: "\e924"; color: #a2003e; } .file_type_erl:before { content: "\e924"; color: #a2003e; } .file_type_hrl:before { content: "\e924"; color: #a2003e; } .file_type_eslint:before { content: "\e925"; color: #4b32c3; } .file_type_fortran:before { content: "\e926"; color: #9e564c; } .file_type_f:before { content: "\e926"; color: #9e564c; } .file_type_f90:before { content: "\e926"; color: #9e564c; } .file_type_fsharp:before { content: "\e927"; color: #378bba; } .file_type_fs1:before { content: "\e927"; color: #378bba; } .file_type_fsi:before { content: "\e927"; color: #378bba; } .file_type_fsx:before { content: "\e927"; color: #378bba; } .file_type_fsscript:before { content: "\e927"; color: #378bba; } .file_type_gcode:before { content: "\e928"; color: #ba0000; } .file_type_git:before { content: "\e929"; color: #f05032; } .file_type_gitignore:before { content: "\e929"; color: #f05032; } .file_type_golang:before { content: "\e92a"; color: #00add8; } .file_type_go:before { content: "\e92a"; color: #00add8; } .file_type_gradle:before { content: "\e92b"; color: #136a7d; } .file_type_graphql:before { content: "\e92c"; color: #e10098; } .file_type_gql:before { content: "\e92c"; color: #e10098; } .file_type_graphqlschema:before { content: "\e92c"; color: #e10098; } .file_type_prql:before { content: "\e9a3"; color: #dfb13c; } .file_type_groovy:before { content: "\e92d"; } .file_type_haml:before { content: "\e92e"; } .file_type_handlebars:before { content: "\e92f"; color: #c19770; } .file_type_hbs:before { content: "\e92f"; color: #c19770; } .file_type_tpl:before { content: "\e92f"; color: #c19770; } .file_type_mustache:before { content: "\e92f"; color: #c19770; } .file_type_haskell:before { content: "\e930"; color: #5d4f85; } .file_type_hs:before { content: "\e930"; color: #5d4f85; } .file_type_haxe:before { content: "\e931"; color: #ea8220; } .file_type_hx:before { content: "\e931"; color: #ea8220; } .file_type_hjson:before { content: "\e932"; color: #01ca24; } .file_type_html:before { content: "\e933"; color: #e34f26; } .file_type_htm:before { content: "\e933"; color: #e34f26; } .file_type_xhtml:before { content: "\e933"; color: #e34f26; } .file_type_we:before { content: "\e933"; color: #e34f26; } .file_type_wpy:before { content: "\e933"; color: #e34f26; } .file_type_ini:before { content: "\e935"; color: #99b8c4; } .file_type_conf1:before { content: "\e935"; color: #99b8c4; } .file_type_cfg:before { content: "\e935"; color: #99b8c4; } .file_type_prefs:before { content: "\e935"; color: #99b8c4; } .file_type_io:before { content: "\e936"; color: #c2c2c2; } .file_type_java:before { content: "\e937"; color: #007396; } .file_type_javascript:before { content: "\e938"; color: #f5de19; } .file_type_js:before { content: "\e938"; color: #f5de19; } .file_type_jsm:before { content: "\e938"; color: #f5de19; } .file_type_jsx:before { content: "\e938"; color: #f5de19; } .file_type_mjs:before { content: "\e938"; color: #f5de19; } .file_type_cjs:before { content: "\e938"; color: #f5de19; } .file_type_jsbeautify:before { content: "\e939"; color: #f1662a; } .file_type_jsconfig:before { content: "\e93a"; color: #f5de19; } .file_type_jsmap:before { content: "\e93b"; color: #f5de19; } .file_type_json:before { content: "\e93c"; color: #f5de19; } .file_type_curly:before { content: "\e93c"; color: #f5de19; } .file_type_json5:before { content: "\e93c"; color: #f5de19; } .file_type_jsp:before { content: "\e93d"; color: #e56f14; } .file_type_julia:before { content: "\e93e"; color: #aa79c1; } .file_type_jl:before { content: "\e93e"; color: #aa79c1; } .file_type_jupyter:before { content: "\e93f"; color: #f37626; } .file_type_kotlin:before { content: "\e940"; color: #0095d5; } .file_type_kt:before { content: "\e940"; color: #0095d5; } .file_type_kts:before { content: "\e940"; color: #0095d5; } .file_type_latex:before { content: "\e941"; color: #008080; } .file_type_less:before { content: "\e942"; color: #294e82; } .file_type_license:before { content: "\e943"; color: #aa79c1; } .file_type_odin:before { content: "\e9a2"; color: #00f3ff; } .file_type_liquid:before { content: "\e944"; color: #004999; } .file_type_lisp:before { content: "\e945"; color: #c40804; } .file_type_livescript:before { content: "\e946"; color: #317eac; } .file_type_log:before { content: "\e947"; color: #00bd02; } .file_type_lsl:before { content: "\e948"; color: #7fadb2; } .file_type_lua:before { content: "\e949"; color: #2c2d72; } .file_type_luau:before { content: "\e949"; color: #2c2d72; } .file_type_lp:before { content: "\e949"; color: #2c2d72; } .file_type_luapage:before { content: "\e949"; color: #2c2d72; } .file_type_makefile:before { content: "\e94a"; color: #a42e2b; } .file_type_gnumakefile:before { content: "\e94a"; color: #a42e2b; } .file_type_ocamlmakefile:before { content: "\e94a"; color: #a42e2b; } .file_type_make:before { content: "\e94a"; color: #a42e2b; } .file_type_map:before { content: "\e94b"; color: #45d339; } .file_type_mariadb:before { content: "\e94c"; color: #c49a6c; } .file_type_markdown:before { content: "\e94d"; } .file_type_md:before { content: "\e94d"; } .file_type_matlab:before { content: "\e94e"; color: #ff8d10; } .file_type_mediawiki:before { content: "\e94f"; color: #ffd800; } .file_type_wiki:before { content: "\e94f"; color: #ffd800; } .file_type_mysql:before { content: "\e951"; color: #4479a1; } .file_type_mysqlserver:before { content: "\e951"; color: #4479a1; } .file_type_nginx:before { content: "\e952"; color: #269539; } .file_type_nim:before { content: "\e953"; color: #ffe953; } .file_type_npm:before { content: "\e954"; color: #cb3837; } .file_type_nunjucks:before { content: "\e955"; color: #486411; } .file_type_nunjs:before { content: "\e955"; color: #486411; } .file_type_nj:before { content: "\e955"; color: #486411; } .file_type_njk:before { content: "\e955"; color: #486411; } .file_type_objectivec:before { content: "\e956"; color: #c2c2c2; } .file_type_m:before { content: "\e956"; color: #c2c2c2; } .file_type_objectivecpp:before { content: "\e957"; color: #c2c2c2; } .file_type_mm:before { content: "\e957"; color: #c2c2c2; } .file_type_ocaml:before { content: "\e958"; color: #ec6813; } .file_type_ml:before { content: "\e958"; color: #ec6813; } .file_type_mli:before { content: "\e958"; color: #ec6813; } .file_type_opengl:before { content: "\e959"; color: #4386b5; } .file_type_glsl:before { content: "\e959"; color: #4386b5; } .file_type_frag:before { content: "\e959"; color: #4386b5; } .file_type_vert:before { content: "\e959"; color: #4386b5; } .file_type_perl:before { content: "\e95a"; color: #3a3c5b; } .file_type_pl:before { content: "\e95a"; color: #3a3c5b; } .file_type_pm:before { content: "\e95a"; color: #3a3c5b; } .file_type_p6:before { content: "\e95a"; color: #3a3c5b; } .file_type_pl6:before { content: "\e95a"; color: #3a3c5b; } .file_type_pm6:before { content: "\e95a"; color: #3a3c5b; } .file_type_pgsql:before { content: "\e95b"; color: #336791; } .file_type_php:before { content: "\e95c"; color: #777bb4; } .file_type_inc:before { content: "\e95c"; color: #777bb4; } .file_type_phtml:before { content: "\e95c"; color: #777bb4; } .file_type_shtml:before { content: "\e95c"; color: #777bb4; } .file_type_php3:before { content: "\e95c"; color: #777bb4; } .file_type_php4:before { content: "\e95c"; color: #777bb4; } .file_type_php5:before { content: "\e95c"; color: #777bb4; } .file_type_phps:before { content: "\e95c"; color: #777bb4; } .file_type_aw:before { content: "\e95c"; color: #777bb4; } .file_type_ctp:before { content: "\e95c"; color: #777bb4; } .file_type_module:before { content: "\e95c"; color: #777bb4; } .file_type_plsql:before { content: "\e95d"; color: #f00; } .file_type_postcss:before { content: "\e95e"; color: #dd3735; } .file_type_postcssconfig:before { content: "\e95f"; color: #dd3735; } .file_type_postgresql:before { content: "\e960"; color: #336791; } .file_type_powershell:before { content: "\e961"; color: #5391fe; } .file_type_ps1:before { content: "\e961"; color: #5391fe; } .file_type_batchfile:before { content: "\e961"; color: #5391fe; } .file_type_bat:before { content: "\e961"; color: #5391fe; } .file_type_cmd:before { content: "\e961"; color: #5391fe; } .file_type_prettier:before { content: "\e962"; color: #f7b93e; } .file_type_prisma:before { content: "\e963"; color: #d2d2d2; } .file_type_prolog:before { content: "\e964"; color: #ec1c24; } .file_type_plg:before { content: "\e964"; color: #ec1c24; } .file_type_protobuf:before { content: "\e965"; color: #ff5c77; } .file_type_proto:before { content: "\e965"; color: #ff5c77; } .file_type_puppet:before { content: "\e966"; color: #ffae1a; } .file_type_epp:before { content: "\e966"; color: #ffae1a; } .file_type_pp:before { content: "\e966"; color: #ffae1a; } .file_type_python:before { content: "\e967"; color: #3776ab; } .file_type_py:before { content: "\e967"; color: #3776ab; } .file_type_pyc:before { content: "\e967"; color: #3776ab; } .file_type_pyd:before { content: "\e967"; color: #3776ab; } .file_type_pyo:before { content: "\e967"; color: #3776ab; } .file_type_pyw:before { content: "\e967"; color: #3776ab; } .file_type_pyz:before { content: "\e967"; color: #3776ab; } .file_type_gyp:before { content: "\e967"; color: #3776ab; } .file_type_q:before { content: "\e968"; color: #1e78b3; } .file_type_qml:before { content: "\e969"; color: #41cd52; } .file_type_qsharp:before { content: "\e96a"; color: #33c; } .file_type_rlang:before { content: "\e96b"; color: #276dc3; } .file_type_r:before { content: "\e96b"; color: #276dc3; } .file_type_razor:before { content: "\e96c"; color: #368832; } .file_type_cshtml:before { content: "\e96c"; color: #368832; } .file_type_asp:before { content: "\e96c"; color: #368832; } .file_type_red:before { content: "\e96d"; color: #b32629; } .file_type_reds:before { content: "\e96d"; color: #b32629; } .file_type_rest:before { content: "\e96e"; color: #ce3f31; } .file_type_robot:before { content: "\e96f"; color: #00b0d8; } .file_type_resource:before { content: "\e96f"; color: #00b0d8; } .file_type_rollup:before { content: "\e970"; color: #ec4a3f; } .file_type_ruby:before { content: "\e971"; color: #cc342d; } .file_type_rakefile:before { content: "\e971"; color: #cc342d; } .file_type_guardfile:before { content: "\e971"; color: #cc342d; } .file_type_gemfile:before { content: "\e971"; color: #cc342d; } .file_type_rb:before { content: "\e971"; color: #cc342d; } .file_type_ru:before { content: "\e971"; color: #cc342d; } .file_type_gemspec:before { content: "\e971"; color: #cc342d; } .file_type_rake:before { content: "\e971"; color: #cc342d; } .file_type_rust:before { content: "\e972"; } .file_type_rs:before { content: "\e972"; } .file_type_sass:before { content: "\e973"; color: #cd6799; } .file_type_scala:before { content: "\e974"; color: #dc322f; } .file_type_sbt:before { content: "\e974"; color: #dc322f; } .file_type_scss:before { content: "\e975"; color: #cd6799; } .file_type_shell:before { content: "\e976"; color: #d9b400; } .file_type_sh:before { content: "\e976"; color: #d9b400; } .file_type_bash:before { content: "\e976"; color: #d9b400; } .file_type_bashrc:before { content: "\e976"; color: #d9b400; } .file_type_slim:before { content: "\e977"; color: #5b4a8f; } .file_type_skim:before { content: "\e977"; color: #5b4a8f; } .file_type_smarty:before { content: "\e978"; color: #fdf700; } .file_type_tpl1:before { content: "\e978"; color: #fdf700; } .file_type_sql:before { content: "\e979"; color: #ffda44; } .file_type_sqlserver:before { content: "\e979"; color: #ffda44; } .file_type_sqlite:before { content: "\e97a"; color: #0f80cc; } .file_type_sqlite3:before { content: "\e97a"; color: #0f80cc; } .file_type_stylus:before { content: "\e97b"; } .file_type_styl:before { content: "\e97b"; } .file_type_svelte:before { content: "\e97c"; color: #ff3e00; } .file_type_svg:before { content: "\e97d"; color: #ffb13b; } .file_type_swift:before { content: "\e97e"; color: #fa7343; } .file_type_tailwind:before { content: "\e97f"; color: #44a8b3; } .file_type_tcl:before { content: "\e980"; color: #c3b15f; } .file_type_terraform:before { content: "\e981"; color: #623ce4; } .file_type_tf:before { content: "\e981"; color: #623ce4; } .file_type_tfvars:before { content: "\e981"; color: #623ce4; } .file_type_terragrunt:before { content: "\e981"; color: #623ce4; } .file_type_testjs:before { content: "\e982"; color: #f5de19; } .file_type_testts:before { content: "\e983"; color: #007acc; } .file_type_tex:before { content: "\e984"; color: #cfcfcf; } .file_type_textile:before { content: "\e986"; color: #ffe7ac; } .file_type_toml:before { content: "\e987"; color: #aaa; } .file_type_tsconfig:before { content: "\e988"; color: #007acc; } .file_type_twig:before { content: "\e989"; color: #78dc50; } .file_type_swig:before { content: "\e989"; color: #78dc50; } .file_type_typescript:before { content: "\e98a"; color: #007acc; } .file_type_ts:before { content: "\e98a"; color: #007acc; } .file_type_typescriptdef:before { content: "\e98b"; color: #007acc; } .file_type_types:before { content: "\e98b"; color: #007acc; } .file_type_typings:before { content: "\e98b"; color: #007acc; } .file_type_vala:before { content: "\e98c"; color: #403757; } .file_type_vb:before { content: "\e98d"; color: #00519a; } .file_type_vbs:before { content: "\e98d"; color: #00519a; } .file_type_velocity:before { content: "\e98e"; color: #262693; } .file_type_vm:before { content: "\e98e"; color: #262693; } .file_type_verilog:before { content: "\e98f"; color: #1a348f; } .file_type_v:before { content: "\e98f"; color: #1a348f; } .file_type_vh:before { content: "\e98f"; color: #1a348f; } .file_type_sv:before { content: "\e98f"; color: #1a348f; } .file_type_svh:before { content: "\e98f"; color: #1a348f; } .file_type_vhdl:before { content: "\e990"; color: #0d9b35; } .file_type_vhd:before { content: "\e990"; color: #0d9b35; } .file_type_view:before { content: "\e992"; color: #e44f26; } .file_type_vue:before { content: "\e993"; color: #41b883; } .file_type_wasm:before { content: "\e994"; color: #654ff0; } .file_type_webpack:before { content: "\e995"; color: #8dd6f9; } .file_type_xml:before { content: "\e996"; color: #f1662a; } .file_type_rdf:before { content: "\e996"; color: #f1662a; } .file_type_rss:before { content: "\e996"; color: #f1662a; } .file_type_wsdl:before { content: "\e996"; color: #f1662a; } .file_type_xslt:before { content: "\e996"; color: #f1662a; } .file_type_xamlatom:before { content: "\e996"; color: #f1662a; } .file_type_mathml:before { content: "\e996"; color: #f1662a; } .file_type_mml:before { content: "\e996"; color: #f1662a; } .file_type_xul:before { content: "\e996"; color: #f1662a; } .file_type_xbl:before { content: "\e996"; color: #f1662a; } .file_type:before { content: "\e996"; color: #f1662a; } .file_type_xquery:before { content: "\e997"; color: #f1662a; } .file_type_xsl:before { content: "\e998"; color: #33a9dc; } .file_type_yaml:before { content: "\e999"; color: #ffe885; } .file_type_yml:before { content: "\e999"; color: #ffe885; } .file_type_yarn:before { content: "\e99a"; color: #2188b6; } .file_type_actionscript:before { content: "\e99c"; color: #c41718; } .file_type_as:before { content: "\e99c"; color: #c41718; } .file_type_ada:before { content: "\e99d"; color: #0f23c3; } .file_type_adb:before { content: "\e99d"; color: #0f23c3; } .file_type_zig:before { content: "\e9a5"; color: #f7a41d; } ================================================ FILE: src/res/icons/style.css ================================================ @font-face { font-family: "code-editor-icon"; src: url("icons.ttf?v2") format("truetype"); font-weight: normal; font-style: normal; font-display: block; } .icon { /* Use !important to prevent extensions from overriding this font. */ font-family: "code-editor-icon" !important; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon.text-search:before { content: "\f02a"; } .icon.wand:before { content: "\f014"; } .icon.wand-sparkles:before { content: "\f013"; } .icon.link:before { content: "\f012"; } .icon.brain:before { content: "\f011"; } .icon.paperclip:before { content: "\f010"; } .icon.palette:before { content: "\f00f"; } .icon.loader:before { content: "\f00e"; } .icon.square-terminal:before { content: "\f00d"; } .icon.house:before { content: "\f00c"; } .icon.message-circle:before { content: "\f00b"; } .icon.user-round:before { content: "\f00a"; } .icon.funnel:before { content: "\f009"; } .icon.zap:before { content: "\f000"; } .icon.verified:before { content: "\f001"; } .icon.terminal:before { content: "\f002"; } .icon.tag:before { content: "\f003"; } .icon.scale:before { content: "\f004"; } .icon.cart:before { content: "\f005"; } .icon.lightbulb:before { content: "\f006"; } .icon.pin:before { content: "\f007"; } .icon.pin-off:before { content: "\f008"; } .icon.document-information:before { content: "\e900"; } .icon.document-information-outline:before { content: "\e901"; } .icon.document-forbidden:before { content: "\e902"; } .icon.document-forbidden-outline:before { content: "\e903"; } .icon.document-remove:before { content: "\e904"; } .icon.document-remove-outline:before { content: "\e905"; } .icon.document-checked:before { content: "\e906"; } .icon.document-checked-outline:before { content: "\e907"; } .icon.document-cancel:before { content: "\e908"; } .icon.document-cancel-outline:before { content: "\e909"; } .icon.document-error:before { content: "\e90a"; } .icon.document-error-outline:before { content: "\e90b"; } .icon.document-locked:before { content: "\e90c"; } .icon.document-locked-outline:before { content: "\e90d"; } .icon.document-unlocked:before { content: "\e90e"; } .icon.document-unlocked-outline:before { content: "\e90f"; } .icon.document-search:before { content: "\e910"; } .icon.document-search-outline:before { content: "\e911"; } .icon.document-code:before { content: "\e912"; } .icon.document-code-outline:before { content: "\e913"; } .icon.document-text:before { content: "\e914"; } .icon.document-text-outline:before { content: "\e915"; } .icon.text_format:before { content: "\e916"; } .icon.chat_bubble:before { content: "\e917"; } .icon.movie:before { content: "\e918"; } .icon.videocam:before { content: "\e919"; } .icon.document-add:before { content: "\e91a"; } .icon.document-add-outline:before { content: "\e91b"; } .icon.documents:before { content: "\e91c"; } .icon.documents-outline:before { content: "\e91d"; } .icon.folder-information:before { content: "\e91e"; } .icon.folder-information-outline:before { content: "\e91f"; } .icon.folder-remove:before { content: "\e920"; } .icon.folder-remove-outline:before { content: "\e921"; } .icon.folder-add:before { content: "\e922"; } .icon.folder-add-outline:before { content: "\e923"; } .icon.folder-upload:before { content: "\e924"; } .icon.folder-upload-outline:before { content: "\e925"; } .icon.folder-download:before { content: "\e926"; } .icon.folder-download-outline:before { content: "\e927"; } .icon.folder-search:before { content: "\e928"; } .icon.folder-search-outline:before { content: "\e929"; } .icon.folder:before { content: "\e92a"; } .icon.folder-outline:before { content: "\e92b"; } .icon.folder2:before { content: "\e92c"; } .icon.folder2-outline:before { content: "\e92d"; } .icon.android:before { content: "\e92e"; color: #a4c639; } .icon.angular:before { content: "\e92f"; color: #dd0031; } .icon.css3:before { content: "\e930"; color: #1572b6; } .icon.dev-dot-to:before { content: "\e931"; } .icon.facebook:before { content: "\e932"; color: #4172b8; } .icon.git:before { content: "\e933"; color: #f05032; } .icon.github:before { content: "\e934"; } .icon.gmail:before { content: "\e935"; color: #d14836; } .icon.googlechrome:before { content: "\e936"; color: #4285f4; } .icon.googledrive:before { content: "\e937"; color: #4285f4; } .icon.googleplay:before { content: "\e938"; color: #607d8b; } .icon.html5:before { content: "\e939"; color: #e34f26; } .icon.instagram:before { content: "\e93a"; color: #e4405f; } .icon.ionic:before { content: "\e93b"; color: #3880ff; } .icon.javascript:before { content: "\e93c"; color: #f7df1e; } .icon.jekyll:before { content: "\e93d"; color: #c00; } .icon.linkedin:before { content: "\e93e"; color: #0077b5; } .icon.markdown:before { content: "\e93f"; } .icon.npm:before { content: "\e940"; color: #cb3837; } .icon.python:before { content: "\e941"; color: #3776ab; } .icon.react:before { content: "\e942"; color: #61dafb; } .icon.stackexchange:before { content: "\e943"; color: #1e5397; } .icon.stackoverflow:before { content: "\e944"; color: #fe7a16; } .icon.telegram:before { content: "\e945"; color: #2ca5e0; } .icon.twitter:before { content: "\e946"; color: #1da1f2; } .icon.visualstudiocode:before { content: "\e947"; color: #007acc; } .icon.webpack:before { content: "\e948"; color: #8dd6f9; } .icon.yarn:before { content: "\e949"; color: #2c8ebb; } .icon.youtube:before { content: "\e94a"; color: #f00; } .icon.error:before { content: "\e94b"; } .icon.error_outline:before { content: "\e94c"; } .icon.warningreport_problem:before { content: "\e94d"; } .icon.library_addqueueadd_to_photos:before { content: "\e94e"; } .icon.library_music:before { content: "\e94f"; } .icon.new_releases:before { content: "\e950"; } .icon.not_interesteddo_not_disturb:before { content: "\e951"; } .icon.pause:before { content: "\e952"; } .icon.pause_circle_filled:before { content: "\e953"; } .icon.pause_circle_outline:before { content: "\e954"; } .icon.play_arrow:before { content: "\e955"; } .icon.play_circle_filled:before { content: "\e956"; } .icon.play_circle_outline:before { content: "\e957"; } .icon.repeat:before { content: "\e958"; } .icon.repeat_one:before { content: "\e959"; } .icon.replay:before { content: "\e95a"; } .icon.shuffle:before { content: "\e95b"; } .icon.skip_next:before { content: "\e95c"; } .icon.skip_previous:before { content: "\e95d"; } .icon.emailmailmarkunreadlocal_post_office:before { content: "\e95e"; } .icon.vpn_key:before { content: "\e95f"; } .icon.add:before { content: "\e960"; } .icon.add_box:before { content: "\e961"; } .icon.add_circle:before { content: "\e962"; } .icon.add_circle_outlinecontrol_point:before { content: "\e963"; } .icon.block:before { content: "\e964"; } .icon.clearclose:before { content: "\e965"; } .icon.copy:before { content: "\e966"; } .icon.cut:before { content: "\e967"; } .icon.paste:before { content: "\e968"; } .icon.edit:before { content: "\e969"; } .icon.drafts:before { content: "\e96a"; } .icon.forward:before { content: "\e96b"; } .icon.remove:before { content: "\e96c"; } .icon.remove_circledo_not_disturb_on:before { content: "\e96d"; } .icon.remove_circle_outline:before { content: "\e96e"; } .icon.send:before { content: "\e96f"; } .icon.undo:before { content: "\e970"; } .icon.save_alt:before { content: "\e971"; } .icon.file_copy:before { content: "\e972"; } .icon.sd_storagesd_card:before { content: "\e973"; } .icon.attach_file:before { content: "\e974"; } .icon.attach_money:before { content: "\e975"; } .icon.format_bold:before { content: "\e976"; } .icon.format_color_fill:before { content: "\e977"; } .icon.music_video:before { content: "\e978"; } .icon.format_size:before { content: "\e979"; } .icon.format_underlined:before { content: "\e97a"; } .icon.insert_chartpollassessment:before { content: "\e97b"; } .icon.insert_emoticontag_facesmood:before { content: "\e97c"; } .icon.insert_invitationevent:before { content: "\e97d"; } .icon.image:before { content: "\e97e"; } .icon.publish:before { content: "\e97f"; } .icon.vertical_align_bottom:before { content: "\e980"; } .icon.vertical_align_top:before { content: "\e981"; } .icon.monetization_on:before { content: "\e982"; } .icon.cloud:before { content: "\e983"; } .icon.cloud_done:before { content: "\e984"; } .icon.cloud_download:before { content: "\e985"; } .icon.cloud_uploadbackup:before { content: "\e986"; } .icon.file_downloadget_app:before { content: "\e987"; } .icon.file_upload:before { content: "\e988"; } .icon.audiotrack:before { content: "\e989"; } .icon.music_note:before { content: "\e98a"; } .icon.movie_filter:before { content: "\e98b"; } .icon.local_moviestheaters:before { content: "\e98c"; } .icon.keyboard_arrow_down:before { content: "\e98d"; } .icon.keyboard_arrow_left:before { content: "\e98e"; } .icon.keyboard_arrow_right:before { content: "\e98f"; } .icon.keyboard_arrow_up:before { content: "\e990"; } .icon.keyboard_backspace:before { content: "\e991"; } .icon.keyboard_capslock:before { content: "\e992"; } .icon.keyboard_hide:before { content: "\e993"; } .icon.keyboard_tab:before { content: "\e994"; } .icon.keyboard_voice:before { content: "\e995"; } .icon.laptop_chromebook:before { content: "\e996"; } .icon.laptop_mac:before { content: "\e997"; } .icon.laptop_windows:before { content: "\e998"; } .icon.phone_android:before { content: "\e999"; } .icon.phone_iphone:before { content: "\e99a"; } .icon.color_lenspalette:before { content: "\e99b"; } .icon.colorize:before { content: "\e99c"; } .icon.navigate_beforechevron_left:before { content: "\e99d"; } .icon.navigate_nextchevron_right:before { content: "\e99e"; } .icon.remove_red_eyevisibility:before { content: "\e99f"; } .icon.tune:before { content: "\e9a0"; } .icon.add_photo_alternate:before { content: "\e9a1"; } .icon.image_search:before { content: "\e9a2"; } .icon.beenhere:before { content: "\e9a3"; } .icon.apps:before { content: "\e9a4"; } .icon.arrow_back:before { content: "\e9a5"; } .icon.arrow_drop_down:before { content: "\e9a6"; } .icon.arrow_drop_down_circle:before { content: "\e9a7"; } .icon.arrow_drop_up:before { content: "\e9a8"; } .icon.arrow_forward:before { content: "\e9a9"; } .icon.cancel:before { content: "\e9aa"; } .icon.check:before { content: "\e9ab"; } .icon.expand_less:before { content: "\e9ac"; } .icon.expand_more:before { content: "\e9ad"; } .icon.fullscreen:before { content: "\e9ae"; } .icon.fullscreen_exit:before { content: "\e9af"; } .icon.menu:before { content: "\e9b0"; } .icon.keyboard_control:before { content: "\e9b1"; } .icon.more_vert:before { content: "\e9b2"; } .icon.refresh:before { content: "\e9b3"; } .icon.unfold_less:before { content: "\e9b4"; } .icon.unfold_more:before { content: "\e9b5"; } .icon.arrow_upward:before { content: "\e9b6"; } .icon.subdirectory_arrow_left:before { content: "\e9b7"; } .icon.subdirectory_arrow_right:before { content: "\e9b8"; } .icon.arrow_downward:before { content: "\e9b9"; } .icon.first_page:before { content: "\e9ba"; } .icon.last_page:before { content: "\e9bb"; } .icon.arrow_left:before { content: "\e9bc"; } .icon.arrow_right:before { content: "\e9bd"; } .icon.arrow_back_ios:before { content: "\e9be"; } .icon.arrow_forward_ios:before { content: "\e9bf"; } .icon.folder_special:before { content: "\e9c0"; } .icon.priority_high:before { content: "\e9c1"; } .icon.notifications:before { content: "\e9c2"; } .icon.notifications_none:before { content: "\e9c3"; } .icon.person:before { content: "\e9c4"; } .icon.public:before { content: "\e9c5"; } .icon.share:before { content: "\e9c6"; } .icon.sentiment_dissatisfied:before { content: "\e9c7"; } .icon.sentiment_neutral:before { content: "\e9c8"; } .icon.sentiment_satisfied:before { content: "\e9c9"; } .icon.sentiment_very_dissatisfied:before { content: "\e9ca"; } .icon.sentiment_very_satisfied:before { content: "\e9cb"; } .icon.stargrade:before { content: "\e9cc"; } .icon.star_half:before { content: "\e9cd"; } .icon.star_outline:before { content: "\e9ce"; } .icon.account_box:before { content: "\e9cf"; } .icon.account_circle:before { content: "\e9d0"; } .icon.android-full:before { content: "\e9d1"; } .icon.autorenew:before { content: "\e9d2"; } .icon.cached:before { content: "\e9d3"; } .icon.check_circle:before { content: "\e9d4"; } .icon.code:before { content: "\e9d5"; } .icon.delete:before { content: "\e9d6"; } .icon.exit_to_app:before { content: "\e9d7"; } .icon.extension:before { content: "\e9d8"; } .icon.favorite:before { content: "\e9d9"; } .icon.favorite_outline:before { content: "\e9da"; } .icon.help:before { content: "\e9db"; } .icon.highlight_remove:before { content: "\e9dc"; } .icon.historyrestore:before { content: "\e9dd"; } .icon.home:before { content: "\e9de"; } .icon.httpslock:before { content: "\e9df"; } .icon.info:before { content: "\e9e0"; } .icon.info_outline:before { content: "\e9e1"; } .icon.input:before { content: "\e9e2"; } .icon.label:before { content: "\e9e3"; } .icon.label_outline:before { content: "\e9e4"; } .icon.perm_media:before { content: "\e9e5"; } .icon.power_settings_new:before { content: "\e9e6"; } .icon.search:before { content: "\e9e7"; } .icon.settings:before { content: "\e9e8"; } .icon.settings_applications:before { content: "\e9e9"; } .icon.shop:before { content: "\e9ea"; } .icon.spellcheck:before { content: "\e9eb"; } .icon.stars:before { content: "\e9ec"; } .icon.translate:before { content: "\e9ed"; } .icon.visibility_off:before { content: "\e9ee"; } .icon.update:before { content: "\e9ef"; } .icon.g_translate:before { content: "\e9f0"; } .icon.check_circle_outline:before { content: "\e9f1"; } .icon.delete_outline:before { content: "\e9f2"; } .icon.drive_folder_upload:before { content: "\e9f3"; } .icon.library_add_check:before { content: "\e9f4"; } .icon.replay_circle_filled:before { content: "\e9f5"; } .icon.redo:before { content: "\e9f6"; } .icon.save:before { content: "\e9f7"; } .icon.zip:before { content: "\e9f8"; } .icon.zip-outline:before { content: "\e9f9"; } .icon.logout:before { content: "\e9fa"; } .icon.folder_open:before { content: "\e9fb"; } .icon.launchopen_in_new:before { content: "\e9fc"; } .icon.open_in_browser:before { content: "\e9fd"; } .icon.vue:before { content: "\e9fe"; color: #4fc08d; } .icon.angularuniversal:before { content: "\e9ff"; color: #00acc1; } .icon.linkinsert_link:before { content: "\ea00"; } .icon.document-text2:before { content: "\ea01"; } .icon.document-text2-outline:before { content: "\ea02"; } .icon.document-text4:before { content: "\ea03"; } .icon.document-text5:before { content: "\ea04"; } .icon.folder4:before { content: "\ea05"; } .icon.shift:before { content: "\ea06"; } .icon.replace:before { content: "\ea07"; } .icon.replace_all:before { content: "\ea08"; } .icon.moveline-up:before { content: "\ea09"; } .icon.moveline-down:before { content: "\ea0a"; } .icon.copyline-up:before { content: "\ea0b"; } .icon.copyline-down:before { content: "\ea0c"; } .icon.acode:before { content: "\ea0d"; color: #3499fe; } .icon.patreon:before { content: "\ea0f"; color: #f96854; } .icon.paypal:before { content: "\ea10"; color: #00457c; } .icon.ruby:before { content: "\ea11"; color: #cc342d; } .icon.font_download:before { content: "\ea12"; } .icon.notes:before { content: "\ea13"; } .icon.http:before { content: "\ea14"; } .icon.compare_arrows:before { content: "\ea15"; } .icon.home_filled:before { content: "\ea16"; } .icon.height:before { content: "\ea17"; } .icon.all_inclusive:before { content: "\ea18"; } ================================================ FILE: src/settings/appSettings.js ================================================ 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 fonts from "lib/fonts"; import lang from "lib/lang"; import openFile from "lib/openFile"; import appSettings from "lib/settings"; import FontManager from "pages/fontManager"; import QuickToolsSettings from "pages/quickTools"; import encodings, { getEncoding } from "utils/encodings"; import helpers from "utils/helpers"; import Url from "utils/Url"; export default function otherSettings() { const values = appSettings.value; const title = strings["app settings"].capitalize(); const appFontText = strings["app font"] || "App font"; const appFontInfo = strings["settings-info-app-font-family"] || "Choose the font used across the app interface."; const defaultFontLabel = strings.default || "Default"; const categories = { interface: strings["settings-category-interface"], fonts: strings["settings-category-fonts"], filesSessions: strings["settings-category-files-sessions"], advanced: strings["settings-category-advanced"], }; const items = [ { key: "lang", text: strings["change language"], value: values.lang, select: lang.list, valueText: (value) => lang.getName(value), info: strings["settings-info-app-language"], category: categories.interface, }, { key: "animation", text: strings.animation, value: values.animation, valueText: (value) => strings[value], select: [ ["no", strings.no], ["yes", strings.yes], ["system", strings.system], ], info: strings["settings-info-app-animation"], category: categories.interface, }, { key: "fullscreen", text: strings.fullscreen.capitalize(), checkbox: values.fullscreen, info: strings["settings-info-app-fullscreen"], category: categories.interface, }, { key: "keyboardMode", text: strings["keyboard mode"], value: values.keyboardMode, valueText(mode) { return strings[mode.replace(/_/g, " ").toLocaleLowerCase()]; }, select: [ [appSettings.KEYBOARD_MODE_NORMAL, strings.normal], [appSettings.KEYBOARD_MODE_NO_SUGGESTIONS, strings["no suggestions"]], [ appSettings.KEYBOARD_MODE_NO_SUGGESTIONS_AGGRESSIVE, strings["no suggestions aggressive"], ], ], info: strings["settings-info-app-keyboard-mode"], category: categories.interface, }, { key: "vibrateOnTap", text: strings["vibrate on tap"], checkbox: values.vibrateOnTap, info: strings["settings-info-app-vibrate-on-tap"], category: categories.interface, }, { key: "floatingButton", text: strings["floating button"], checkbox: values.floatingButton, info: strings["settings-info-app-floating-button"], category: categories.interface, }, { key: "showSideButtons", text: strings["show side buttons"], checkbox: values.showSideButtons, info: strings["settings-info-app-side-buttons"], category: categories.interface, }, { key: "showSponsorSidebarApp", text: `${strings.sponsor} (${strings.sidebar})`, checkbox: values.showSponsorSidebarApp, info: strings["settings-info-app-sponsor-sidebar"], category: categories.interface, }, { key: "openFileListPos", text: strings["active files"], value: values.openFileListPos, valueText: (value) => strings[value], select: [ [appSettings.OPEN_FILE_LIST_POS_SIDEBAR, strings.sidebar], [appSettings.OPEN_FILE_LIST_POS_HEADER, strings.header], [appSettings.OPEN_FILE_LIST_POS_BOTTOM, strings.bottom], ], info: strings["settings-info-app-open-file-list-position"], category: categories.interface, }, { key: "quickTools", text: strings["quick tools"], checkbox: !!values.quickTools, info: strings["info-quickTools"], category: categories.interface, }, { key: "quickToolsTriggerMode", text: strings["quicktools trigger mode"], value: values.quickToolsTriggerMode, select: [ [appSettings.QUICKTOOLS_TRIGGER_MODE_CLICK, "click"], [appSettings.QUICKTOOLS_TRIGGER_MODE_TOUCH, "touch"], ], info: strings["settings-info-app-quick-tools-trigger-mode"], category: categories.interface, }, { key: "quickToolsSettings", text: strings["shortcut buttons"], info: strings["settings-info-app-quick-tools-settings"], category: categories.interface, chevron: true, }, { key: "touchMoveThreshold", text: strings["touch move threshold"], value: values.touchMoveThreshold, prompt: strings["touch move threshold"], promptType: "number", promptOptions: { test(value) { return value >= 0; }, }, info: strings["settings-info-app-touch-move-threshold"], category: categories.interface, }, { key: "appFont", text: appFontText, value: values.appFont || "", valueText: (value) => value || defaultFontLabel, get select() { return [["", defaultFontLabel], ...fonts.getNames()]; }, info: appFontInfo, category: categories.fonts, }, { key: "fontManager", text: strings["fonts"], info: strings["settings-info-app-font-manager"], category: categories.fonts, chevron: true, }, { key: "rememberFiles", text: strings["remember opened files"], checkbox: values.rememberFiles, info: strings["settings-info-app-remember-files"], category: categories.filesSessions, }, { key: "rememberFolders", text: strings["remember opened folders"], checkbox: values.rememberFolders, info: strings["settings-info-app-remember-folders"], category: categories.filesSessions, }, { key: "retryRemoteFsAfterFail", text: strings["retry ftp/sftp when fail"], checkbox: values.retryRemoteFsAfterFail, info: strings["settings-info-app-retry-remote-fs"], category: categories.filesSessions, }, { key: "excludeFolders", text: strings["exclude files"], value: values.excludeFolders.join("\n"), prompt: strings["exclude files"], promptType: "textarea", promptOptions: { test(value) { return value.split("\n").every((item) => { return item.trim().length > 0; }); }, }, info: strings["settings-info-app-exclude-folders"], category: categories.filesSessions, }, { key: "defaultFileEncoding", text: strings["default file encoding"], value: values.defaultFileEncoding, valueText: (value) => value === "auto" ? strings.auto || "Auto" : getEncoding(value).label, select: [ ["auto", strings.auto || "Auto"], ...Object.keys(encodings).map((id) => { const encoding = encodings[id]; return [id, encoding.label]; }), ], info: strings["settings-info-app-default-file-encoding"], category: categories.filesSessions, }, { key: "keybindings", text: strings["key bindings"], info: strings["settings-info-app-keybindings"], category: categories.advanced, chevron: true, }, { key: "confirmOnExit", text: strings["confirm on exit"], checkbox: values.confirmOnExit, info: strings["settings-info-app-confirm-on-exit"], category: categories.advanced, }, { key: "checkFiles", text: strings["check file changes"], checkbox: values.checkFiles, info: strings["settings-info-app-check-files"], category: categories.advanced, }, { key: "checkForAppUpdates", text: strings["check for app updates"], checkbox: values.checkForAppUpdates, info: strings["info-checkForAppUpdates"], category: categories.advanced, }, { key: "console", text: strings.console, value: values.console, select: [appSettings.CONSOLE_LEGACY, appSettings.CONSOLE_ERUDA], info: strings["settings-info-app-console"], category: categories.advanced, }, { key: "developerMode", text: strings["developer mode"], checkbox: values.developerMode, info: strings["info-developermode"], category: categories.advanced, }, { key: "cleanInstallState", text: strings["clean install state"], info: strings["settings-info-app-clean-install-state"], category: categories.advanced, chevron: true, }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", infoAsDescription: true, valueInTail: true, }); async function callback(key, value) { switch (key) { case "keybindings": { value = await select(strings["key bindings"], [ ["edit", strings.edit], ["reset", strings.reset], ]); if (!value) return; if (value === "edit") { actionStack.pop(2); openFile(KEYBINDING_FILE); } else { resetKeyBindings(); } return; } case "quickToolsSettings": QuickToolsSettings(); return; case "fontManager": FontManager(); return; case "appFont": await fonts.setAppFont(value); break; case "console": { if (value !== "eruda") { break; } const fs = fsOperation(Url.join(DATA_STORAGE, "eruda.js")); if (await fs.exists()) { break; } loader.create( strings["downloading file"].replace("{file}", "eruda.js"), strings["downloading..."], ); try { const erudaScript = await ajax({ url: constants.ERUDA_CDN, responseType: "text", contentType: "application/x-www-form-urlencoded", }); await fsOperation(DATA_STORAGE).createFile("eruda.js", erudaScript); loader.destroy(); } catch (error) { helpers.error(error); } break; } case "developerMode": { if (value) { const devTools = (await import("lib/devTools")).default; try { await devTools.init(true); toast( strings["developer mode enabled"] || "Developer mode enabled. Use command palette to toggle inspector.", ); } catch (error) { helpers.error(error); value = false; } } else { const devTools = (await import("lib/devTools")).default; devTools.destroy(); toast( strings["developer mode disabled"] || "Developer mode disabled", ); } break; } case "cleanInstallState": { const INSTALL_STATE_STORAGE = Url.join(DATA_STORAGE, ".install-state"); const fs = fsOperation(INSTALL_STATE_STORAGE); if (!(await fs.exists())) { toast(strings["no such file or directory"]); break; } loader.create("loading..."); try { await fs.delete(); loader.destroy(); toast(strings["success"]); } catch (error) { helpers.error(error); loader.destroy(); } } case "rememberFiles": if (!value) { delete localStorage.files; } break; case "rememberFolders": if (!value) { delete localStorage.folders; } break; case "floatingButton": root.classList.toggle("hide-floating-button"); break; case "keyboardMode": system.setInputType(value); break; case "fullscreen": if (value) acode.exec("enable-fullscreen"); else acode.exec("disable-fullscreen"); break; case "quickTools": if (value) { value = 1; actions("set-height", 1); } else { value = 0; actions("set-height", 0); } break; case "excludeFolders": value = value .split("\n") .map((item) => item.trim()) .filter((item) => item.length > 0); break; default: break; } appSettings.update({ [key]: value, }); } } ================================================ FILE: src/settings/backupRestore.js ================================================ import fsOperation from "fileSystem"; import settingsPage from "components/settingsPage"; 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 appSettings from "lib/settings"; import FileBrowser from "pages/fileBrowser"; import helpers from "utils/helpers"; import Uri from "utils/Uri"; import Url from "utils/Url"; // Backup format version for future compatibility const BACKUP_VERSION = 2; /** * CRC32 lookup table for checksum calculation */ const CRC32_TABLE = (() => { const table = new Uint32Array(256); for (let i = 0; i < 256; i++) { let crc = i; for (let j = 0; j < 8; j++) { crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1; } table[i] = crc >>> 0; } return table; })(); /** * Generates a CRC32 checksum for data integrity verification * More robust than simple hash for detecting corrupted data * @param {string} data * @returns {string} */ function generateChecksum(data) { let crc = 0xffffffff; for (let i = 0; i < data.length; i++) { const byte = data.charCodeAt(i) & 0xff; crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); } return ((crc ^ 0xffffffff) >>> 0).toString(16).padStart(8, "0"); } /** * Validates the structure of a backup object * Supports both v1 (legacy) and v2 (new) backup formats * @param {object} backup * @returns {{valid: boolean, errors: string[], warnings: string[], isLegacy: boolean}} */ function validateBackupStructure(backup) { const errors = []; const warnings = []; if (!backup || typeof backup !== "object") { errors.push(strings["backup not valid object"]); return { valid: false, errors, warnings, isLegacy: false }; } // Determine if this is a legacy (v1) backup const isLegacy = !backup.version; if (isLegacy) { // Legacy backup (v1) - just needs settings or installedPlugins to be valid const hasData = backup.settings || backup.keyBindings || backup.installedPlugins; if (!hasData) { errors.push(strings["backup no data"]); } warnings.push(strings["backup legacy warning"]); } else { // Version 2+ backup if (backup.version >= 2) { if (!backup.metadata) { warnings.push(strings["backup missing metadata"]); } // Verify checksum if present if (backup.checksum) { try { const dataToCheck = JSON.stringify({ settings: backup.settings, keyBindings: backup.keyBindings, installedPlugins: backup.installedPlugins, }); const expectedChecksum = generateChecksum(dataToCheck); if (backup.checksum !== expectedChecksum) { warnings.push(strings["backup checksum mismatch"]); } } catch (e) { warnings.push(strings["backup checksum verify failed"]); } } } } // Validate settings (both versions) if (backup.settings !== undefined && typeof backup.settings !== "object") { errors.push(strings["backup invalid settings"]); } // Validate keyBindings (both versions) if ( backup.keyBindings !== undefined && typeof backup.keyBindings !== "object" ) { errors.push(strings["backup invalid keybindings"]); } // Validate installedPlugins (both versions) if ( backup.installedPlugins !== undefined && !Array.isArray(backup.installedPlugins) ) { errors.push(strings["backup invalid plugins"]); } return { valid: errors.length === 0, errors, warnings, isLegacy }; } /** * Formats a date for backup filename * @param {Date} date * @returns {string} */ function formatDateForFilename(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); return `${year}${month}${day}_${hours}${minutes}`; } function backupRestore() { const title = strings.backup.capitalize() + "/" + strings.restore.capitalize(); const items = [ { key: "backup", text: strings.backup.capitalize(), icon: "file_downloadget_app", chevron: true, }, { key: "restore", text: strings.restore.capitalize(), icon: "historyrestore", chevron: true, }, { note: strings["backup/restore note"], }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", groupByDefault: true, }); function callback(key) { switch (key) { case "backup": backup(); return; case "restore": restore(); return; default: break; } } async function backup() { const loaderDialog = loader.create( strings.backup.capitalize(), strings["preparing backup"], ); try { loaderDialog.show(); loaderDialog.setMessage(strings["collecting settings"]); const settings = appSettings.value; // Read keybindings with fallback loaderDialog.setMessage(strings["collecting key bindings"]); let keyBindings = null; try { const keybindingsFS = fsOperation(KEYBINDING_FILE); if (await keybindingsFS.exists()) { keyBindings = await keybindingsFS.readFile("json"); } } catch (error) { console.warn("Could not read keybindings:", error); } // Collect plugin information loaderDialog.setMessage(strings["collecting plugins"]); const installedPlugins = []; const pluginDetails = []; try { const pluginsDir = fsOperation(window.PLUGIN_DIR); if (await pluginsDir.exists()) { const plugins = await pluginsDir.lsDir(); for (const plugin of plugins) { try { const pluginJsonPath = Url.join( window.PLUGIN_DIR, plugin.name, "plugin.json", ); const pluginJson = await fsOperation(pluginJsonPath).readFile("json"); // Store detailed plugin info for better restore const pluginInfo = { id: pluginJson.id || plugin.name, name: pluginJson.name || plugin.name, version: pluginJson.version || "unknown", source: pluginJson.source || null, }; pluginDetails.push(pluginInfo); if (pluginJson.source) { installedPlugins.push(pluginJson.source); } else { installedPlugins.push(pluginJson.id || plugin.name); } } catch (error) { // Fallback to just plugin name installedPlugins.push(plugin.name); pluginDetails.push({ id: plugin.name, name: plugin.name, version: "unknown", source: null, }); } } } } catch (error) { console.warn("Could not read plugins directory:", error); } loaderDialog.hide(); // Get destination folder const { url } = await FileBrowser("folder", strings["select folder"]); loaderDialog.show(); loaderDialog.setMessage( strings["creating backup"] || "Creating backup file...", ); const timestamp = formatDateForFilename(new Date()); const backupFilename = `Acode_backup_${timestamp}.backup`; const backupDirname = "Backup"; const backupDir = Url.join(url, backupDirname); const backupFile = Url.join(backupDir, backupFilename); const backupStorageFS = fsOperation(url); const backupDirFS = fsOperation(backupDir); const backupFileFS = fsOperation(backupFile); // Create backup directory if needed if (!(await backupDirFS.exists())) { await backupStorageFS.createDirectory(backupDirname); } // Create backup file if (!(await backupFileFS.exists())) { await backupDirFS.createFile(backupFilename); } // Prepare backup data with checksum const backupData = { settings, keyBindings, installedPlugins, }; const checksum = generateChecksum(JSON.stringify(backupData)); const backupObject = { version: BACKUP_VERSION, metadata: { createdAt: new Date().toISOString(), appVersion: BuildInfo?.version || "unknown", pluginCount: installedPlugins.length, hasSettings: !!settings, hasKeyBindings: !!keyBindings, }, checksum, settings, keyBindings, installedPlugins, pluginDetails, // Extra detail for better restoration info }; const backupString = JSON.stringify(backupObject, null, 2); await backupFileFS.writeFile(backupString); loaderDialog.destroy(); const message = [ strings["backup successful"], `
          ${Uri.getVirtualAddress(backupFile)}
          `, `${strings.settings || "Settings"}: ✓`, `${strings["key bindings"] || "Key Bindings"}: ${keyBindings ? "✓" : "-"}`, `${strings.plugins || "Plugins"}: ${installedPlugins.length}`, ].join("
          "); alert(strings.success.toUpperCase(), message); } catch (error) { loaderDialog.destroy(); console.error("Backup error:", error); alert( strings.error.toUpperCase(), `${strings["error details"] || "Error"}: ${error.message || error}`, ); } } function restore() { sdcard.openDocumentFile( (data) => { backupRestore.restore(data.uri); }, (error) => { console.error("File picker error:", error); toast(strings.error || "Error selecting file"); }, "application/octet-stream", ); } } backupRestore.restore = async function (url) { const loaderDialog = loader.create( strings.restore.capitalize(), strings["please wait..."], ); const restoreResults = { settings: { attempted: false, success: false, error: null }, keyBindings: { attempted: false, success: false, error: null }, plugins: { attempted: false, success: [], failed: [], skipped: [] }, }; try { loaderDialog.show(); // Read and parse backup file const fs = fsOperation(url); let backupContent; try { backupContent = await fs.readFile("utf8"); } catch (error) { throw new Error(`Could not read backup file: ${error.message}`); } let backup; try { backup = JSON.parse(backupContent); } catch (error) { loaderDialog.destroy(); alert(strings.error.toUpperCase(), strings["invalid backup file"]); return; } // Validate backup structure loaderDialog.setMessage(strings["validating backup"]); const validation = validateBackupStructure(backup); if (!validation.valid) { loaderDialog.destroy(); const errorMessage = [ strings["invalid backup file"], "", `${strings["issues found"]}:`, ...validation.errors.map((e) => `• ${e}`), ].join("\n"); alert(strings.error.toUpperCase(), errorMessage); return; } loaderDialog.hide(); // Show backup info and ask for confirmation const backupInfo = backup.metadata || {}; const confirmParts = [ `${strings["restore will include"]}

          `, `${strings.settings}: ${backup.settings ? strings.yes : strings.no}
          `, `${strings["key bindings"]}: ${backup.keyBindings ? strings.yes : strings.no}
          `, `${strings.plugins}: ${backup.installedPlugins?.length || 0}
          `, ]; if (backupInfo.createdAt) { confirmParts.push( `
          ${strings["last modified"]}: ${new Date(backupInfo.createdAt).toLocaleString()}
          `, ); } // Show warnings if any (legacy backup, checksum issues, etc.) if (validation.warnings && validation.warnings.length > 0) { confirmParts.push(`
          ${strings.warning}:
          `); for (const warning of validation.warnings) { confirmParts.push(`- ${warning}
          `); } } confirmParts.push(`
          ${strings["restore warning"]}`); const shouldContinue = await confirm( strings.restore.capitalize(), confirmParts.join(""), true, ); if (!shouldContinue) { return; } // What to restore - restore everything available const selectedOptions = []; if (backup.settings) selectedOptions.push("settings"); if (backup.keyBindings) selectedOptions.push("keyBindings"); if (backup.installedPlugins?.length) selectedOptions.push("plugins"); loaderDialog.show(); // Restore key bindings first (before settings, in case of reload) if (selectedOptions.includes("keyBindings") && backup.keyBindings) { restoreResults.keyBindings.attempted = true; loaderDialog.setMessage(strings["restoring key bindings"]); try { const keybindingsFS = fsOperation(window.KEYBINDING_FILE); // Ensure file exists if (!(await keybindingsFS.exists())) { const parentDir = fsOperation(DATA_STORAGE); await parentDir.createFile(".key-bindings.json"); } const text = JSON.stringify(backup.keyBindings, undefined, 2); await keybindingsFS.writeFile(text); restoreResults.keyBindings.success = true; } catch (error) { console.error("Error restoring key bindings:", error); restoreResults.keyBindings.error = error.message; } } // Restore plugins if ( selectedOptions.includes("plugins") && Array.isArray(backup.installedPlugins) && backup.installedPlugins.length > 0 ) { restoreResults.plugins.attempted = true; const { default: installPlugin } = await import("lib/installPlugin"); const totalPlugins = backup.installedPlugins.length; let currentPlugin = 0; for (const id of backup.installedPlugins) { currentPlugin++; if (!id) { restoreResults.plugins.skipped.push({ id: "(empty)", reason: "Empty plugin ID", }); continue; } const pluginName = backup.pluginDetails?.find((p) => p.id === id || p.source === id) ?.name || id; loaderDialog.setMessage( `${strings["restoring plugins"]} (${currentPlugin}/${totalPlugins}): ${pluginName}`, ); try { if ( id.startsWith("content://") || id.startsWith("file://") || id.includes("/") ) { // Local plugin case try { // Check if the source file still exists const sourceFS = fsOperation(id); if (!(await sourceFS.exists())) { restoreResults.plugins.skipped.push({ id: pluginName, reason: strings["source not found"], }); continue; } await installPlugin(id); restoreResults.plugins.success.push(pluginName); } catch (error) { restoreResults.plugins.failed.push({ id: pluginName, reason: error.message, }); } } else { // Remote plugin case - fetch from API const pluginUrl = Url.join(constants.API_BASE, `plugin/${id}`); let remotePlugin = null; try { remotePlugin = await fsOperation(pluginUrl).readFile("json"); } catch (error) { restoreResults.plugins.failed.push({ id: pluginName, reason: strings["plugin not found"], }); continue; } if (remotePlugin) { let purchaseToken = null; const isPaid = Number.parseFloat(remotePlugin.price) > 0; if (isPaid) { try { const [product] = await helpers.promisify(iap.getProducts, [ remotePlugin.sku, ]); if (product) { const purchases = await helpers.promisify(iap.getPurchases); const purchase = purchases.find((p) => p.productIds.includes(product.productId), ); purchaseToken = purchase?.purchaseToken; } if (!purchaseToken) { restoreResults.plugins.skipped.push({ id: pluginName, reason: strings["paid plugin skipped"], }); continue; } } catch (error) { restoreResults.plugins.skipped.push({ id: pluginName, reason: `Paid plugin - ${error.message}`, }); continue; } } try { await installPlugin(id, remotePlugin.name, purchaseToken); restoreResults.plugins.success.push(pluginName); } catch (error) { restoreResults.plugins.failed.push({ id: pluginName, reason: error.message, }); } } } } catch (error) { console.error(`Error restoring plugin ${id}:`, error); restoreResults.plugins.failed.push({ id: pluginName, reason: error.message, }); } } } // Restore settings last (may trigger reload) if (selectedOptions.includes("settings") && backup.settings) { restoreResults.settings.attempted = true; loaderDialog.setMessage(strings["restoring settings"]); try { // Validate and merge settings carefully const currentSettings = appSettings.value; const restoredSettings = backup.settings; // Only restore known settings keys to prevent issues with outdated backups const validSettings = {}; for (const key of Object.keys(currentSettings)) { if (key in restoredSettings) { // Type check before applying if (typeof restoredSettings[key] === typeof currentSettings[key]) { validSettings[key] = restoredSettings[key]; } } } await appSettings.update(validSettings, false); restoreResults.settings.success = true; } catch (error) { console.error("Error restoring settings:", error); restoreResults.settings.error = error.message; } } loaderDialog.destroy(); // Build restore summary const summaryParts = [ `${strings["restore completed"]}

          `, ]; if (restoreResults.settings.attempted) { const status = restoreResults.settings.success ? `✓ ${strings.restored}` : `✗ ${strings.failed}`; summaryParts.push(`${strings.settings}: ${status}
          `); } if (restoreResults.keyBindings.attempted) { const status = restoreResults.keyBindings.success ? `✓ ${strings.restored}` : `✗ ${strings.failed}`; summaryParts.push(`${strings["key bindings"]}: ${status}
          `); } if (restoreResults.plugins.attempted) { summaryParts.push(`
          ${strings.plugins}
          `); if (restoreResults.plugins.success.length > 0) { summaryParts.push( `✓ ${strings.restored}: ${restoreResults.plugins.success.length}
          `, ); } if (restoreResults.plugins.failed.length > 0) { summaryParts.push( `✗ ${strings.failed}: ${restoreResults.plugins.failed.length}
          `, ); for (const f of restoreResults.plugins.failed.slice(0, 3)) { summaryParts.push(`- ${f.id}: ${f.reason}
          `); } if (restoreResults.plugins.failed.length > 3) { summaryParts.push( `...${strings.more || "and"} ${restoreResults.plugins.failed.length - 3} ${strings.more || "more"}
          `, ); } } if (restoreResults.plugins.skipped.length > 0) { summaryParts.push( `${strings.skipped}: ${restoreResults.plugins.skipped.length}
          `, ); for (const s of restoreResults.plugins.skipped.slice(0, 3)) { summaryParts.push(`- ${s.id}: ${s.reason}
          `); } if (restoreResults.plugins.skipped.length > 3) { summaryParts.push( `...${strings.more} ${restoreResults.plugins.skipped.length - 3} ${strings.more}
          `, ); } } } summaryParts.push(`
          ${strings["reload to apply"]}`); const shouldReload = await confirm( strings["restore completed"], summaryParts.join(""), true, ); // Reload only if user confirms if (shouldReload) { location.reload(); } } catch (err) { loaderDialog.destroy(); console.error("Restore error:", err); alert( strings.error.toUpperCase(), `${strings["error details"]}: ${err.message || err}`, ); } }; export default backupRestore; ================================================ FILE: src/settings/editorSettings.js ================================================ import settingsPage from "components/settingsPage"; import constants from "lib/constants"; import fonts from "lib/fonts"; import appSettings from "lib/settings"; import scrollSettings from "./scrollSettings"; export default function editorSettings() { const title = strings["editor settings"]; const values = appSettings.value; const categories = { scrolling: strings["settings-category-scrolling"], textLayout: strings["settings-category-text-layout"], editing: strings["settings-category-editing"], assistance: strings["settings-category-assistance"], guidesIndicators: strings["settings-category-guides-indicators"], cursorSelection: strings["settings-category-cursor-selection"], }; const items = [ { key: "scroll-settings", text: strings["scroll settings"], info: strings["settings-info-editor-scroll-settings"], category: categories.scrolling, chevron: true, }, { key: "editorFont", text: strings["editor font"], value: values.editorFont, get select() { return fonts.getNames(); }, info: strings["settings-info-editor-font-family"], category: categories.textLayout, }, { key: "fontSize", text: strings["font size"], value: values.fontSize, prompt: strings["font size"], promptOptions: { required: true, match: constants.FONT_SIZE, }, info: strings["settings-info-editor-font-size"], category: categories.textLayout, }, { key: "lineHeight", text: strings["line height"], value: values.lineHeight, prompt: strings["line height"], promptType: "number", promptOptions: { test(value) { value = Number.parseFloat(value); return value >= 1 && value <= 2; }, }, info: strings["settings-info-editor-line-height"], category: categories.textLayout, }, { key: "textWrap", text: strings["text wrap"], checkbox: values.textWrap, info: strings["settings-info-editor-text-wrap"], category: categories.textLayout, }, { key: "hardWrap", text: strings["hard wrap"], checkbox: values.hardWrap, info: strings["settings-info-editor-hard-wrap"], category: categories.textLayout, }, { key: "autosave", text: strings.autosave, value: values.autosave, valueText: (value) => (value ? value : strings.no), prompt: strings.delay + " (>=1000 || 0)", promptType: "number", promptOptions: { test(value) { value = Number.parseInt(value); return value >= 1000 || value === 0; }, }, info: strings["settings-info-editor-autosave"], category: categories.editing, }, { key: "softTab", text: strings["soft tab"], checkbox: values.softTab, info: strings["settings-info-editor-soft-tab"], category: categories.editing, }, { key: "tabSize", text: strings["tab size"], value: values.tabSize, prompt: strings["tab size"], promptType: "number", promptOptions: { test(value) { value = Number.parseInt(value); return value >= 1 && value <= 8; }, }, info: strings["settings-info-editor-tab-size"], category: categories.editing, }, { key: "formatOnSave", text: strings["format on save"], checkbox: values.formatOnSave, info: strings["settings-info-editor-format-on-save"], category: categories.editing, }, { key: "liveAutoCompletion", text: strings["live autocompletion"], checkbox: values.liveAutoCompletion, info: strings["settings-info-editor-live-autocomplete"], category: categories.assistance, }, { key: "colorPreview", text: strings["color preview"], checkbox: values.colorPreview, info: strings["settings-info-editor-color-preview"], category: categories.assistance, }, { key: "linenumbers", text: strings["show line numbers"], checkbox: values.linenumbers, info: strings["settings-info-editor-line-numbers"], category: categories.guidesIndicators, }, { key: "relativeLineNumbers", text: strings["relative line numbers"], checkbox: values.relativeLineNumbers, info: strings["settings-info-editor-relative-line-numbers"], category: categories.guidesIndicators, }, { key: "lintGutter", text: strings["lint gutter"] || "Show lint gutter", checkbox: values.lintGutter ?? true, info: strings["settings-info-editor-lint-gutter"], category: categories.guidesIndicators, }, { key: "showSpaces", text: strings["show spaces"], checkbox: values.showSpaces, info: strings["settings-info-editor-show-spaces"], category: categories.guidesIndicators, }, { key: "indentGuides", text: strings["indent guides"] || "Indent guides", checkbox: values.indentGuides ?? true, info: strings["settings-info-editor-indent-guides"], category: categories.guidesIndicators, }, { key: "rainbowBrackets", text: strings["rainbow brackets"] || "Rainbow brackets", checkbox: values.rainbowBrackets ?? true, info: strings["settings-info-editor-rainbow-brackets"], category: categories.guidesIndicators, }, { key: "fadeFoldWidgets", text: strings["fade fold widgets"], checkbox: values.fadeFoldWidgets, info: strings["settings-info-editor-fade-fold-widgets"], category: categories.guidesIndicators, }, { key: "shiftClickSelection", text: strings["shift click selection"], checkbox: values.shiftClickSelection, info: strings["settings-info-editor-shift-click-selection"], category: categories.cursorSelection, }, { key: "rtlText", text: strings["line based rtl switching"], checkbox: values.rtlText, info: strings["settings-info-editor-rtl-text"], category: categories.cursorSelection, }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", infoAsDescription: true, valueInTail: true, }); /** * Callback for settings page when an item is clicked * @param {string} key * @param {string} value */ function callback(key, value) { switch (key) { case "scroll-settings": appSettings.uiSettings[key].show(); return; case "editorFont": fonts.setFont(value); default: appSettings.update({ [key]: value, }); break; } } } ================================================ FILE: src/settings/filesSettings.js ================================================ import settingsPage from "components/settingsPage"; import appSettings from "lib/settings"; export default function filesSettings() { const title = strings.settings; const values = appSettings.value.fileBrowser; const items = [ { key: "sortByName", text: strings["sort by name"], checkbox: values.sortByName, }, { key: "showHiddenFiles", text: strings["show hidden files"], checkbox: values.showHiddenFiles, info: strings["info-showHiddenFiles"], }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", groupByDefault: true, }); function callback(key, value) { appSettings.value.fileBrowser[key] = value; appSettings.update(); } } ================================================ FILE: src/settings/formatterSettings.js ================================================ import { getModes } from "cm/modelist"; import settingsPage from "components/settingsPage"; import appSettings from "lib/settings"; import helpers from "utils/helpers"; export default function formatterSettings(languageName) { const title = strings.formatter; const values = appSettings.value; const { formatters } = acode; const languagesLabel = strings.languages || "Languages"; // Build items from CodeMirror modelist const items = getModes() .slice() .sort((a, b) => String(a.caption || a.name).localeCompare(String(b.caption || b.name)), ) .map((mode) => { const { name, caption, extensions } = mode; const formatterID = values.formatter[name] || null; // Only pass real extensions (skip anchored filename patterns like ^Dockerfile) const extList = String(extensions) .split("|") .filter((e) => e && !e.startsWith("^")); const options = acode.getFormatterFor(extList); const sampleExt = extList[0] || name; return { key: name, text: caption, icon: helpers.getIconForFile(`sample.${sampleExt}`), value: formatterID, valueText: (value) => { const formatter = formatters.find(({ id }) => id === value); if (formatter) { return formatter.name; } return strings.none; }, select: options, chevron: true, category: languagesLabel, }; }); items.unshift({ note: strings["settings-note-formatter-settings"], }); const page = settingsPage(title, items, callback, "separate", { preserveOrder: true, pageClassName: "detail-settings-page formatter-settings-page", listClassName: "detail-settings-list formatter-settings-list", notePosition: "top", }); page.show(languageName); function callback(key, value) { if (value === null) { // Delete the key when "none" is selected delete values.formatter[key]; } else { values.formatter[key] = value; } appSettings.update(); } } ================================================ FILE: src/settings/helpSettings.js ================================================ import settingsPage from "components/settingsPage"; import constants from "lib/constants"; export default function help() { const title = strings.help; const items = [ { key: "docs", text: strings.documentation, link: constants.DOCS_URL, chevron: true, }, { key: "help", text: strings.help, link: constants.TELEGRAM_URL, chevron: true, }, { key: "faqs", text: strings.faqs, link: `${constants.WEBSITE_URL}/faqs`, chevron: true, }, { key: "bug_report", text: strings.bug_report, link: `${constants.GITHUB_URL}/issues`, chevron: true, }, ]; const page = settingsPage(title, items, () => {}, "separate", { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", groupByDefault: true, }); page.show(); } ================================================ FILE: src/settings/lspConfigUtils.js ================================================ import lspApi from "cm/lsp/api"; import appSettings from "lib/settings"; function cloneLspSettings() { return JSON.parse(JSON.stringify(appSettings.value?.lsp || {})); } export function normalizeServerId(id) { return String(id || "") .trim() .toLowerCase(); } export function normalizeLanguages(value) { if (Array.isArray(value)) { return value .map((lang) => String(lang || "") .trim() .toLowerCase(), ) .filter(Boolean); } return String(value || "") .split(",") .map((lang) => lang.trim().toLowerCase()) .filter(Boolean); } export function getServerOverride(id) { return appSettings.value?.lsp?.servers?.[normalizeServerId(id)] || {}; } export function isCustomServer(id) { return getServerOverride(id).custom === true; } export async function updateServerConfig(serverId, partial) { const key = normalizeServerId(serverId); if (!key) { throw new Error("Server id is required"); } const current = cloneLspSettings(); current.servers = current.servers || {}; const nextServer = { ...(current.servers[key] || {}), }; Object.entries(partial || {}).forEach(([entryKey, value]) => { if (value === undefined) { delete nextServer[entryKey]; return; } nextServer[entryKey] = value; }); if (Object.keys(nextServer).length) { current.servers[key] = nextServer; } else { delete current.servers[key]; } await appSettings.update({ lsp: current }, false); } export async function upsertCustomServer(serverId, config) { const key = normalizeServerId(serverId); if (!key) { throw new Error("Server id is required"); } const existingServer = lspApi.servers.get(key); if (existingServer && getServerOverride(key).custom !== true) { throw new Error("A built-in server already uses this id"); } const languages = normalizeLanguages(config.languages); if (!languages.length) { throw new Error("At least one language id is required"); } const current = cloneLspSettings(); current.servers = current.servers || {}; const existing = current.servers[key] || {}; const hasTransport = Object.prototype.hasOwnProperty.call( config, "transport", ); const hasLauncher = Object.prototype.hasOwnProperty.call(config, "launcher"); const nextConfig = { ...existing, ...config, custom: true, label: config.label || existing.label || key, languages, transport: hasTransport ? config.transport : existing.transport || { kind: "websocket" }, launcher: hasLauncher ? config.launcher : existing.launcher, enabled: config.enabled !== false, }; const installKind = nextConfig.launcher?.install?.kind; if (installKind && installKind !== "shell") { const providedExecutable = nextConfig.launcher.install.binaryPath || nextConfig.launcher.install.executable; if (!providedExecutable) { throw new Error( "Managed installers must declare the executable path or command they provide", ); } } current.servers[key] = nextConfig; await appSettings.update({ lsp: current }, false); const definition = { id: key, label: nextConfig.label, languages, transport: nextConfig.transport, launcher: nextConfig.launcher, clientConfig: nextConfig.clientConfig, initializationOptions: nextConfig.initializationOptions, startupTimeout: nextConfig.startupTimeout, enabled: nextConfig.enabled !== false, }; lspApi.upsert(definition); return key; } export async function removeCustomServer(serverId) { const key = normalizeServerId(serverId); const current = cloneLspSettings(); current.servers = current.servers || {}; delete current.servers[key]; await appSettings.update({ lsp: current }, false); lspApi.servers.unregister(key); } ================================================ FILE: src/settings/lspServerDetail.js ================================================ import lspApi from "cm/lsp/api"; import { checkServerInstallation, getInstallCommand, getUninstallCommand, installServer, stopManagedServer, uninstallServer, } from "cm/lsp/serverLauncher"; import settingsPage from "components/settingsPage"; import toast from "components/toast"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import prompt from "dialogs/prompt"; import appSettings from "lib/settings"; import { getServerOverride, isCustomServer, removeCustomServer, updateServerConfig, } from "./lspConfigUtils"; function getFeatureItems() { return [ [ "ext_hover", "hover", strings["lsp-feature-hover"], strings["lsp-feature-hover-info"], ], [ "ext_completion", "completion", strings["lsp-feature-completion"], strings["lsp-feature-completion-info"], ], [ "ext_signature", "signature", strings["lsp-feature-signature"], strings["lsp-feature-signature-info"], ], [ "ext_diagnostics", "diagnostics", strings["lsp-feature-diagnostics"], strings["lsp-feature-diagnostics-info"], ], [ "ext_inlayHints", "inlayHints", strings["lsp-feature-inlay-hints"], strings["lsp-feature-inlay-hints-info"], ], [ "ext_formatting", "formatting", strings["lsp-feature-formatting"], strings["lsp-feature-formatting-info"], ], ]; } function fillTemplate(template, replacements) { return Object.entries(replacements).reduce( (result, [key, value]) => result.replaceAll(`{${key}}`, String(value)), String(template || ""), ); } function clone(value) { if (!value || typeof value !== "object") return value; return JSON.parse(JSON.stringify(value)); } function mergeLauncher(base, patch) { if (!base && !patch) return undefined; return { ...(base || {}), ...(patch || {}), bridge: { ...(base?.bridge || {}), ...(patch?.bridge || {}), }, install: { ...(base?.install || {}), ...(patch?.install || {}), }, }; } function isDirectWebSocketServer(server) { return server?.transport?.kind === "websocket" && !server?.launcher?.bridge; } function getMergedConfig(server) { const override = getServerOverride(server.id); return { ...server, enabled: override.enabled ?? server.enabled, startupTimeout: override.startupTimeout ?? server.startupTimeout, initializationOptions: { ...(server.initializationOptions || {}), ...(override.initializationOptions || {}), }, clientConfig: { ...(server.clientConfig || {}), ...(override.clientConfig || {}), builtinExtensions: { ...(server.clientConfig?.builtinExtensions || {}), ...(override.clientConfig?.builtinExtensions || {}), }, }, launcher: mergeLauncher(server.launcher, override.launcher), }; } function formatInstallStatus(result) { switch (result?.status) { case "present": return result.version ? fillTemplate(strings["lsp-status-installed-version"], { version: result.version, }) : strings["lsp-status-installed"]; case "missing": return strings["lsp-status-not-installed"]; case "failed": return strings["lsp-status-check-failed"]; default: return strings["lsp-status-unknown"]; } } function formatStartupTimeoutValue(timeout) { return typeof timeout === "number" ? fillTemplate(strings["lsp-timeout-ms"], { timeout }) : strings["lsp-default"]; } function sanitizeInstallMessage(message) { const lines = String(message || "") .split("\n") .map((line) => line.trim()) .filter(Boolean) .filter( (line) => !/^proot warning:/i.test(line) && !line.includes(`"/proc/self/fd/0"`) && !line.includes(`"/proc/self/fd/1"`) && !line.includes(`"/proc/self/fd/2"`), ); return lines.join(" "); } function formatInstallInfo(result) { const cleanedMessage = sanitizeInstallMessage(result?.message); switch (result?.status) { case "present": return result.version ? fillTemplate(strings["lsp-install-info-version-available"], { version: result.version, }) : strings["lsp-install-info-ready"]; case "missing": return strings["lsp-install-info-missing"]; case "failed": return cleanedMessage || strings["lsp-install-info-check-failed"]; default: return cleanedMessage || strings["lsp-install-info-unknown"]; } } function formatValue(value) { if (value === undefined || value === null || value === "") return ""; let text = String(value); if (text.includes("\n")) { [text] = text.split("\n"); } if (text.length > 47) { text = `${text.slice(0, 47)}...`; } return text; } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = String(text || ""); return div.innerHTML; } function updateItemDisplay($list, itemsByKey, key, value, extras = {}) { const item = itemsByKey.get(key); if (!item) return; if ("value" in extras) { item.value = extras.value; } else if (value !== undefined) { item.value = value; } if ("info" in extras) { item.info = extras.info; } if ("checkbox" in extras) { item.checkbox = extras.checkbox; } const $item = $list?.querySelector?.(`[data-key="${key}"]`); if (!$item) return; const $subtitle = $item.querySelector(".value"); if ($subtitle) { $subtitle.textContent = $subtitle.classList.contains("setting-info") ? String(item.info || "") : formatValue(item.value); } const $trailingValue = $item.querySelector(".setting-trailing-value"); if ($trailingValue) { $trailingValue.textContent = formatValue(item.value); } const $checkbox = $item.querySelector(".input-checkbox"); if ($checkbox && typeof item.checkbox === "boolean") { $checkbox.checked = item.checkbox; } } async function buildSnapshot(serverId) { const liveServer = lspApi.servers.get(serverId); if (!liveServer) return null; const merged = getMergedConfig(liveServer); const override = getServerOverride(serverId); const directWebSocket = isDirectWebSocketServer(merged); const installResult = directWebSocket ? { status: "unknown", version: null, canInstall: false, canUpdate: false, message: strings["lsp-websocket-server-managed-externally"], } : await checkServerInstallation(merged).catch((error) => ({ status: "failed", version: null, canInstall: true, canUpdate: true, message: error instanceof Error ? error.message : String(error), })); return { liveServer, merged, override, directWebSocket, isCustom: isCustomServer(serverId), installResult, builtinExts: merged.clientConfig?.builtinExtensions || {}, installCommand: getInstallCommand(merged, "install"), updateCommand: getInstallCommand(merged, "update"), uninstallCommand: getUninstallCommand(merged), }; } function createItems(snapshot) { const featureItems = getFeatureItems(); const categories = { general: strings["settings-category-general"], installation: strings["settings-category-installation"], advanced: strings["settings-category-advanced"], features: strings["settings-category-features"], }; const items = [ { key: "enabled", text: strings["lsp-enabled"], checkbox: snapshot.merged.enabled !== false, info: strings["settings-info-lsp-server-enabled"], category: categories.general, }, ...(snapshot.isCustom ? [ { key: "remove_custom_server", text: strings["lsp-remove-custom-server"], info: strings["settings-info-lsp-remove-custom-server"], category: categories.general, chevron: true, }, ] : []), { key: "startup_timeout", text: strings["lsp-startup-timeout"], value: formatStartupTimeoutValue(snapshot.merged.startupTimeout), info: strings["settings-info-lsp-startup-timeout"], category: categories.advanced, chevron: true, }, { key: "edit_init_options", text: strings["lsp-edit-initialization-options"], value: Object.keys(snapshot.override.initializationOptions || {}).length ? strings["lsp-configured"] : strings["lsp-empty"], info: strings["settings-info-lsp-edit-init-options"], category: categories.advanced, chevron: true, }, { key: "view_init_options", text: strings["lsp-view-initialization-options"], info: strings["settings-info-lsp-view-init-options"], category: categories.advanced, chevron: true, }, ]; if (!snapshot.directWebSocket) { items.splice( 1, 0, { key: "install_status", text: strings["lsp-installed"], value: formatInstallStatus(snapshot.installResult), info: formatInstallInfo(snapshot.installResult), category: categories.installation, chevron: true, }, { key: "install_server", text: strings["lsp-install-repair"], info: strings["settings-info-lsp-install-server"], category: categories.installation, chevron: true, }, { key: "update_server", text: strings["lsp-update-server"], info: strings["settings-info-lsp-update-server"], category: categories.installation, chevron: true, }, { key: "uninstall_server", text: strings["lsp-uninstall-server"], info: strings["settings-info-lsp-uninstall-server"], category: categories.installation, chevron: true, }, ); } featureItems.forEach(([key, extKey, text, info]) => { items.push({ key, text, checkbox: isBuiltinFeatureEnabled(snapshot.builtinExts, extKey), info, category: categories.features, }); }); return items; } async function refreshVisibleState($list, itemsByKey, serverId) { if (!$list) return; const snapshot = await buildSnapshot(serverId); if (!snapshot) return; updateItemDisplay($list, itemsByKey, "enabled", undefined, { checkbox: snapshot.merged.enabled !== false, }); updateItemDisplay( $list, itemsByKey, "install_status", formatInstallStatus(snapshot.installResult), { info: formatInstallInfo(snapshot.installResult), }, ); updateItemDisplay($list, itemsByKey, "install_server", ""); updateItemDisplay($list, itemsByKey, "update_server", ""); updateItemDisplay($list, itemsByKey, "uninstall_server", ""); updateItemDisplay( $list, itemsByKey, "startup_timeout", formatStartupTimeoutValue(snapshot.merged.startupTimeout), ); updateItemDisplay( $list, itemsByKey, "edit_init_options", Object.keys(snapshot.override.initializationOptions || {}).length ? strings["lsp-configured"] : strings["lsp-empty"], ); getFeatureItems().forEach(([key, extKey]) => { updateItemDisplay($list, itemsByKey, key, undefined, { checkbox: isBuiltinFeatureEnabled(snapshot.builtinExts, extKey), }); }); } function isBuiltinFeatureEnabled(builtinExts, extKey) { if (extKey === "inlayHints") { return builtinExts?.[extKey] === true; } return builtinExts?.[extKey] !== false; } async function persistEnabled(serverId, value) { await updateServerConfig(serverId, { enabled: value }); lspApi.servers.update(serverId, (current) => ({ ...current, enabled: value, })); } async function persistClientConfig(serverId, clientConfig) { await updateServerConfig(serverId, { clientConfig }); lspApi.servers.update(serverId, (current) => ({ ...current, clientConfig: { ...(current.clientConfig || {}), ...clientConfig, }, })); } async function persistStartupTimeout(serverId, timeout) { await updateServerConfig(serverId, { startupTimeout: timeout }); lspApi.servers.update(serverId, (current) => ({ ...current, startupTimeout: timeout, })); } async function persistInitOptions(serverId, value) { await updateServerConfig(serverId, { initializationOptions: value }); lspApi.servers.update(serverId, (current) => ({ ...current, initializationOptions: value, })); } export default function lspServerDetail(serverId) { const initialServer = lspApi.servers.get(serverId); if (!initialServer) { toast(strings["lsp-server-not-found"]); return null; } const initialSnapshot = { liveServer: initialServer, merged: getMergedConfig(initialServer), override: getServerOverride(serverId), directWebSocket: isDirectWebSocketServer(getMergedConfig(initialServer)), isCustom: isCustomServer(serverId), installResult: isDirectWebSocketServer(getMergedConfig(initialServer)) ? { status: "unknown", version: null, canInstall: false, canUpdate: false, message: strings["lsp-websocket-server-managed-externally"], } : { status: "unknown", version: null, canInstall: true, canUpdate: true, message: strings["lsp-checking-installation-status"], }, builtinExts: getMergedConfig(initialServer).clientConfig?.builtinExtensions || {}, installCommand: getInstallCommand( getMergedConfig(initialServer), "install", ), updateCommand: getInstallCommand(getMergedConfig(initialServer), "update"), uninstallCommand: getUninstallCommand(getMergedConfig(initialServer)), }; const items = createItems(initialSnapshot); const itemsByKey = new Map(items.map((item) => [item.key, item])); const page = settingsPage( initialServer.label || initialServer.id, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", valueInTail: true, }, ); const baseShow = page.show.bind(page); return { ...page, show(goTo) { baseShow(goTo); const $list = document.querySelector("#settings .main.list"); refreshVisibleState($list, itemsByKey, serverId).catch(console.error); }, }; async function callback(key, value) { const $list = this?.parentElement; const snapshot = await buildSnapshot(serverId); if (!snapshot) { toast(strings["lsp-server-not-found"]); return; } switch (key) { case "enabled": await persistEnabled(serverId, value); if (!value) { stopManagedServer(serverId); } toast( value ? strings["lsp-server-enabled-toast"] : strings["lsp-server-disabled-toast"], ); break; case "remove_custom_server": if ( !(await confirm( strings["lsp-remove-custom-server"], fillTemplate(strings["lsp-remove-custom-server-confirm"], { server: snapshot.liveServer.label || serverId, }), )) ) { break; } stopManagedServer(serverId); await removeCustomServer(serverId); toast(strings["lsp-custom-server-removed"]); page.hide(); appSettings.uiSettings["lsp-settings"]?.show(); return; case "install_status": { const result = await checkServerInstallation(snapshot.merged); const lines = [ fillTemplate(strings["lsp-status-line"], { status: formatInstallStatus(result), }), result.version ? fillTemplate(strings["lsp-version-line"], { version: result.version, }) : null, fillTemplate(strings["lsp-details-line"], { details: formatInstallInfo(result), }), ].filter(Boolean); alert(strings["lsp-installation-status"], lines.join("
          ")); break; } case "install_server": if (!snapshot.installCommand) { toast(strings["lsp-install-command-unavailable"]); break; } await installServer(snapshot.merged, "install"); break; case "update_server": if (!snapshot.updateCommand) { toast(strings["lsp-update-command-unavailable"]); break; } await installServer(snapshot.merged, "update"); break; case "uninstall_server": if (!snapshot.uninstallCommand) { toast(strings["lsp-uninstall-command-unavailable"]); break; } if ( !(await confirm( strings["lsp-uninstall-server"], fillTemplate(strings["lsp-remove-installed-files"], { server: snapshot.liveServer.label || serverId, }), )) ) { break; } await uninstallServer(snapshot.merged, { promptConfirm: false }); toast(strings["lsp-server-uninstalled"]); break; case "startup_timeout": { const currentTimeout = snapshot.override.startupTimeout ?? snapshot.liveServer.startupTimeout ?? 5000; const result = await prompt( strings["lsp-startup-timeout-ms"], String(currentTimeout), "number", { test: (val) => { const timeout = Number.parseInt(String(val), 10); return Number.isFinite(timeout) && timeout >= 1000; }, }, ); if (result === null) { break; } const timeout = Number.parseInt(String(result), 10); if (!Number.isFinite(timeout) || timeout < 1000) { toast(strings["lsp-invalid-timeout"]); break; } await persistStartupTimeout(serverId, timeout); toast( fillTemplate(strings["lsp-startup-timeout-set"], { timeout, }), ); break; } case "edit_init_options": { const currentJson = JSON.stringify( snapshot.override.initializationOptions || {}, null, 2, ); const result = await prompt( strings["lsp-initialization-options-json"], currentJson || "{}", "textarea", { test: (val) => { try { JSON.parse(val); return true; } catch { return false; } }, }, ); if (result === null) { break; } await persistInitOptions(serverId, JSON.parse(result)); toast(strings["lsp-initialization-options-updated"]); break; } case "view_init_options": { const json = JSON.stringify( snapshot.merged.initializationOptions || {}, null, 2, ); alert( strings["lsp-initialization-options"], `

          ${escapeHtml(json)}
          `, ); break; } case "ext_hover": case "ext_completion": case "ext_signature": case "ext_diagnostics": case "ext_inlayHints": case "ext_formatting": { const extKey = key.replace("ext_", ""); const feature = getFeatureItems().find( ([featureKey]) => featureKey === key, ); const currentClientConfig = clone(snapshot.override.clientConfig || {}); const currentBuiltins = currentClientConfig.builtinExtensions || {}; await persistClientConfig(serverId, { ...currentClientConfig, builtinExtensions: { ...currentBuiltins, [extKey]: value, }, }); toast( fillTemplate(strings["lsp-feature-state-toast"], { feature: feature?.[2] || extKey, state: value ? strings["lsp-state-enabled"] : strings["lsp-state-disabled"], }), ); break; } default: break; } await refreshVisibleState($list, itemsByKey, serverId); } } ================================================ FILE: src/settings/lspSettings.js ================================================ import { quoteArg } from "cm/lsp/installRuntime"; import serverRegistry from "cm/lsp/serverRegistry"; import settingsPage from "components/settingsPage"; import toast from "components/toast"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import { getServerOverride, normalizeLanguages, normalizeServerId, upsertCustomServer, } from "./lspConfigUtils"; import lspServerDetail from "./lspServerDetail"; function parseArgsInput(value) { const normalized = String(value || "").trim(); if (!normalized) return []; const parsed = JSON.parse(normalized); if (!Array.isArray(parsed)) { throw new Error(strings["lsp-error-args-must-be-array"]); } return parsed.map((entry) => String(entry)); } function normalizePackages(value) { return String(value || "") .split(",") .map((entry) => entry.trim()) .filter(Boolean); } function getInstallMethods() { return [ { value: "manual", text: strings["lsp-install-method-manual"] }, { value: "apk", text: strings["lsp-install-method-apk"] }, { value: "npm", text: strings["lsp-install-method-npm"] }, { value: "pip", text: strings["lsp-install-method-pip"] }, { value: "cargo", text: strings["lsp-install-method-cargo"] }, { value: "shell", text: strings["lsp-install-method-shell"] }, ]; } function getTransportMethods() { return [ { value: "stdio", text: strings["lsp-transport-method-stdio"] || "STDIO (launch a binary command)", }, { value: "websocket", text: strings["lsp-transport-method-websocket"] || "WebSocket (connect to a ws/wss URL)", }, ]; } function parseWebSocketUrl(value) { const normalized = String(value || "").trim(); if (!normalized) { throw new Error( strings["lsp-error-websocket-url-required"] || "WebSocket URL is required", ); } if (!/^wss?:\/\//i.test(normalized)) { throw new Error( strings["lsp-error-websocket-url-invalid"] || "WebSocket URL must start with ws:// or wss://", ); } return normalized; } function buildDefaultCheckCommand(binaryCommand, installer) { const executable = String( installer?.binaryPath || installer?.executable || binaryCommand || "", ).trim(); if (!executable) return ""; if (installer?.kind === "manual" && installer?.binaryPath) { return `test -x ${quoteArg(installer.binaryPath)}`; } if (executable.includes("/")) { return `test -x ${quoteArg(executable)}`; } return `which ${quoteArg(executable)}`; } async function promptInstaller(binaryCommand) { const method = await select( strings["lsp-install-method-title"], getInstallMethods(), ); if (!method) return null; switch (method) { case "manual": { const binaryPath = await prompt( strings["lsp-binary-path-optional"], String(binaryCommand || "").includes("/") ? String(binaryCommand) : "", "text", ); if (binaryPath === null) return null; return { kind: "manual", source: "manual", executable: String(binaryCommand || "").trim() || undefined, binaryPath: String(binaryPath || "").trim() || undefined, }; } case "apk": case "npm": case "pip": case "cargo": { const packagesInput = await prompt( strings["lsp-packages-prompt"].replace( "{method}", method.toUpperCase(), ), "", "text", ); if (packagesInput === null) return null; const packages = normalizePackages(packagesInput); if (!packages.length) { throw new Error(strings["lsp-error-package-required"]); } return { kind: method, source: method, executable: String(binaryCommand || "").trim() || undefined, packages, }; } case "shell": { const installCommand = await prompt( strings["lsp-install-command"], "", "textarea", ); if (installCommand === null) return null; const updateCommand = await prompt( strings["lsp-update-command-optional"], String(installCommand || ""), "textarea", ); if (updateCommand === null) return null; return { kind: "shell", source: "custom", executable: String(binaryCommand || "").trim() || undefined, command: String(installCommand || "").trim() || undefined, updateCommand: String(updateCommand || "").trim() || undefined, }; } default: return null; } } /** * LSP Settings page - shows list of all language servers * @returns {object} Settings page interface */ export default function lspSettings() { const title = strings?.lsp_settings || strings["language servers"] || "Language Servers"; const categories = { customServers: strings["settings-category-custom-servers"], servers: strings["settings-category-servers"], }; let page = createPage(); return { show(goTo) { page = createPage(); page.show(goTo); }, hide() { page.hide(); }, search(key) { page = createPage(); return page.search(key); }, restoreList() { page.restoreList(); }, setTitle(nextTitle) { page.setTitle(nextTitle); }, }; function createPage() { const servers = serverRegistry.listServers(); const sortedServers = servers.sort((a, b) => { const aEnabled = getServerOverride(a.id).enabled ?? a.enabled; const bEnabled = getServerOverride(b.id).enabled ?? b.enabled; if (aEnabled !== bEnabled) { return bEnabled ? 1 : -1; } return a.label.localeCompare(b.label); }); const items = [ { key: "add_custom_server", text: strings["lsp-add-custom-server"], info: strings["settings-info-lsp-add-custom-server"], category: categories.customServers, index: 0, chevron: true, }, ]; for (const server of sortedServers) { const source = server.launcher?.install?.source ? ` • ${server.launcher.install.source}` : ""; const languagesList = Array.isArray(server.languages) && server.languages.length ? `${server.languages.join(", ")}${source}` : source.slice(3); items.push({ key: `server:${server.id}`, text: server.label, info: languagesList || undefined, category: categories.servers, chevron: true, }); } items.push({ note: strings["settings-note-lsp-settings"], }); return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", groupByDefault: true, }); } function refreshVisiblePage() { page.hide(); page = createPage(); page.show(); } async function callback(key) { if (key === "add_custom_server") { try { const idInput = await prompt(strings["lsp-server-id"], "", "text"); if (idInput === null) return; const serverId = normalizeServerId(idInput); if (!serverId) { toast(strings["lsp-error-server-id-required"]); return; } const label = await prompt( strings["lsp-server-label"], serverId, "text", ); if (label === null) return; const languageInput = await prompt( strings["lsp-language-ids"], "", "text", ); if (languageInput === null) return; const languages = normalizeLanguages(languageInput); if (!languages.length) { toast(strings["lsp-error-language-id-required"]); return; } const transportKind = await select( strings.type || "Type", getTransportMethods(), ); if (!transportKind) return; let transport; let launcher; if (transportKind === "websocket") { const websocketUrlInput = await prompt( strings["lsp-websocket-url"] || "WebSocket URL", "ws://127.0.0.1:3000/", "text", { test: (value) => { try { parseWebSocketUrl(value); return true; } catch { return false; } }, }, ); if (websocketUrlInput === null) return; transport = { kind: "websocket", url: parseWebSocketUrl(websocketUrlInput), }; } else { const binaryCommand = await prompt( strings["lsp-binary-command"], "", "text", ); if (binaryCommand === null) return; if (!String(binaryCommand).trim()) { toast(strings["lsp-error-binary-command-required"]); return; } const argsInput = await prompt( strings["lsp-binary-args"], "[]", "textarea", { test: (value) => { try { parseArgsInput(value); return true; } catch { return false; } }, }, ); if (argsInput === null) return; const parsedArgs = parseArgsInput(argsInput); const installer = await promptInstaller(binaryCommand); if (installer === null) return; const defaultCheckCommand = buildDefaultCheckCommand( binaryCommand, installer, ); const checkCommand = await prompt( strings["lsp-check-command-optional"], defaultCheckCommand, "text", { placeholder: defaultCheckCommand || "which my-language-server", }, ); if (checkCommand === null) return; transport = { kind: "stdio", command: String(binaryCommand).trim(), args: parsedArgs, }; launcher = { bridge: { kind: "axs", command: String(binaryCommand).trim(), args: parsedArgs, }, checkCommand: String(checkCommand || "").trim() || undefined, install: installer, }; } await upsertCustomServer(serverId, { label: String(label || "").trim() || serverId, languages, transport, launcher, enabled: true, }); toast(strings["lsp-custom-server-added"]); refreshVisiblePage(); const detailPage = lspServerDetail(serverId); detailPage?.show(); } catch (error) { toast( error instanceof Error ? error.message : strings["lsp-error-add-server-failed"], ); } return; } if (key.startsWith("server:")) { const id = key.split(":")[1]; const detailPage = lspServerDetail(id); if (detailPage) { detailPage.show(); } } } } ================================================ FILE: src/settings/mainSettings.js ================================================ import settingsPage from "components/settingsPage"; import confirm from "dialogs/confirm"; import rateBox from "dialogs/rateBox"; import actionStack from "lib/actionStack"; import openFile from "lib/openFile"; import removeAds from "lib/removeAds"; import appSettings from "lib/settings"; import settings from "lib/settings"; import openAdRewardsPage from "pages/adRewards"; import Changelog from "pages/changelog/changelog"; import plugins from "pages/plugins"; import Sponsors from "pages/sponsors"; import themeSetting from "pages/themeSetting"; import helpers from "utils/helpers"; import About from "../pages/about"; import otherSettings from "./appSettings"; import backupRestore from "./backupRestore"; import editorSettings from "./editorSettings"; import filesSettings from "./filesSettings"; import formatterSettings from "./formatterSettings"; import lspSettings from "./lspSettings"; import previewSettings from "./previewSettings"; import scrollSettings from "./scrollSettings"; import searchSettings from "./searchSettings"; import terminalSettings from "./terminalSettings"; export default function mainSettings() { const title = strings.settings.capitalize(); const categories = { core: strings["settings-category-core"], customizationTools: strings["settings-category-customization-tools"], maintenance: strings["settings-category-maintenance"], aboutAcode: strings["settings-category-about-acode"], supportAcode: strings["settings-category-support-acode"], }; const items = [ { key: "app-settings", text: strings["app settings"], icon: "tune", info: strings["settings-info-main-app-settings"], category: categories.core, chevron: true, }, { key: "editor-settings", text: strings["editor settings"], icon: "text_format", info: strings["settings-info-main-editor-settings"], category: categories.core, chevron: true, }, { key: "terminal-settings", text: `${strings["terminal settings"]}`, icon: "terminal", info: strings["settings-info-main-terminal-settings"], category: categories.core, chevron: true, }, { key: "preview-settings", text: strings["preview settings"], icon: "public", info: strings["settings-info-main-preview-settings"], category: categories.core, chevron: true, }, { key: "formatter", text: strings.formatter, icon: "spellcheck", info: strings["settings-info-main-formatter"], category: categories.customizationTools, chevron: true, }, { key: "theme", text: strings.theme, icon: "color_lenspalette", info: strings["settings-info-main-theme"], category: categories.customizationTools, chevron: true, }, { key: "plugins", text: strings["plugins"], icon: "extension", info: strings["settings-info-main-plugins"], category: categories.customizationTools, chevron: true, }, { key: "lsp-settings", text: strings?.lsp_settings || strings["language servers"] || "Language servers", icon: "zap", info: strings["settings-info-main-lsp-settings"], category: categories.customizationTools, chevron: true, }, { key: "backup-restore", text: `${strings.backup.capitalize()} & ${strings.restore.capitalize()}`, icon: "cached", info: strings["settings-info-main-backup-restore"], category: categories.maintenance, chevron: true, }, { key: "editSettings", text: `${strings["edit"]} settings.json`, icon: "edit", info: strings["settings-info-main-edit-settings"], category: categories.maintenance, chevron: true, }, { key: "reset", text: strings["restore default settings"], icon: "historyrestore", info: strings["settings-info-main-reset"], category: categories.maintenance, chevron: true, }, { key: "about", text: strings.about, icon: "info", info: `Version ${BuildInfo.version}`, category: categories.aboutAcode, chevron: true, }, { key: "sponsors", text: strings.sponsor, icon: "favorite", info: strings["settings-info-main-sponsors"], category: categories.aboutAcode, chevron: true, }, { key: "changeLog", text: `${strings["changelog"]}`, icon: "update", info: strings["settings-info-main-changelog"], category: categories.aboutAcode, chevron: true, }, { key: "rateapp", text: strings["rate acode"], icon: "star_outline", info: strings["settings-info-main-rateapp"], category: categories.aboutAcode, chevron: true, }, ]; if (IS_FREE_VERSION) { items.push({ key: "adRewards", text: strings["earn ad-free time"], icon: "play_arrow", info: strings["settings-info-main-ad-rewards"], category: categories.supportAcode, chevron: true, }); items.push({ key: "removeads", text: strings["remove ads"], icon: "block", info: strings["settings-info-main-remove-ads"], category: categories.supportAcode, chevron: true, }); } /** * Callback for settings page for handling click event * @this {HTMLElement} * @param {string} key */ async function callback(key) { switch (key) { case "app-settings": case "backup-restore": case "editor-settings": case "preview-settings": case "terminal-settings": case "lsp-settings": appSettings.uiSettings[key].show(); break; case "theme": themeSetting(); break; case "about": About(); break; case "sponsors": Sponsors(); break; case "rateapp": rateBox(); break; case "plugins": plugins(); break; case "adRewards": openAdRewardsPage(); break; case "formatter": formatterSettings(); break; case "editSettings": { actionStack.pop(); openFile(settings.settingsFile); break; } case "reset": const confirmation = await confirm( strings.warning, strings["restore default settings"], ); if (confirmation) { await appSettings.reset(); location.reload(); } break; case "removeads": try { await removeAds(); this.remove(); } catch (error) { helpers.error(error); } break; case "changeLog": Changelog(); break; default: break; } } const page = settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "main-settings-page", listClassName: "main-settings-list", }); page.show(); appSettings.uiSettings["main-settings"] = page; appSettings.uiSettings["app-settings"] = otherSettings(); appSettings.uiSettings["file-settings"] = filesSettings(); appSettings.uiSettings["backup-restore"] = backupRestore(); appSettings.uiSettings["editor-settings"] = editorSettings(); appSettings.uiSettings["scroll-settings"] = scrollSettings(); appSettings.uiSettings["search-settings"] = searchSettings(); appSettings.uiSettings["preview-settings"] = previewSettings(); appSettings.uiSettings["terminal-settings"] = terminalSettings(); appSettings.uiSettings["lsp-settings"] = lspSettings(); } ================================================ FILE: src/settings/previewSettings.js ================================================ import settingsPage from "components/settingsPage"; import appSettings from "lib/settings"; export default function previewSettings() { const values = appSettings.value; const title = strings["preview settings"]; const categories = { server: strings["settings-category-server"], preview: strings["settings-category-preview"], }; const PORT_REGEX = /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; const items = [ { key: "previewPort", text: strings["preview port"], value: values.previewPort, prompt: strings["preview port"], promptType: "number", promptOptions: { test(value) { return PORT_REGEX.test(value); }, }, info: strings["settings-info-preview-preview-port"], category: categories.server, }, { key: "serverPort", text: strings["server port"], value: values.serverPort, prompt: strings["server port"], promptType: "number", promptOptions: { test(value) { return PORT_REGEX.test(value); }, }, info: strings["settings-info-preview-server-port"], category: categories.server, }, { key: "previewMode", text: strings["preview mode"], value: values.previewMode, select: [ [appSettings.PREVIEW_MODE_BROWSER, strings.browser], [appSettings.PREVIEW_MODE_INAPP, strings.inapp], ], info: strings["settings-info-preview-mode"], category: categories.server, }, { key: "host", text: strings.host, value: values.host, prompt: strings.host, promptType: "text", promptOptions: { test(value) { try { new URL(`http://${value}:${values.previewPort}`); return true; } catch (error) { return false; } }, }, info: strings["settings-info-preview-host"], category: categories.server, }, { key: "disableCache", text: strings["disable in-app-browser caching"], checkbox: values.disableCache, info: strings["settings-info-preview-disable-cache"], category: categories.preview, }, { key: "useCurrentFileForPreview", text: strings["should_use_current_file_for_preview"], checkbox: !!values.useCurrentFileForPreview, info: strings["settings-info-preview-use-current-file"], category: categories.preview, }, { key: "showConsoleToggler", text: strings["show console toggler"], checkbox: values.showConsoleToggler, info: strings["settings-info-preview-show-console-toggler"], category: categories.preview, }, { note: strings["preview settings note"], }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", infoAsDescription: true, valueInTail: true, }); function callback(key, value) { appSettings.update({ [key]: value, }); } } ================================================ FILE: src/settings/scrollSettings.js ================================================ import settingsPage from "components/settingsPage"; import constants from "lib/constants"; import appSettings from "lib/settings"; export default function scrollSettings() { const values = appSettings.value; const title = strings["scroll settings"]; const items = [ /*{ key: "scrollSpeed", text: strings["scroll speed"], value: values.scrollSpeed, valueText: getScrollSpeedString, select: [ [constants.SCROLL_SPEED_FAST_X2, `${strings.fast} x2`], [constants.SCROLL_SPEED_FAST, strings.fast], [constants.SCROLL_SPEED_NORMAL, strings.normal], [constants.SCROLL_SPEED_SLOW, strings.slow], ], },*/ /*{ key: "reverseScrolling", text: strings["reverse scrolling"], checkbox: values.reverseScrolling, },*/ /*{ key: "diagonalScrolling", text: strings["diagonal scrolling"], checkbox: values.diagonalScrolling, },*/ { key: "scrollbarSize", text: strings["scrollbar size"], value: values.scrollbarSize, valueText: (size) => `${size}px`, select: [5, 10, 15, 20], }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", infoAsDescription: true, valueInTail: true, groupByDefault: true, }); function callback(key, value) { appSettings.update({ [key]: value, }); } } function getScrollSpeedString(speed) { switch (speed) { case constants.SCROLL_SPEED_FAST: return strings.fast; case constants.SCROLL_SPEED_SLOW: return strings.slow; case constants.SCROLL_SPEED_FAST_X2: return `${strings.fast} x2`; case constants.SCROLL_SPEED_NORMAL: return strings.normal; default: return strings.normal; } } ================================================ FILE: src/settings/searchSettings.js ================================================ import settingsPage from "../components/settingsPage"; import appSettings from "../lib/settings"; export default function searchSettings() { const title = strings.search; const values = appSettings.value.search; const items = [ { key: "caseSensitive", text: strings["case sensitive"], checkbox: values.caseSensitive, }, { key: "regExp", text: strings["regular expression"], checkbox: values.regExp, }, { key: "wholeWord", text: strings["whole word"], checkbox: values.wholeWord, }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", groupByDefault: true, }); function callback(key, value) { values[key] = value; appSettings.update(); } } ================================================ FILE: src/settings/terminalSettings.js ================================================ import fsOperation from "fileSystem"; import settingsPage from "components/settingsPage"; import { DEFAULT_TERMINAL_SETTINGS, TerminalThemeManager, } from "components/terminal"; import toast from "components/toast"; import alert from "dialogs/alert"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import fonts from "lib/fonts"; import appSettings from "lib/settings"; import FileBrowser from "pages/fileBrowser"; import helpers from "utils/helpers"; export default function terminalSettings() { const title = strings["terminal settings"]; const values = appSettings.value; const categories = { permissions: strings["settings-category-permissions"], display: strings["settings-category-display"], cursor: strings["settings-category-cursor"], session: strings["settings-category-session"], maintenance: strings["settings-category-maintenance"], }; // Initialize terminal settings with defaults if not present if (!values.terminalSettings) { values.terminalSettings = { ...DEFAULT_TERMINAL_SETTINGS, fontFamily: DEFAULT_TERMINAL_SETTINGS.fontFamily || appSettings.value.fontFamily, }; } const terminalValues = values.terminalSettings; const items = [ { key: "all_file_access", text: strings["allFileAccess"], info: strings["info-all_file_access"], category: categories.permissions, chevron: true, }, { key: "fontSize", text: strings["font size"], value: terminalValues.fontSize, prompt: strings["font size"], promptType: "number", promptOptions: { test(value) { value = Number.parseInt(value); return value >= 8 && value <= 32; }, }, info: strings["info-fontSize"], category: categories.display, }, { key: "fontFamily", text: strings["terminal:font family"], value: terminalValues.fontFamily, get select() { return fonts.getNames(); }, info: strings["info-fontFamily"], category: categories.display, }, { key: "theme", text: strings["theme"], value: terminalValues.theme, info: strings["info-theme"], get select() { return TerminalThemeManager.getThemeNames().map((name) => [ name, name.charAt(0).toUpperCase() + name.slice(1), ]); }, valueText(value) { const option = this.select.find(([v]) => v === value); return option ? option[1] : value; }, category: categories.display, }, { key: "fontWeight", text: strings["terminal:font weight"], value: terminalValues.fontWeight, select: [ "normal", "bold", "100", "200", "300", "400", "500", "600", "700", "800", "900", ], info: strings["info-fontWeight"], category: categories.display, }, { key: "letterSpacing", text: strings["letter spacing"], value: terminalValues.letterSpacing, prompt: strings["letter spacing"], promptType: "number", info: strings["info-letterSpacing"], category: categories.display, }, { key: "fontLigatures", text: strings["font ligatures"], checkbox: terminalValues.fontLigatures, info: strings["info-fontLigatures"], category: categories.display, }, { key: "cursorStyle", text: strings["terminal:cursor style"], value: terminalValues.cursorStyle, select: ["block", "underline", "bar"], info: strings["info-cursorStyle"], category: categories.cursor, }, { key: "cursorInactiveStyle", text: strings["terminal:cursor inactive style"], value: terminalValues.cursorInactiveStyle, select: ["outline", "block", "bar", "underline", "none"], info: strings["info-cursorInactiveStyle"], category: categories.cursor, }, { key: "cursorBlink", text: strings["terminal:cursor blink"], checkbox: terminalValues.cursorBlink, info: strings["info-cursorBlink"], category: categories.cursor, }, { key: "scrollback", text: strings["terminal:scrollback"], value: terminalValues.scrollback, prompt: strings["terminal:scrollback"], promptType: "number", promptOptions: { test(value) { value = Number.parseInt(value); return value >= 100 && value <= 10000; }, }, info: strings["info-scrollback"], category: categories.session, }, { key: "tabStopWidth", text: strings["terminal:tab stop width"], value: terminalValues.tabStopWidth, prompt: strings["terminal:tab stop width"], promptType: "number", promptOptions: { test(value) { value = Number.parseInt(value); return value >= 1 && value <= 8; }, }, info: strings["info-tabStopWidth"], category: categories.session, }, { key: "convertEol", text: strings["terminal:convert eol"], checkbox: terminalValues.convertEol, info: strings["settings-info-terminal-convert-eol"], category: categories.session, }, { key: "imageSupport", text: strings["terminal:image support"], checkbox: terminalValues.imageSupport, info: strings["info-imageSupport"], category: categories.session, }, { key: "confirmTabClose", text: strings["terminal:confirm tab close"], checkbox: terminalValues.confirmTabClose !== false, info: strings["info-confirmTabClose"], category: categories.session, }, { key: "backup", text: strings.backup, info: strings["info-backup"], category: categories.maintenance, chevron: true, }, { key: "restore", text: strings.restore, info: strings["info-restore"], category: categories.maintenance, chevron: true, }, { key: "uninstall", text: strings.uninstall, info: strings["info-uninstall"], category: categories.maintenance, chevron: true, }, ]; return settingsPage(title, items, callback, undefined, { preserveOrder: true, pageClassName: "detail-settings-page", listClassName: "detail-settings-list", infoAsDescription: true, valueInTail: true, }); /** * Callback for settings page when an item is clicked * @param {string} key * @param {string} value */ async function callback(key, value) { switch (key) { case "all_file_access": if (ANDROID_SDK_INT >= 30) { system.isManageExternalStorageDeclared((boolStr) => { if (boolStr === "true") { system.requestStorageManager(console.log, console.error); } else { alert(strings["feature not available"]); } }, alert); } else { alert(strings["feature not available"]); } return; case "backup": terminalBackup(); return; case "restore": terminalRestore(); return; case "uninstall": const confirmation = await confirm( strings.confirm, "Are you sure you want to uninstall the terminal?", ); if (confirmation) { loader.showTitleLoader(); Terminal.uninstall() .then(() => { loader.removeTitleLoader(); alert( strings.success.toUpperCase(), "Terminal uninstalled successfully.", ); }) .catch((error) => { loader.removeTitleLoader(); console.error("Terminal uninstall failed:", error); helpers.error(error); }); } return; default: appSettings.update({ terminalSettings: { ...values.terminalSettings, [key]: value, }, }); // Update any active terminal instances updateActiveTerminals(key, value); break; } } /** * Creates a backup of the terminal installation */ async function terminalBackup() { try { // Ask user to select backup location const { url } = await FileBrowser("folder", strings["select folder"]); loader.showTitleLoader(); // Create backup const backupPath = await Terminal.backup(); await system.copyToUri( backupPath, url, "aterm_backup.tar", console.log, console.error, ); loader.removeTitleLoader(); alert(strings.success.toUpperCase(), `${strings["backup successful"]}.`); } catch (error) { loader.removeTitleLoader(); console.error("Terminal backup failed:", error); toast(error.toString()); } } /** * Restores terminal installation */ async function terminalRestore() { try { await Executor.execute("rm -rf $PREFIX/aterm_backup.*"); sdcard.openDocumentFile( async (data) => { loader.showTitleLoader(); //this will create a file at $PREFIX/atem_backup.tar.tar await system.copyToUri( data.uri, cordova.file.dataDirectory, "aterm_backup.tar", console.log, console.error, ); // Restore await Terminal.restore(); //Cleanup restore file await Executor.execute("rm -rf $PREFIX/aterm_backup.*"); loader.removeTitleLoader(); alert( strings.success.toUpperCase(), "Terminal restored successfully", ); }, toast, "application/x-tar", ); } catch (error) { loader.removeTitleLoader(); console.error("Terminal restore failed:", error); toast(error.toString()); } } } /** * Update active terminal instances with new settings * @param {string} key * @param {any} value */ export async function updateActiveTerminals(key, value) { // Find all terminal tabs and update their settings const terminalTabs = editorManager.files.filter( (file) => file.type === "terminal", ); terminalTabs.forEach(async (tab) => { if (tab.terminalComponent) { const terminalOptions = {}; switch (key) { case "fontSize": tab.terminalComponent.terminal.options.fontSize = value; break; case "fontFamily": // Load font if it's not already loaded try { await fonts.loadFont(value); } catch (error) { console.warn(`Failed to load font ${value}:`, error); } tab.terminalComponent.terminal.options.fontFamily = value; tab.terminalComponent.terminal.refresh( 0, tab.terminalComponent.terminal.rows - 1, ); break; case "fontWeight": tab.terminalComponent.terminal.options.fontWeight = value; break; case "cursorBlink": tab.terminalComponent.terminal.options.cursorBlink = value; break; case "cursorStyle": tab.terminalComponent.terminal.options.cursorStyle = value; break; case "cursorInactiveStyle": tab.terminalComponent.terminal.options.cursorInactiveStyle = value; break; case "scrollback": tab.terminalComponent.terminal.options.scrollback = value; break; case "tabStopWidth": tab.terminalComponent.terminal.options.tabStopWidth = value; break; case "convertEol": tab.terminalComponent.terminal.options.convertEol = value; break; case "letterSpacing": tab.terminalComponent.terminal.options.letterSpacing = value; break; case "theme": tab.terminalComponent.terminal.options.theme = TerminalThemeManager.getTheme(value); // Update container background to match new theme if (tab.terminalComponent.container) { tab.terminalComponent.container.style.background = tab.terminalComponent.terminal.options.theme.background; } break; case "imageSupport": tab.terminalComponent.updateImageSupport(value); break; case "fontLigatures": tab.terminalComponent.updateFontLigatures(value); break; } } }); } ================================================ FILE: src/sidebarApps/extensions/index.js ================================================ 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 InstallState from "lib/installState"; import loadPlugin from "lib/loadPlugin"; import settings from "lib/settings"; import FileBrowser from "pages/fileBrowser"; import plugin from "pages/plugin"; import helpers from "utils/helpers"; import Url from "utils/Url"; /** @type {HTMLElement} */ let $installed = null; /** @type {HTMLElement} */ let $explore = null; /** @type {HTMLElement} */ let container = null; /** @type {HTMLElement} */ let $searchResult = null; const LIMIT = 50; let currentPage = 1; let hasMore = true; 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}`; } const $header = (
          {strings.plugins}
          ); const $style = ; /** @type {Set} */ const $scrollableLists = new Set(); let searchTimeout = null; let installedPlugins = []; export default [ "extension", // icon "extensions", // id strings.plugins, // title initApp, // init function false, // prepend onSelected, // onSelected function ]; /** * On selected handler for files app * @param {HTMLElement} el */ function onSelected(el) { const $scrollableLists = container.getAll(":scope .scroll[data-scroll-top]"); for (const $el of $scrollableLists) { $el.scrollTop = $el.dataset.scrollTop; } } /** * Initialize extension app * @param {HTMLElement} el */ function initApp(el) { container = el; container.classList.add("extensions"); container.content = $header; if (!$searchResult) { $searchResult =
            ; container.append($searchResult); } if (!$explore) { $explore = collapsableList(strings.explore); $explore.ontoggle = loadExplore; $explore.$ul.onscroll = handleScroll; container.append($explore); } if (!$installed) { $installed = collapsableList(strings.installed); $installed.ontoggle = loadInstalled; container.append($installed); } Sidebar.on("show", onSelected); document.head.append($style); } async function handleScroll(e) { if (isLoading || !hasMore) return; const { scrollTop, scrollHeight, clientHeight } = e.target; if (scrollTop + clientHeight >= scrollHeight - 50) { await loadMorePlugins(); } } async function handleFilterScroll(e) { if (isFilterLoading || !filterHasMore || !currentFilter) return; const { scrollTop, scrollHeight, clientHeight } = e.target; if (scrollTop + clientHeight >= scrollHeight - 50) { await loadFilteredPlugins(currentFilter, false); } } async function loadMorePlugins() { try { isLoading = true; startLoading($explore); const response = await fetch( withSupportedEditor( `${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, ), ); const newPlugins = await response.json(); if (newPlugins.length < LIMIT) { hasMore = false; } installedPlugins = await listInstalledPlugins(); const pluginElements = newPlugins.map(ListItem); $explore.$ul.append(...pluginElements); currentPage++; updateHeight($explore); } catch (error) { window.log("error", error); } finally { isLoading = false; stopLoading($explore); } } async function loadFilteredPlugins(filterState, isInitial = false) { if (isFilterLoading || !filterHasMore || !filterState) return; try { isFilterLoading = true; const { items, hasMore } = await getFilteredPlugins(filterState); if (currentFilter !== filterState) { return; } installedPlugins = await listInstalledPlugins(); const pluginElements = items.map(ListItem); if (pluginElements.length) { $searchResult.append(...pluginElements); } else if (isInitial) { $searchResult.append( {strings["no plugins found"] || strings.empty || "No plugins found"} , ); } filterHasMore = hasMore; if (!filterHasMore) { $searchResult.onscroll = null; } updateHeight($searchResult); } catch (error) { window.log("error", "Error loading filtered plugins:"); window.log("error", error); } finally { isFilterLoading = false; } } async function searchPlugin() { clearTimeout(searchTimeout); searchTimeout = setTimeout(async () => { // Clear filter when searching currentFilter = null; filterHasMore = true; isFilterLoading = false; $searchResult.onscroll = null; $searchResult.content = ""; const status = await helpers.checkAPIStatus(); if (!status) { $searchResult.content = ( {strings.api_error} ); return; } const query = this.value; if (!query) return; try { $searchResult.classList.add("loading"); const plugins = await fsOperation( withSupportedEditor( Url.join(constants.API_BASE, `plugins?name=${query}`), ), ).readFile("json"); installedPlugins = await listInstalledPlugins(); $searchResult.content = plugins.map(ListItem); updateHeight($searchResult); } catch (error) { window.log("error", error); $searchResult.content = {strings.error}; } finally { $searchResult.classList.remove("loading"); } }, 500); } async function filterPlugins() { const verifiedLabel = strings["verified publisher"]; const authorLabel = strings.author || strings.name; const keywordsLabel = strings.keywords; const filterItems = [ { value: "orderBy:top_rated", text: strings.top_rated }, { value: "orderBy:newest", text: strings.newly_added }, { value: "orderBy:downloads", text: strings.most_downloaded }, { value: "attribute:verified", text: verifiedLabel }, { value: "attribute:author", text: authorLabel }, { value: "attribute:keywords", text: keywordsLabel }, ]; const filterConfig = { "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", baseLabel: verifiedLabel, value: true, }, "attribute:author": { type: "author", baseLabel: authorLabel }, "attribute:keywords": { type: "keywords", baseLabel: keywordsLabel }, }; const selection = await select("Filter", filterItems); if (!selection) return; const option = filterConfig[selection]; if (!option) return; const filterState = { ...option, nextPage: 1, buffer: [], hasMoreSource: true, 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}`; } currentFilter = filterState; filterHasMore = true; isFilterLoading = false; $searchResult.content = ""; try { $searchResult.classList.add("loading"); const filterMessage = (
            {strings["filtered by"]} {filterState.displayLabel} clearFilter()} />
            ); $searchResult.content = [filterMessage]; $searchResult.onscroll = handleFilterScroll; await loadFilteredPlugins(currentFilter, true); updateHeight($searchResult); function clearFilter() { currentFilter = null; filterHasMore = true; isFilterLoading = false; $searchResult.content = ""; $searchResult.onscroll = null; updateHeight($searchResult); } } catch (error) { window.log("error", "Error filtering plugins:"); window.log("error", error); $searchResult.content = {strings.error}; } finally { $searchResult.classList.remove("loading"); } } async function addSource(sourceType, value = "https://") { if (!sourceType) { const sourceOption = [ ["remote", strings.remote], ["local", strings.local], ]; sourceType = await select("Select Source", sourceOption); } if (!sourceType) return; 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 { const { default: installPlugin } = await import("lib/installPlugin"); await installPlugin(source); if (!$explore.collapsed) { $explore.ontoggle(); } if (!$installed.collapsed) { $installed.ontoggle(); } } catch (error) { console.error(error); window.toast(helpers.errorMessage(error)); addSource(sourceType, source); } } async function loadInstalled() { if (this.collapsed) return; const plugins = await listInstalledPlugins(); if (!plugins.length) { $installed.collapse(); } $installed.$ul.content = plugins.map(ListItem); updateHeight($installed); } async function loadExplore() { if (this.collapsed) return; const status = await helpers.checkAPIStatus(); if (!status) { $explore.$ul.content = {strings.api_error}; return; } try { startLoading($explore); currentPage = 1; hasMore = true; const response = await fetch( withSupportedEditor( `${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`, ), ); const plugins = await response.json(); if (plugins.length < LIMIT) { hasMore = false; } installedPlugins = await listInstalledPlugins(); $explore.$ul.content = plugins.map(ListItem); currentPage++; updateHeight($explore); } catch (error) { console.error("Failed to load plugins in sidebar explore:", error); $explore.$ul.content = {strings.error}; } finally { stopLoading($explore); } } async function listInstalledPlugins() { const plugins = await Promise.all( (await fsOperation(PLUGIN_DIR).lsDir()).map(async (item) => { const id = Url.basename(item.url); try { const url = Url.join(item.url, "plugin.json"); const plugin = await fsOperation(url).readFile("json"); if (plugin.icon) { const iconUrl = getLocalRes(id, plugin.icon); try { plugin.icon = await helpers.toInternalUri(iconUrl); } catch (error) { console.warn( `Failed to resolve plugin icon for "${id}" in sidebar.`, error, ); } } plugin.installed = true; return plugin; } catch (error) { console.warn( `Skipping unreadable installed plugin "${id}" in sidebar.`, error, ); return null; } }), ); return plugins.filter(Boolean); } async function getFilteredPlugins(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 hasMore = items.length === LIMIT; return { items, hasMore }; } catch (error) { console.error(`Failed to get Filtered 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) => doesPluginMatchFilter(plugin, filterState), ); filterState.buffer.push(...matched); } catch (error) { window.log("error", "Failed to fetch filtered plugins:"); window.log("error", error); filterState.hasMoreSource = false; break; } } while (items.length < LIMIT && filterState.buffer.length) { items.push(filterState.buffer.shift()); } const hasMore = (filterState.hasMoreSource !== false && filterState.nextPage) || filterState.buffer.length > 0; return { items, hasMore: Boolean(hasMore) }; } function doesPluginMatchFilter(plugin, filterState) { if (!plugin) return false; switch (filterState.type) { case "verified": return Boolean(plugin.author_verified); case "author": { const authorName = getPluginAuthorName(plugin); if (!authorName) return false; return authorName.toLowerCase().includes(filterState.value); } case "keywords": { const pluginKeywords = getPluginKeywords(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 getPluginAuthorName(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 getPluginKeywords(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 []; } function startLoading($list) { $list.$title.classList.add("loading"); } function stopLoading($list) { $list.$title.classList.remove("loading"); } /** * Update the height of the element * @param {HTMLElement} $el */ function updateHeight($el) { removeHeight($installed, $el !== $installed); removeHeight($explore, $el !== $explore); try { let height = $header?.getBoundingClientRect().height; const tileHeight = $el.get(":scope>.tile")?.getBoundingClientRect().height; if ($el === $searchResult) { height += 60; } else { height += $searchResult?.getBoundingClientRect().height + tileHeight; } setHeight($el, height); } catch (error) { console.error(error); } } /** * Remove height styles from an element * @param {HTMLElement} $el * @param {Boolean} collapse */ function removeHeight($el, collapse = false) { if (collapse) $el.collapse?.(); $scrollableLists.delete($el); updateStyle(); } /** * Change the height of an element * @param {HTMLElement} $el * @param {Number} height */ function setHeight($el, height) { $scrollableLists.add($el); const calcHeight = height ? `calc(100% - ${height}px)` : "100%"; $el.dataset.height = calcHeight; if ($el === $searchResult) { $el.style.height = "fit-content"; return; } updateStyle(); } function updateStyle() { let style = ""; $scrollableLists.forEach(($el) => { style += ` .list.collapsible[data-id="${$el.dataset.id}"] { max-height: ${$el.dataset.height} !important; } `; }); $style.innerHTML = style; } function getLocalRes(id, name) { return Url.join(PLUGIN_DIR, id, name); } function ListItem({ icon, name, id, version, downloads, installed, source }) { if (installed === undefined) { installed = !!installedPlugins.find(({ id: _id }) => _id === id); } const disabledMap = settings.value.pluginsDisabled || {}; const enabled = disabledMap[id] !== true; const $el = (
            {name} {installed ? ( <> {source ? ( ) : null} ) : ( )}
            ); $el.onclick = async (event) => { const morePluginActionButton = event.target.closest( '[data-action="more-plugin-action"]', ); const installPluginBtn = event.target.closest( '[data-action="install-plugin"]', ); const rebuildPluginBtn = event.target.closest( '[data-action="rebuild-plugin"]', ); if (morePluginActionButton) { more_plugin_action(id, name); return; } if (installPluginBtn) { try { let purchaseToken; let product; const pluginUrl = Url.join(constants.API_BASE, `plugin/${id}`); const remotePlugin = await fsOperation(pluginUrl) .readFile("json") .catch(() => { throw new Error("Failed to fetch plugin details"); }); const isPaid = remotePlugin.price > 0; if (isPaid) { [product] = await helpers.promisify(iap.getProducts, [ remotePlugin.sku, ]); if (product) { const purchase = await getPurchase(product.productId); purchaseToken = purchase?.purchaseToken; } } if (isPaid && !purchaseToken) { if (!product) throw new Error("Product not found"); const apiStatus = await helpers.checkAPIStatus(); if (!apiStatus) { alert(strings.error, strings.api_error); return; } iap.setPurchaseUpdatedListener( ...purchaseListener(onpurchase, onerror), ); await helpers.promisify(iap.purchase, product.productId); 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, }, }); purchaseToken = purchase?.purchaseToken; } async function onerror(error) { throw error; } } const { default: installPlugin } = await import("lib/installPlugin"); await installPlugin( id, remotePlugin.name, purchaseToken ? purchaseToken : undefined, ); const searchInput = container.querySelector('input[name="search-ext"]'); if (searchInput) { searchInput.value = ""; $searchResult.content = ""; // Reset filter state when clearing search results currentFilter = null; filterHasMore = true; isFilterLoading = false; $searchResult.onscroll = null; updateHeight($searchResult); $installed.expand(); } window.toast(strings.success, 3000); if (!$explore.collapsed) { $explore.ontoggle(); } if (!$installed.collapsed) { $installed.ontoggle(); } async function getPurchase(sku) { const purchases = await helpers.promisify(iap.getPurchases); const purchase = purchases.find((p) => p.productIds.includes(sku)); return purchase; } } catch (err) { console.error(err); window.toast(helpers.errorMessage(err), 3000); } return; } if (rebuildPluginBtn) { try { const { default: installPlugin } = await import("lib/installPlugin"); await installPlugin(source); window.toast(strings.success, 3000); } catch (err) { console.error(err); window.toast(helpers.errorMessage(err), 3000); } return; } plugin( { id, installed }, () => { if (!$explore.collapsed) { $explore.ontoggle(); } if (!$installed.collapsed) { $installed.ontoggle(); } }, () => { if (!$explore.collapsed) { $explore.ontoggle(); } if (!$installed.collapsed) { $installed.ontoggle(); } }, ); }; return $el; } async function loadAd(el) { if (!helpers.canShowAds()) return; try { if (!(await window.iad?.isLoaded())) { const oldText = el.textContent; el.textContent = strings["loading..."]; await window.iad.load(); el.textContent = oldText; } } catch (error) { console.error(error); } } async function uninstall(id) { try { const pluginDir = Url.join(PLUGIN_DIR, id); const state = await InstallState.new(id); await Promise.all([ loadAd(this), fsOperation(pluginDir).delete(), state.delete(state.storeUrl), ]); const pluginMainScript = document.getElementById(`${id}-mainScript`); if (pluginMainScript) document.head.removeChild(pluginMainScript); acode.unmountPlugin(id); const searchInput = container.querySelector('input[name="search-ext"]'); if (searchInput) { searchInput.value = ""; $searchResult.content = ""; // Reset filter state when clearing search results currentFilter = null; filterHasMore = true; isFilterLoading = false; $searchResult.onscroll = null; updateHeight($searchResult); if ($installed.collapsed) { $installed.expand(); } } // Show Ad If Its Free Version, interstitial Ad(iad) is loaded. await helpers.showInterstitialIfReady(); } catch (err) { helpers.error(err); } } async function more_plugin_action(id, pluginName) { const disabledMap = settings.value.pluginsDisabled || {}; const enabled = disabledMap[id] !== true; let actions = []; const pluginSettings = settings.uiSettings[`plugin-${id}`]; if (pluginSettings) { actions.push(strings.settings); } actions.push( enabled ? strings.disable || "Disable" : strings.enable || "Enable", ); actions.push(strings.uninstall); const action = await select("Action", actions); if (!action) return; switch (action) { case strings.settings: pluginSettings.setTitle(pluginName); pluginSettings.show(); break; case strings.uninstall: await uninstall(id); if (!$explore.collapsed) { $explore.ontoggle(); } if (!$installed.collapsed) { $installed.ontoggle(); } break; case strings.disable || "Disable": // fallthrough case strings.enable || "Enable": if (enabled) { disabledMap[id] = true; // Disabling } else { delete disabledMap[id]; // Enabling } settings.update({ pluginsDisabled: disabledMap }, false); // INFO: I don't know how to get all loaded plugins(not installed). const choice = await select( strings.info, [ // { value: "reload_plugins", text: strings["reload_plugins"] || "Reload Plugins" }, { value: "restart_app", text: strings["restart_app"] || "Restart App", }, { value: "single", text: enabled ? strings["disable_plugin"] || "Disable this Plugin" : strings["enable_plugin"] || "Enable this Plugin", }, ], { default: "single", }, ); // if (choice === "reload_plugins") { // // Unmount all currently loaded plugins before reloading // if (window.acode && typeof window.acode.getLoadedPluginIds === "function") { // for (const pluginId of window.acode.getLoadedPluginIds()) { // window.acode.unmountPlugin(pluginId); // } // } // await window.loadPlugins?.(); // window.toast(strings.success); // } if (choice === "restart_app") { location.reload(); } else if (choice === "single") { if (enabled) { window.acode.unmountPlugin(id); window.toast(strings["plugin_disabled"] || "Plugin Disabled"); } else { await loadPlugin(id); window.toast(strings["plugin_enabled"] || "Plugin enabled"); } if (!$explore.collapsed) { $explore.ontoggle(); } if (!$installed.collapsed) { $installed.ontoggle(); } } break; } } ================================================ FILE: src/sidebarApps/extensions/style.scss ================================================ @use "../../styles/mixins.scss"; .container.extensions { .header { padding: 10px; display: flex; flex-direction: column; border-bottom: 1px solid var(--border-color); box-sizing: border-box; .title { font-weight: 600; display: flex; justify-content: space-between !important; align-items: center; width: 100%; color: var(--primary-text-color); margin-bottom: 8px; .actions { display: flex; align-items: center; } .icon-button { background: none; border: none; cursor: pointer; color: var(--primary-text-color); margin-left: 0.4rem; border-radius: 6px; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; min-width: 32px; min-height: 32px; font-size: 1.1em; &:hover { background-color: var(--primary-color); } } } input[type="search"] { width: 100% !important; padding: 0.5rem !important; border: 1px solid var(--border-color); border-radius: 6px; background: var(--secondary-color); color: var(--primary-text-color); font-size: 0.875rem; transition: all 0.2s ease; margin: 0 !important; &:focus { outline: none; border-color: var(--active-color); box-shadow: 0 0 0 2px rgba(51, 153, 255, 0.1); } &::placeholder { color: var(--secondary-text-color); } } } .list.search-result { min-height: 0; &.loading { @include mixins.loader(20px); } .filter-message { display: flex; justify-content: space-between; align-items: center; background: var(--primary-color); padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border-color); font-size: 0.875rem; color: var(--primary-text-color); strong { color: var(--active-color); margin-left: 0.25rem; } .close-button { background: none; border: none; cursor: pointer; padding: 0.25rem; border-radius: 4px; transition: all 0.2s ease; color: var(--secondary-text-color); &:hover { background-color: var(--error-text-color); color: white; } } } } .tile { display: flex; justify-content: space-between; align-items: center; min-height: 30px; padding: 6px 8px; .text { &.sub-text { font-size: 0.8rem; &::after { font-size: 0.7rem; } } } .more_vert { display: flex; align-items: center; cursor: pointer; justify-content: center; transition: background-color 0.3s ease; margin-left: 10px; &:hover { background-color: var(--active-icon-color); } } .replay { display: flex; align-items: center; cursor: pointer; justify-content: center; transition: background-color 0.3s ease; &:hover { background-color: var(--active-icon-color); } } .install-btn { background: none; border: none; margin-left: 10px; border-radius: 50%; cursor: pointer; color: var(--primary-text-color); transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; &:hover { background-color: var(--active-icon-color); } &:active { transform: scale(0.95); } } } .icon { background-size: 24px !important; background-position: center !important; } } ================================================ FILE: src/sidebarApps/files/index.js ================================================ import "./style.scss"; import Sidebar from "components/sidebar"; import settings from "lib/settings"; /**@type {HTMLElement} */ let container; export default [ "documents", // icon "files", // id strings["files"], // title initApp, // init function false, // prepend onSelected, // onSelected function ]; /** * Initialize files app * @param {HTMLElement} el */ function initApp(el) { container = el; container.classList.add("files"); container.setAttribute("data-msg", strings["open folder"]); container.style.overflowX = "auto"; container.addEventListener("click", clickHandler); editorManager.on( ["new-file", "int-open-file-list", "remove-file"], (position) => { if ( typeof position === "string" && position !== settings.OPEN_FILE_LIST_POS_SIDEBAR ) return; const fileList = container.get(":scope > div.file-list"); if (fileList) fixHeight(fileList); }, ); editorManager.on("add-folder", fixHeight); Sidebar.on("show", onSelected); } /** * On selected handler for files app * @param {HTMLElement} el */ function onSelected(el) { const $scrollableLists = container.getAll(":scope .scroll[data-scroll-top]"); $scrollableLists.forEach(($el) => { $el.scrollTop = $el.dataset.scrollTop; }); } /** * Click handler for files app * @param {MouseEvent} e * @returns */ function clickHandler(e) { if (!container.children.length) { acode.exec("open-folder"); return; } const { target } = e; if (target.matches(".files>.list>.tile")) { fixHeight(target.parentElement); } } /** * Update list height * @param {HTMLElement} target Target element */ export function fixHeight(target) { const lists = Array.from(container.getAll(":scope > div")); const ITEM_HEIGHT = 30; let height = (lists.length - 1) * ITEM_HEIGHT; let activeFileList; if (settings.value.openFileListPos === settings.OPEN_FILE_LIST_POS_SIDEBAR) { const [firstList] = lists; if (firstList.classList.contains("file-list")) { activeFileList = firstList; if (firstList.unclasped) { const heightOffset = height - ITEM_HEIGHT; const totalHeight = ITEM_HEIGHT * activeFileList.$ul.children.length + ITEM_HEIGHT; const maxHeight = lists.length === 1 || !lists.slice(1).find((list) => list.unclasped) ? window.innerHeight : window.innerHeight / 2; const minHeight = Math.min(totalHeight, maxHeight - heightOffset); activeFileList.style.maxHeight = `${minHeight}px`; activeFileList.style.height = `${minHeight}px`; height += minHeight - ITEM_HEIGHT; } } } lists.forEach((list) => { if (list === activeFileList) return; if (target === activeFileList) { if (list.collapsed) { list.style.removeProperty("max-height"); list.style.removeProperty("height"); return; } target = list; } if (list === target && target.unclasped) { list.style.maxHeight = `calc(100% - ${height}px)`; list.style.height = `calc(100% - ${height}px)`; return; } if (list.collapsed) { list.style.removeProperty("max-height"); list.style.removeProperty("height"); return; } list.collapse(); list.style.removeProperty("max-height"); list.style.removeProperty("height"); return; }); } ================================================ FILE: src/sidebarApps/files/style.scss ================================================ .container.files { &:empty { height: 100%; width: 100%; display: flex; align-items: center; justify-content: center; &::after { content: attr(data-msg); font-weight: 600; } } /* Make the container horizontally scrollable */ overflow-x: auto !important; max-width: 100%; &::-webkit-scrollbar { height: 5px; width: 5px; } &::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.3); border-radius: 3px; } &::-webkit-scrollbar-corner { background: transparent; } scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, 0.3) transparent; .list { min-width: 100%; width: max-content; max-width: none; } ul { min-width: 100%; width: max-content; overflow-x: visible !important; max-width: none; margin-left: 0; &::-webkit-scrollbar-corner { background: transparent; } } li { min-width: 100%; width: max-content; } .tile { > .text { white-space: nowrap !important; overflow: visible !important; width: max-content !important; text-overflow: clip !important; } } /* Add indent guides for folders (excluding first level) */ .list.collapsible > ul > .collapsible > ul { position: relative; padding-left: 24px; &::before { content: ""; position: absolute; left: 14px; top: 0; height: 100%; width: 1px; background: var(--border-color); z-index: 0; } /* Add guides for deeper nesting */ .collapsible > ul { padding-left: 24px; &::before { content: ""; position: absolute; left: 14px; top: 0; height: 100%; width: 1px; background: var(--border-color); z-index: 0; } } } } ================================================ FILE: src/sidebarApps/index.js ================================================ import appSettings from "lib/settings"; import Sponsors from "pages/sponsors"; import SidebarApp from "./sidebarApp"; const SIDEBAR_APPS_LAST_SECTION = "sidebarAppsLastSection"; /**@type {HTMLElement} */ let $apps; /**@type {HTMLElement} */ let $sidebar; /**@type {string} */ let currentSection = localStorage.getItem(SIDEBAR_APPS_LAST_SECTION); /**@type {SidebarApp[]} */ const apps = []; /**@type {HTMLSpanElement | null} */ let $sponsorIcon = null; /** * @param {string} icon icon of the app * @param {string} id id of the app * @param {HTMLElement} el element to show in sidebar * @param {string} title title of the app * @param {(container:HTMLElement)=>void} initFunction * @param {boolean} prepend weather to show this app at the top of the sidebar or not * @param {(container:HTMLElement)=>void} onSelected * @returns {void} */ function add( icon, id, title, initFunction, prepend = false, onSelected = () => {}, ) { currentSection ??= id; const app = new SidebarApp(icon, id, title, initFunction, onSelected); apps.push(app); app.install(prepend); if (currentSection === id) { setActiveApp(id); } } /** * Removes a sidebar app with the given ID. * @param {string} id - The ID of the sidebar app to remove. * @returns {void} */ function remove(id) { const app = apps.find((app) => app.id === id); if (!app) return; const wasActive = app.active; app.remove(); apps.splice(apps.indexOf(app), 1); if (wasActive && apps.length > 0) { const preferredApp = apps.find((app) => app.id === currentSection); setActiveApp(preferredApp?.id || apps[0].id); return; } if (!apps.length) { currentSection = null; localStorage.removeItem(SIDEBAR_APPS_LAST_SECTION); } } /** * Initialize sidebar apps * @param {HTMLElement} $el */ function init($el) { $sidebar = $el; $apps = $sidebar.get(".app-icons-container"); $apps.addEventListener("click", onclick); SidebarApp.init($el, $apps); appSettings.on( "update:showSponsorSidebarApp", setSponsorSidebarAppVisibility, ); } /** * Loads all sidebar apps. */ async function loadApps() { add(...(await import("./files")).default); add(...(await import("./searchInFiles")).default); add(...(await import("./extensions")).default); add(...(await import("./notification")).default); setSponsorSidebarAppVisibility(appSettings.value.showSponsorSidebarApp); } /** * Adds or removes the sponsor icon in sidebar based on settings. * @param {boolean} visible */ function setSponsorSidebarAppVisibility(visible) { if (!$apps) return; if (visible) { if ($sponsorIcon?.isConnected) return; $sponsorIcon = ( ); $apps.append($sponsorIcon); return; } if ($sponsorIcon) { $sponsorIcon.remove(); $sponsorIcon = null; } } /** * Ensures that at least one app is active. * Call this AFTER all plugins have been loaded to handle cases where * the stored section was from an uninstalled plugin. * @returns {void} */ function ensureActiveApp() { const activeApps = apps.filter((app) => app.active); if (activeApps.length === 1) return; if (activeApps.length > 1) { const preferredActiveApp = activeApps.find( (app) => app.id === currentSection, ); setActiveApp(preferredActiveApp?.id || activeApps[0].id); return; } if (apps.length > 0) { const preferredApp = apps.find((app) => app.id === currentSection); setActiveApp(preferredApp?.id || apps[0].id); } } /** * Gets the container of the app with the given ID. * @param {string} id * @returns */ function get(id) { const app = apps.find((app) => app.id === id); return app.container; } /** * Handles click on sidebar apps * @param {MouseEvent} e */ function onclick(e) { const target = e.target; const { action, id } = target.dataset; if (action !== "sidebar-app") return; setActiveApp(id); } /** * Activates the given sidebar app and deactivates all others. * @param {string} id * @returns {void} */ function setActiveApp(id) { const app = apps.find((app) => app.id === id); if (!app) return; currentSection = id; localStorage.setItem(SIDEBAR_APPS_LAST_SECTION, id); for (const currentApp of apps) { currentApp.active = currentApp.id === id; } } export default { init, add, get, remove, loadApps, ensureActiveApp, }; ================================================ FILE: src/sidebarApps/notification/index.js ================================================ import NotificationManager from "lib/notificationManager"; import "./style.scss"; import Sidebar from "components/sidebar"; /**@type {HTMLElement} */ let container; /** @type {HTMLElement} */ let $notificationContainer = null; let notificationManager; export default [ "notifications", // icon "notification", // id strings["notifications"], // title initApp, // init function false, // prepend onSelected, // onSelected function ]; const $header = (
            {strings["notifications"]}
            ); /** * Initialize files app * @param {HTMLElement} el */ function initApp(el) { container = el; container.classList.add("notifications"); container.content = $header; $notificationContainer = (
            ); container.append($notificationContainer); notificationManager = new NotificationManager(); Sidebar.on("show", onSelected); } /** * On selected handler for files app * @param {HTMLElement} el */ function onSelected(el) { const $scrollableLists = container.getAll(":scope .scroll[data-scroll-top]"); $scrollableLists.forEach(($el) => { $el.scrollTop = $el.dataset.scrollTop; }); } ================================================ FILE: src/sidebarApps/notification/style.scss ================================================ .container.notifications { display: flex; flex-direction: column; .header { padding: 10px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); .title { gap: 4px; display: flex; font-weight: 600; align-items: center; color: var(--primary-text-color); justify-content: space-between !important; .icon-button { border: none; background: none; cursor: pointer; color: var(--primary-text-color); margin-left: 0.4rem; border-radius: 6px; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; min-width: 32px; min-height: 32px; font-size: 1.1em; &:hover { background-color: var(--primary-color); } } } } .notifications-container { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; .empty-state { text-align: center; color: var(--secondary-text-color); padding: 20px; font-size: 14px; } .notification-item { padding: 10px 12px; border-radius: 6px; background: var(--popup-background-color); display: flex; gap: 10px; align-items: flex-start; animation: slideIn 0.3s ease-out; border: 1px solid var(--popup-border-color); transition: all 0.2s ease; &:hover { background: rgba($color: #000000, $alpha: 0.2); border-color: var(--popup-border-color); } .notification-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--primary-text-color); } .notification-content { flex: 1; min-width: 0; .notification-title { font-size: 13px; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; word-wrap: break-word; color: var(--primary-text-color); display: flex; justify-content: space-between; align-items: center; .notification-time { font-size: 11px; color: var(--secondary-text-color); } } .notification-message { font-size: 12px; color: var(--secondary-text-color); line-height: 1.4; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; word-break: break-word; } .notification-actions { margin-top: 8px; display: flex; justify-content: flex-end; .action-button { font-size: 11px; padding: 6px 12px; border-radius: 4px; background: transparent; color: var(--secondary-text-color); cursor: pointer; transition: all 0.2s ease; border: 1px solid var(--border-color); display: flex; align-items: center; gap: 4px; &:hover { background: rgba($color: var(--border-color), $alpha: 0.6); color: var(--primary-text-color); border-color: var(--border-color); } &:active { transform: translateY(1px); } &::before { content: '×'; font-size: 14px; line-height: 1; opacity: 0.8; } } } } } } } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } ================================================ FILE: src/sidebarApps/searchInFiles/cmResultView.js ================================================ import { EditorState, StateEffect, StateField } from "@codemirror/state"; import { Decoration, EditorView, ViewPlugin, WidgetType, } from "@codemirror/view"; import appSettings from "lib/settings"; import helpers from "utils/helpers"; /** * CodeMirror view to render search results * * @param {HTMLElement} container * @param {object} opts * @param {(lineIndex:number)=>void} opts.onLineClick * @param {()=>string[]} opts.getWords - returns list of words to highlight * @param {()=>string[]} opts.getFileNames - returns list of filenames (used to style header lines) */ export function createSearchResultView( container, { onLineClick, getWords, getFileNames, getRegex }, ) { let view; // Effect and field to maintain collapsed headers (by line number) const toggleFold = StateEffect.define(); const foldState = StateField.define({ create() { return new Set(); }, update(value, tr) { let next = value; for (const e of tr.effects) { if (e.is(toggleFold)) { if (next === value) next = new Set(value); const ln = e.value; if (next.has(ln)) next.delete(ln); else next.add(ln); } } // Reset folds on full document reset if (tr.docChanged && tr.startState.doc.length === 0) return new Set(); return next; }, }); function eachGroup(doc, fn) { // Groups start at lines not beginning with a tab const total = doc.lines; let start = 1; while (start <= total) { const header = doc.line(start); // If header starts with tab, advance until a non-tab header if (header.text.startsWith("\t")) { start++; continue; } let end = start; for (let i = start + 1; i <= total; i++) { const line = doc.line(i); if (!line.text.startsWith("\t")) break; end = i; } fn({ start, end }); start = end + 1; } } class ChevronWidget extends WidgetType { constructor(collapsed) { super(); this.collapsed = collapsed; } eq(other) { return other.collapsed === this.collapsed; } toDOM() { const span = document.createElement("span"); span.className = `cm-foldChevron icon keyboard_arrow_${this.collapsed ? "right" : "down"}`; return span; } ignoreEvent() { return false; } } class SummaryWidget extends WidgetType { constructor(text) { super(); this.text = text; } eq(other) { return other.text === this.text; } toDOM() { const div = document.createElement("div"); div.className = "cm-collapsedSummary"; div.textContent = this.text; return div; } ignoreEvent() { return false; } } class CountWidget extends WidgetType { constructor(count) { super(); this.count = count; } eq(other) { return other.count === this.count; } toDOM() { const span = document.createElement("span"); span.className = "cm-fileCount"; span.textContent = `(${this.count})`; return span; } ignoreEvent() { return true; } } class FileIconWidget extends WidgetType { constructor(className) { super(); this.className = className; } eq(other) { return other.className === this.className; } toDOM() { const span = document.createElement("span"); span.className = `${this.className} cm-fileIcon`; return span; } ignoreEvent() { return false; } } function buildGroupDecos(state) { const doc = state.doc; const folded = state.field(foldState, false) || new Set(); // No removed groups const fns = (typeof getFileNames === "function" ? getFileNames() : []) || []; if (!fns.length || doc.length === 0 || doc.lines === 0) return Decoration.none; const builder = []; // Build header chevrons and collapses per group let groupIndex = 0; eachGroup(doc, ({ start, end }) => { const header = doc.line(start); const key = start - 1; const collapsed = folded.has(key); // zero-based line index // Header line class and chevron widget builder.push( Decoration.line({ class: "cm-fileName" }).range(header.from), ); builder.push( Decoration.widget({ widget: new ChevronWidget(collapsed), side: -1, }).range(header.from), ); // File icon const fileNames = (typeof getFileNames === "function" ? getFileNames() : []) || []; const fname = fileNames[groupIndex] || ""; const iconClass = helpers.getIconForFile(fname); builder.push( Decoration.widget({ widget: new FileIconWidget(iconClass), side: -1, }).range(header.from), ); // Count badge on right const count = Math.max(0, end - start); builder.push( Decoration.widget({ widget: new CountWidget(count), side: 1 }).range( header.to, ), ); if (collapsed && end > start) { // Hide content lines and show a summary placeholder const first = doc.line(start + 1); const last = doc.line(end); builder.push( Decoration.replace({ block: true }).range(first.from, last.to), ); const count2 = end - start; builder.push( Decoration.widget({ widget: new SummaryWidget( `${count2} result${count2 > 1 ? "s" : ""}`, ), side: 1, block: true, }).range(first.from), ); } groupIndex++; }); return Decoration.set(builder, true); } const groupDecoField = StateField.define({ create(state) { return buildGroupDecos(state); }, update(decos, tr) { if ( tr.docChanged || tr.startState.field(foldState, false) !== tr.state.field(foldState, false) ) { return buildGroupDecos(tr.state); } return decos.map(tr.changes); }, provide: (f) => EditorView.decorations.from(f), }); const decorationsPlugin = ViewPlugin.fromClass( class { constructor(view) { this.decorations = this.buildDecos(view); } update(update) { if ( update.docChanged || update.viewportChanged || update.startState.field(foldState) !== update.state.field(foldState) ) { this.decorations = this.buildDecos(update.view); } } buildDecos(view) { const builder = []; let searchRegex = null; if (typeof getRegex === "function") { const r = getRegex(); if (r && r.source) { const flags = (r.ignoreCase ? "i" : "") + "g"; try { searchRegex = new RegExp(r.source, flags); } catch {} } } const words = searchRegex ? [] : (getWords?.() || []).filter(Boolean); let wordRegex = null; if (!searchRegex && words.length) { const escaped = words .map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) .join("|"); try { wordRegex = new RegExp(escaped, "g"); } catch {} } // Add match highlights only on visible lines to keep it fast const matcher = searchRegex || wordRegex; if (matcher) { for (const { from, to } of view.visibleRanges) { let pos = from; while (pos <= to) { const line = view.state.doc.lineAt(pos); const text = line.text; if (text && text.charCodeAt(0) === 9) { matcher.lastIndex = 0; let m; while ((m = matcher.exec(text))) { const fromPos = line.from + m.index; const toPos = fromPos + m[0].length; builder.push( Decoration.mark({ class: "cm-match" }).range( fromPos, toPos, ), ); if (m.index === matcher.lastIndex) matcher.lastIndex++; } } if (line.to >= to) break; pos = line.to + 1; } } } return Decoration.set(builder, true); } }, { decorations: (v) => v.decorations, eventHandlers: { mousedown(event, view) { // Map click to line number and notify (use client coords) const pos = view.posAtCoords({ x: event.clientX, y: event.clientY }); if (pos == null) return; // Only react when clicking on a line element, not empty space const lineEl = event.target && event.target.closest ? event.target.closest(".cm-line") : null; if (!lineEl) return; const ln = view.state.doc.lineAt(pos).number - 1; // zero-based const lineText = view.state.doc.line(ln + 1).text; const isHeader = lineText.length > 0 && lineText.charCodeAt(0) !== 9; if (isHeader) { // Toggle collapse on header click view.dispatch({ effects: toggleFold.of(ln) }); return; } // Only trigger navigation for match lines (start with tab) if (!(lineText && lineText.charCodeAt(0) === 9)) return; onLineClick?.(ln); }, }, }, ); const readOnly = EditorState.readOnly.of(true); const lineWrap = EditorView.lineWrapping; const noCursor = EditorView.editable.of(false); function getEditorFontFamily() { const font = appSettings?.value?.editorFont || "Roboto Mono"; return `${font}, Noto Mono, Monaco, monospace`; } const theme = EditorView.theme({ "&": { fontSize: String(appSettings?.value?.fontSize || "12px"), lineHeight: String(appSettings?.value?.lineHeight || 1.5), }, ".cm-content": { padding: 0, fontFamily: getEditorFontFamily(), }, ".cm-line": { color: "var(--primary-text-color)", }, }); const state = EditorState.create({ doc: "", extensions: [ EditorState.tabSize.of(1), readOnly, noCursor, lineWrap, theme, foldState, groupDecoField, decorationsPlugin, ], }); view = new EditorView({ state, parent: container }); return { setValue(text) { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text || "" }, }); }, insert(text) { if (!text) return; view.dispatch({ changes: { from: view.state.doc.length, insert: text } }); }, setGhostText(text) { this.setValue(text || ""); }, removeGhostText() { this.setValue(""); }, get view() { return view; }, }; } ================================================ FILE: src/sidebarApps/searchInFiles/index.js ================================================ import "./styles.scss"; import fsOperation from "fileSystem"; import { EditorView } from "@codemirror/view"; import autosize from "autosize"; import Checkbox from "components/checkbox"; import Sidebar, { preventSlide } from "components/sidebar"; import escapeStringRegexp from "escape-string-regexp"; import Reactive from "html-tag-js/reactive"; import Ref from "html-tag-js/ref"; import files, { Tree } from "lib/fileList"; import openFile from "lib/openFile"; import settings from "lib/settings"; import helpers from "utils/helpers"; import { createSearchResultView } from "./cmResultView"; // Local highlight sources const words = []; const fileNames = []; const MAX_HL_WORDS = 400; // cap to avoid massive regex in result view const workers = []; const results = []; const filesSearched = []; const filesReplaced = []; const $container = Ref(); const $regExp = Ref(); const $search = Ref(); const $replace = Ref(); const $exclude = Ref(); const $include = Ref(); const $wholeWord = Ref(); const $caseSensitive = Ref(); const $btnReplaceAll = Ref(); const $resultOverview = Ref(); const $error = Reactive(); const $progress = Reactive(); const resultOverview = { filesCount: 0, matchesCount: 0, reset() { this.filesCount = 0; this.matchesCount = 0; $resultOverview.innerHTML = searchResultText(0, 0); $resultOverview.classList.remove("error"); }, }; const CASE_SENSITIVE = "search-in-files-case-sensitive"; const WHOLE_WORD = "search-in-files-whole-word"; const REG_EXP = "search-in-files-reg-exp"; const EXCLUDE = "search-in-files-exclude"; const INCLUDE = "search-in-files-include"; const store = { get caseSensitive() { return localStorage.getItem(CASE_SENSITIVE) === "true"; }, set caseSensitive(value) { localStorage.setItem(CASE_SENSITIVE, value); }, get wholeWord() { return localStorage.getItem(WHOLE_WORD) === "true"; }, set wholeWord(value) { return localStorage.setItem(WHOLE_WORD, value); }, get regExp() { return localStorage.getItem(REG_EXP) === "true"; }, set regExp(value) { return localStorage.setItem(REG_EXP, value); }, get exclude() { return localStorage.getItem(EXCLUDE); }, set exclude(value) { return localStorage.setItem(EXCLUDE, value); }, get include() { return localStorage.getItem(INCLUDE); }, set include(value) { return localStorage.setItem(INCLUDE, value); }, }; const debounceSearch = helpers.debounce(searchAll, 500); let useIncludeAndExclude = false; let searchResult = null; // CM6 wrapper from createSearchResultView let currentSearchRegex = null; let replacing = false; let newFiles = 0; let searching = false; addEventListener($regExp, "change", onInput); addEventListener($wholeWord, "change", onInput); addEventListener($caseSensitive, "change", onInput); addEventListener($search, "input", onInput); addEventListener($include, "input", onInput); addEventListener($exclude, "input", onInput); addEventListener($btnReplaceAll, "click", replaceAll); files.on("push-file", () => { if (!searching) return; $error.value = strings["missed files"].replace("{count}", ++newFiles); }); $container.onref = ($el) => { searchResult = createSearchResultView($el, { onLineClick: onCursorChange, getWords: () => words, getFileNames: () => fileNames, getRegex: () => currentSearchRegex, }); $container.style.lineHeight = "1.5"; }; preventSlide((target) => { return $container.el?.contains(target); }); export default [ "search", "searchInFiles", strings["search in files"], (/**@type {HTMLElement} */ el) => { el.classList.add("search-in-files"); el.content = ( <>
            , ); } /** * Converts a search string and options into a regular expression. * * @param {string} search - The search string. * @param {object} options - The search options. * @param {boolean} [options.caseSensitive=false] - Whether the search is case-sensitive. * @param {boolean} [options.wholeWord=false] - Whether to match whole words only. * @param {boolean} [options.regExp=false] - Whether the search string is a regular expression. * @returns {RegExp} - The regular expression created from the search string and options. */ function toRegex(search, options) { const { caseSensitive = false, wholeWord = false, regExp = false } = options; let flags = caseSensitive ? "gm" : "gim"; let regexString = regExp ? search : escapeStringRegexp(search); if (wholeWord) { const wordBoundary = "\\b"; regexString = `${wordBoundary}${regexString}${wordBoundary}`; } try { return new RegExp(regexString, flags); } catch (error) { const [, message] = error.message.split(/:(.*)/); $resultOverview.classList.add("error"); $resultOverview.textContent = strings["invalid regex"].replace( "{message}", message || error.message, ); return null; } } /** * On cursor change event handler */ async function onCursorChange(line) { const result = results[line]; if (!result) return; const { file, position } = result; if (!position) { // header line clicked; CM view folding not implemented yet return; } Sidebar.hide(); const { url } = filesSearched[file]; await openFile(url, { render: true }); const { editor } = editorManager; try { // Compute offsets from row/column (rows from worker are 0-based) const doc = editor.state.doc; const startLine = doc.line(position.start.row + 1); const endLine = doc.line(position.end.row + 1); const from = Math.min(startLine.from + position.start.column, startLine.to); const to = Math.min(endLine.from + position.end.column, endLine.to); editor.dispatch({ selection: { anchor: from, head: to }, effects: EditorView.scrollIntoView(from, { y: "center" }), }); } catch (error) { console.warn(`Failed to focus search result at line ${line}.`, error); } } /** * When a file is added or removed from the file list * @param {import('lib/fileList').Tree} tree */ function onFileUpdate(tree) { if (!tree || tree?.children) return; onInput(); } /** * Add event listeners to file changes */ function addEvents() { files.on("add-file", onFileUpdate); files.on("remove-file", onFileUpdate); files.on("add-folder", onInput); files.on("remove-folder", onInput); files.on("refresh", onInput); editorManager.on("rename-file", onInput); editorManager.on("file-content-changed", onInput); } /** * Remove event listeners to file changes */ function removeEvents() { files.off("add-file", onFileUpdate); files.off("remove-file", onFileUpdate); files.off("add-folder", onInput); files.off("remove-folder", onInput); files.off("refresh", onInput); editorManager.off("rename-file", onInput); editorManager.off("file-content-changed", onInput); } ================================================ FILE: src/sidebarApps/searchInFiles/styles.scss ================================================ .search-in-files { width: calc(100% - 40px - 20px) !important; margin: 0 auto; overflow: visible !important; [is='details'] { width: 100%; >[is='summary'] { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; textarea, input { margin: 0 !important; white-space: nowrap; overflow-x: auto !important; } .marker::after { width: 10px; font-size: 1rem; content: '\25B6'; } } >[is='summary']~* { display: none; margin: 0 0 5px auto !important; } &[open] { >[is='summary'] { .marker::after { content: '\25BC'; } } >[is='summary']~* { display: block; } >[is='summary']~div { display: flex !important; justify-content: space-between; align-items: center; >* { margin: 0 !important; } } } } .cm-editor { height: 100%; width: 100%; background-color: var(--primary-color); color: var(--primary-text-color); .cm-content { background-color: inherit; color: inherit; padding: 0 !important; .cm-line { padding: 0 !important; } .cm-line:nth-child(odd) { background-color: rgba(0, 0, 0, 0.2) !important; } } .cm-scroller { overflow: auto; } .cm-line.cm-fileName .cm-foldChevron { display: inline-flex; align-items: center; width: auto; margin-right: 2px; opacity: 0.9; vertical-align: middle; } .cm-line.cm-fileName { font-weight: 600; } .cm-line.cm-fileName .cm-fileIcon { display: inline-flex; align-items: center; vertical-align: middle; margin-right: 2px; font-size: 1em; line-height: 1; } .cm-line.cm-fileName .cm-fileCount { display: inline-block; margin-left: 8px; font-size: 0.9em; color: var(--secondary-text-color); } .cm-collapsedSummary { padding: 4px 8px; margin: 2px 0 6px 18px; font-size: 0.9em; color: var(--secondary-text-color); background: color-mix(in srgb, var(--secondary-color) 60%, transparent); border: 1px solid var(--border-color); border-radius: 4px; } /* Match highlight */ .cm-match { background: color-mix(in srgb, var(--active-color) 60%, transparent); color: var(--secondary-text-color); outline: 1px solid var(--border-color); border-radius: 2px; font-weight: 500; } } .cm-editor { width: 100% !important; margin-left: 0 !important; } .search-in-file-editor { height: 100%; min-height: 0; overflow: hidden; display: block; } textarea { height: auto; resize: none; overflow-y: hidden; } .extras { display: block !important; line-height: 0px !important; margin-bottom: 10px !important; text-align: right !important; } .options { width: 100%; margin-bottom: 10px; text-align: right; } button { display: inline-block; background-color: transparent; border: none; color: rgb(37, 37, 37); color: var(--secondary-text-color); } .error { color: orangered; } .search-result { position: relative; padding-bottom: 5px; } } ================================================ FILE: src/sidebarApps/searchInFiles/worker.js ================================================ import "core-js/stable"; import picomatch from "picomatch/posix"; const resolvers = {}; self.onmessage = (ev) => { const { action, data, error, id } = ev.data; switch (action) { case "search-files": processFiles(data, "search"); break; case "replace-files": processFiles(data, "replace"); break; case "get-file": { if (!resolvers[id]) return; const cb = resolvers[id]; cb(data, error); delete resolvers[id]; break; } default: return false; } }; /** * Process files for search or replace operations. * * @param {object} data - The data containing files, search, replace, and options. * @param {'search' | 'replace'} [mode='search'] - The mode of operation (search or replace). */ function processFiles(data, mode = "search") { const process = mode === "search" ? searchInFile : replaceInFile; const { files, search, replace, options } = data; const { test: skip } = Skip(options); const total = files.length; let count = 0; files.forEach(processFile); /** * Process a file for search or replace operation. * * @param {object} file - The file object to process. * @param {string} file.url - The URL of the file. */ function processFile(file) { if (skip(file)) { done(++count / total, mode); return; } getFile(file.url, (res, err) => { if (err) { done(++count / total, mode); throw err; } process({ file, content: res, search, replace, options }); done(++count / total, mode); }); } } /** * Search for a string in the content of a file. * @param {object} arg - The content of the file to search. * @param {import('lib/fileList').Tree} arg.file - The file. * @param {string} arg.content - The file content. * @param {RegExp} arg.search - The string to search for. */ function searchInFile({ file, content, search }) { const matches = []; let text = `${file.name}`; let match; if (text.length > 30) { text = `...${text.slice(-30)}`; } while ((match = search.exec(content))) { const [word] = match; const start = match.index; const end = start + word.length; const position = { start: getLineColumn(content, start), end: getLineColumn(content, end), }; const [line, renderText] = getSurrounding(content, word, start, end); text += `\n\t${line.trim()}`; matches.push({ match: word, position, renderText }); } self.postMessage({ action: "search-result", data: { file, matches, text, }, }); } /** * Replace a string in the content of a file. * @param {object} arg - The content of the file to search. * @param {import('lib/fileList').Tree} arg.file - The content of the file to search. * @param {string} content - The content of the file to search. * @param {RegExp} arg.search - The string to search for. * @param {string} arg.replace - The string to replace with. */ function replaceInFile({ file, content, search, replace }) { const text = content.replace(search, replace); self.postMessage({ action: "replace-result", data: { file, text }, }); } /** * Gets surrounding text of a match. * @param {string} content * @param {string} word * @param {number} start * @param {number} end */ function getSurrounding(content, word, start, end) { const max = 50; const remaining = max - (end - start); let result = []; if (remaining <= 0) { word = word.slice(-max); result = [`...${word}`, word]; } else { let left = Math.floor(remaining / 2); let right = left; let leftText = content.substring(start - left, start); let rightText = content.substring(end, end + right); result = [`${leftText}${word}${rightText}`, word]; } return result.map((text) => text.replace(/[\r\n]+/g, " ⏎ ")); } /** * Determines the line and column numbers for a given position in the file. * * @param {string} file - The file content as a string. * @param {number} position - The position in the file for which line and column * numbers are to be determined. * * @returns {Object} An object with 'line' and 'column' properties, representing * the line and column numbers respectively for the given position. * * @example * * const file = 'Hello, this is a test.\nAnother test is here.'; * const position = 15; * const lineColumn = getLineColumn(file, position); * * // lineColumn: { line: 1, column: 16 } */ function getLineColumn(file, position) { const lines = file.substring(0, position).split("\n"); const lineNumber = lines.length - 1; const columnNumber = lines[lineNumber].length; return { row: lineNumber, column: columnNumber }; } /** * Retrieves the contents of a file from the main thread. * @param {string} url * @param {function} cb */ function getFile(url, cb) { const id = Number.parseInt(Date.now() + Math.random() * 1000000); resolvers[id] = cb; self.postMessage({ action: "get-file", data: url, id, }); } /** * Sends a message to the main thread to indicate that the worker is done searching * or replacing. * @param {boolean} ratio * @param {'search'|'replace'} mode */ function done(ratio, mode) { if (ratio === 1) { self.postMessage({ action: "progress", data: 100, }); self.postMessage({ action: `done-${mode === "search" ? "searching" : "replacing"}`, }); } else { self.postMessage({ action: "progress", data: Math.floor(ratio * 100), }); } } /** * Creates a skip function that filters files based on exclusion and inclusion patterns. * * @param {object} arg - The exclusion patterns separated by commas. * @param {string} arg.exclude - The exclusion patterns separated by commas. * @param {string} arg.include - The inclusion patterns separated by commas. */ function Skip({ exclude, include }) { // Default exclude patterns for binary/media/archives/fonts/etc. const defaultExcludes = [ "**/*.png", "**/*.jpg", "**/*.jpeg", "**/*.gif", "**/*.bmp", "**/*.webp", "**/*.avif", "**/*.ico", "**/*.svgz", "**/*.mp3", "**/*.wav", "**/*.ogg", "**/*.flac", "**/*.m4a", "**/*.aac", "**/*.mp4", "**/*.mkv", "**/*.webm", "**/*.mov", "**/*.avi", "**/*.zip", "**/*.gz", "**/*.bz2", "**/*.xz", "**/*.7z", "**/*.rar", "**/*.tar", "**/*.exe", "**/*.dll", "**/*.so", "**/*.bin", "**/*.class", "**/*.ttf", "**/*.otf", "**/*.woff", "**/*.woff2", "**/*.pdf", "**/*.psd", "**/*.ai", "**/*.sketch", ]; const userExcludes = (exclude ? exclude.split(",") : []) .map((p) => p.trim()) .filter(Boolean); const excludeFiles = [...defaultExcludes, ...userExcludes]; const includeFiles = (include ? include.split(",") : ["**"]).map((p) => p.trim(), ); /** * Tests whether a file should be skipped based on exclusion and inclusion patterns. * * @param {object} file - The file to be tested. * @param {string} file.path - The relative URL of the file. * @returns {boolean} - Returns true if the file should be skipped, false otherwise. */ function test(file) { if (!file.path) return false; const match = (pattern) => picomatch.isMatch(file.path, pattern, { matchBase: true }); return excludeFiles.some(match) || !includeFiles.some(match); } return { test, }; } /** * @typedef {Object} Match * @property {string} line - The line of the file where the match was found. * @property {string} text - Match result converted to a string. * @property {Object} position - An object representing the start and end positions of the match. * @property {Object} position.start - An object with properties line and column representing the start position. * @property {number} position.start.line - The line number of the start position. * @property {number} position.start.column - The column number of the start position. * @property {Object} position.end - An object with properties line and column representing the end position. * @property {number} position.end.line - The line number of the end position. * @property {number} position.end.column - The column number of the end position. */ ================================================ FILE: src/sidebarApps/sidebarApp.js ================================================ /**@type {HTMLElement} */ let $apps; /**@type {HTMLElement} */ let $sidebar; /**@type {HTMLElement} */ let $contaienr; export default class SidebarApp { /**@type {HTMLSpanElement} */ #icon; /**@type {string} */ #id; /**@type {string} */ #init; /**@type {string} */ #title; /**@type {boolean} */ #active; /**@type {(el:HTMLElement)=>void} */ #onselect; /**@type {HTMLElement} */ #container; /** * Creates a new sidebar app. * @param {string} icon * @param {string} id * @param {string} title * @param {(el:HTMLElement)=>void} init * @param {(el:HTMLElement)=>void} onselect */ constructor(icon, id, title, init, onselect) { const emptyFunc = () => {}; this.#container =
            ; this.#icon = ; this.#id = id; this.#title = title; this.#init = init || emptyFunc; this.#onselect = onselect || emptyFunc; this.#init(this.#container); } /** * Installs the app in the sidebar. * @param {boolean} prepend * @returns {void} */ install(prepend = false) { if (prepend) { $apps.prepend(this.#icon); return; } $apps.append(this.#icon); } /** * Initialize the sidebar element. * @param {HTMLElement} $el sidebar element * @param {HTMLElement} $el2 apps element */ static init($el, $el2) { $sidebar = $el; $apps = $el2; } /**@type {HTMLSpanElement} */ get icon() { return this.#icon; } /**@type {string} */ get id() { return this.#id; } /**@type {string} */ get title() { return this.#title; } /**@type {boolean} */ get active() { return !!this.#active; } /**@param {boolean} value */ set active(value) { const nextValue = !!value; if (this.#active === nextValue) return; this.#active = nextValue; this.#icon.classList.toggle("active", this.#active); if (this.#active) { const oldContainer = getContainer(this.#container); // 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); } else { // Old container not in sidebar, just append the new one const existingContainer = $sidebar.get(".container"); if (existingContainer) { $sidebar.replaceChild($contaienr, existingContainer); } else { $sidebar.appendChild($contaienr); } } } catch (error) { // Fallback: append the new container console.warn("Error switching sidebar container:", error); const existingContainer = $sidebar.get(".container"); if (existingContainer) { existingContainer.remove(); } $sidebar.appendChild($contaienr); } this.#onselect(this.#container); } } /**@type {HTMLElement} */ get container() { return this.#container; } /**@type {(el:HTMLElement)=>void} */ get init() { return this.#init; } /**@type {(el:HTMLElement)=>void} */ get onselect() { return this.#onselect; } remove() { if (this.#icon) { this.#icon.remove(); this.#icon = null; } if (this.#container) { this.#container.remove(); this.#container = null; } } } /** * Creates a icon element for a sidebar app. * @param {object} param0 * @param {string} param0.icon * @param {string} param0.id * @returns {HTMLElement} */ function Icon({ icon, id, title }) { const className = `icon ${icon}`; return ( ); } /** * Gets the container or sets it if it's not set. * @param {HTMLElement} $el * @returns {HTMLElement} */ function getContainer($el) { const res = $contaienr; if ($el) { $contaienr = $el; } return res || $sidebar.get(".container"); } ================================================ FILE: src/styles/codemirror.scss ================================================ .editor-container { position: relative; } .editor-container > .cursor-menu { z-index: 600; } .cm-tooltip { box-sizing: border-box; max-width: min(32rem, calc(100vw - 1.25rem)); width: max-content; padding: 0.4rem 0.45rem; border-radius: 0; overscroll-behavior: contain; overflow-y: auto; max-height: min(70vh, 22rem); .cm-tooltip-section + .cm-tooltip-section { margin-top: 0.5rem; } } .cm-tooltip.cm-tooltip-hover { font-size: 0.9rem; line-height: 1.45; word-break: break-word; max-height: min(65vh, 20rem); } .cm-tooltip.cm-tooltip-autocomplete { display: flex; flex-wrap: nowrap; align-items: stretch; gap: 0.4rem; width: auto; min-width: min(15rem, calc(100vw - 1.75rem)); max-width: min(32rem, calc(100vw - 1.25rem)); max-height: min(60vh, 20rem); padding: 0.25rem; overflow: visible; } .cm-tooltip.cm-tooltip-autocomplete > ul { flex: 1 1 auto; max-height: inherit; overflow: auto; padding: 0.25rem; margin: 0; scrollbar-gutter: stable; } .cm-tooltip.cm-tooltip-autocomplete > ul > li { display: flex; align-items: center; gap: 0.12rem; padding: 0.3rem 0.36rem; border-radius: 0.2rem; } .cm-tooltip.cm-tooltip-autocomplete .cm-completionIcon { flex: 0 0 auto; min-width: 1rem; text-align: center; line-height: 1; } .cm-tooltip.cm-tooltip-autocomplete .cm-completionLabel { flex: 1 1 auto; font-size: 0.95em; line-height: 1.4; overflow-wrap: anywhere; } .cm-tooltip.cm-tooltip-autocomplete .cm-completionMatchedText { font-weight: 600; } .cm-tooltip.cm-tooltip-autocomplete .cm-completionDetail { margin-left: auto; font-size: 0.85em; } .cm-tooltip.cm-tooltip-autocomplete .cm-completionInfo { flex: 1 1 45%; min-width: min(12rem, calc(100vw - 3rem)); max-width: min(18rem, calc(100vw - 2.5rem)); max-height: inherit; padding: 0.3rem 0.35rem; font-size: 0.85rem; line-height: 1.35; overflow: auto; } @media (max-width: 480px) { .cm-tooltip { font-size: 0.9rem; max-width: calc(100vw - 1.25rem); max-height: min(70vh, 20rem); } .cm-tooltip.cm-tooltip-autocomplete { flex-direction: column; min-width: min(13.5rem, calc(100vw - 1.5rem)); max-width: calc(100vw - 1.35rem); max-height: min(65vh, 18rem); } .cm-tooltip.cm-tooltip-autocomplete > ul > li { padding: 0.32rem 0.4rem; } .cm-tooltip.cm-tooltip-autocomplete .cm-completionInfo { min-width: auto; max-width: 100%; max-height: 12rem; padding: 0.35rem 0.4rem 0.2rem; } } ================================================ FILE: src/styles/console.m.scss ================================================ c-toggler { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAE9JREFUOMtjYBh0gBHO+k+cSkYilcPVMpHqpIHRwIgUFERp+I8SekQ5CY8WXH7AqYWJyGglqIERV3QykaYcV7DiSSwsODw8yJIGdTPQIAQAg9gKJl7UINwAAAAASUVORK5CYII=); background-position: center; background-repeat: no-repeat; background-size: 24px; position: fixed; top: 0; left: 0; height: 30px; width: 30px; background-color: #fff; transform-origin: center; border-radius: 50%; box-shadow: -2px 2px 8px rgba(0, 0, 0, .4); z-index: 99999; opacity: 0.5; } c-object { color: #9999ff; text-decoration: underline; } c-toggler:active { box-shadow: -1px 1px 4px rgba(0, 0, 0, .4) } c-line { display: block; } c-console { box-sizing: border-box; overflow-y: auto; position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; background-color: #313131; z-index: 99998; color: #eeeeee; font-family: var(--app-font-family); } c-console[title] { padding-top: 65px; animation: --page-transition .1s ease 1; } c-console br:last-of-type { display: none; } c-console textarea { color: white; caret-color: currentColor !important; background-color: inherit; } c-input { display: flex; width: 100%; height: fit-content; } c-input::before { content: '>>'; margin: 0 5px; height: 100%; } #__c-input { width: 100%; border: none; resize: none; height: 200px; position: relative; background-color: transparent; overflow: visible; } #__c-input:focus { outline: none; } c-console[title]::before { position: fixed; top: 0; left: 0; width: 100vw; background-color: inherit; z-index: 999999; content: attr(title); display: flex; height: 44px; align-items: center; justify-content: center; font-family: Verdana, Geneva, Tahoma, sans-serif; font-weight: 900; box-shadow: 0 2px 4px rgba(0, 0, 0, .2); margin-bottom: 10px; color: white; font-size: medium; } c-message { position: relative; display: flex; border-bottom: solid 1px rgba(204, 204, 204, 0.4); margin-bottom: 35px; font-size: .9rem; flex-wrap: wrap; } c-code { position: relative; color: rgb(214, 211, 211); font-size: 1em; font-family: 'Courier New', Courier, monospace; overflow-x: auto; white-space: pre; margin-bottom: 0px; border: 'none'; } c-code::after { content: 'use'; background-color: #666; color: inherit; border-radius: 4px; padding: 0 0.4rem; font-size: 0.6rem; } c-code::before { content: '>>'; padding: 0 5px; font-style: italic; } c-key { font-size: 0.9rem; color: #cc66ff; } c-message[log-level=error] { border-bottom: solid 1px rgba(255, 255, 255, 0.4); background-color: #422; color: inherit; } c-message[log-level=error]::after { background-color: #cc4343; color: inherit } c-message[log-level=warn] { border-bottom: solid 1px rgba(255, 255, 255, 0.4); background-color: #633; color: inherit; } c-message[log-level=warn]::after { background-color: #cc6969; color: inherit } c-stack:not(:empty) { content: attr(data-stack); font-family: Verdana, Geneva, Tahoma, sans-serif; position: absolute; top: 100%; right: 0; display: flex; height: 20px; align-items: center; justify-content: space-between; width: 100vw; background-color: inherit; padding: 0 5px; box-sizing: border-box; font-size: .8rem; color: inherit; } c-text { padding: 2px; white-space: pre; font-family: Verdana, Geneva, Tahoma, sans-serif; overflow: auto; box-sizing: border-box; max-width: 100vw; font-size: 0.9rem; width: 100%; padding-left: 10px; white-space: break-spaces; } c-text.__c-boolean { color: rgb(130, 80, 177); } c-text.__c-number { color: rgb(97, 88, 221); } c-text.__c-symbol { color: rgb(111, 89, 172); } c-text.__c-function { color: rgb(145, 136, 168); font-family: 'Courier New', Courier, monospace; font-size: 0.9rem; } c-text.__c-function::before { content: 'ƒ'; margin: 0 2px; font-style: italic; color: #9999ff; } c-text.__c-object, c-text.__c-undefined { color: rgb(118, 163, 118); } c-text.__c-string { color: rgb(59, 161, 59); } c-text.__c-string:not(.no-quotes)::before { content: '"'; margin-right: 2px; } c-text.__c-string:not(.no-quotes)::after { content: '"'; margin-left: 2px; } c-message.error c-text { overflow: unset; white-space: pre-wrap; word-break: break-word; color: white; } c-group { display: none; margin-left: 14px; } c-type[type="body-toggler"].__show-data+c-group { display: block; } c-type[type="body-toggler"]::before { display: inline-block; content: '▸'; margin-right: 2.5px; } c-type[type="body-toggler"]::after { content: '{...}'; } c-type[type="body-toggler"].__show-data::before { content: '▾'; } c-type[type="body-toggler"].__show-data::after { display: none; } c-table { display: table; width: 100%; border-collapse: collapse; border-spacing: 0; font-size: 0.9rem; color: rgb(214, 211, 211); border: solid 1px rgba(204, 204, 204, 0.4); } c-table c-row { display: table-row; border-bottom: solid 1px rgba(204, 204, 204, 0.4); } c-table c-row:last-child { border-bottom: none; } c-table c-row:first-child { font-weight: bold; } c-table c-cell { display: table-cell; padding: 5px; border-bottom: solid 1px rgba(204, 204, 204, 0.4); } c-table c-cell:not(:last-child) { border-left: solid 1px rgba(204, 204, 204, 0.4); } @keyframes --page-transition { 0% { opacity: 0; transform: translate3d(0, 50%, 0) } 100% { opacity: 1; transform: translate3d(0, 0, 0) } } ================================================ FILE: src/styles/fileInfo.scss ================================================ #file-info { overflow-x: hidden; padding: 0.8rem; .file-header { margin-bottom: 1.5rem; .file-name { font-size: 1.25rem; font-weight: 500; color: var(--popup-text-color); display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; .name-part { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; position: relative; &:hover { overflow: visible; white-space: normal; word-break: break-all; z-index: 1; border-radius: 4px; background: var(--popup-background-color); } } .file-extension { background: color-mix( in srgb, var(--button-background-color) 40%, transparent ); padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.875rem; color: var(--active-color); font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; flex-shrink: 0; } } } .info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; } .info-item { display: flex; flex-direction: column; gap: 0.5rem; .info-label { font-size: 0.875rem; color: color-mix(in srgb, var(--popup-text-color) 60%, transparent); text-transform: uppercase; letter-spacing: 0.5px; } .info-value { font-size: 1rem; color: var(--popup-text-color); font-weight: 500; } } .path-section { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--border-color); .path-value { word-break: break-all; } } @media (max-width: 480px) { .file-card { padding: 1.5rem; } .info-grid { grid-template-columns: 1fr; gap: 1rem; } } } ================================================ FILE: src/styles/fonts.scss ================================================ @font-face { font-family: 'Fira Code'; src: url('../res/fonts/FiraCode.ttf') format('truetype'); font-weight: 300 700; font-style: normal; } @font-face { font-family: 'Roboto Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('../res/fonts/RobotoMono.ttf') format('truetype'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } @font-face { font-family: 'Source Code'; src: url('../res/fonts/SourceCodePro.ttf') format('truetype'); font-weight: 300 700; font-style: normal; } @font-face { font-family: 'Cascadia Code'; src: url('../res/fonts/CascadiaCode.ttf') format('truetype'); font-weight: 300 700; font-style: normal; } @font-face { font-family: 'Proggy Clean'; src: url('../res/fonts/ProggyClean.ttf') format('truetype'); font-weight: 300 700; font-style: normal; } @font-face { font-family: 'JetBrains Mono Bold'; src: url('../res/fonts/JetBrainsMono-Bold.ttf') format('truetype'); font-weight: 300 700; } @font-face { font-family: 'JetBrains Mono Regular'; src: url('../res/fonts/JetBrainsMono-Regular.ttf') format('truetype'); font-weight: 300 700; font-style: normal; } @font-face { font-display: swap; font-family: Poppins; font-style: normal; font-weight: 400; src: local('Poppins-Regular'), url('../res/fonts/Poppins-Regular.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-display: swap; font-family: Poppins; font-style: normal; font-weight: 500; src: local('Poppins-Medium'), url('../res/fonts/Poppins-Medium.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: Righteous; font-style: normal; font-weight: 400; src: url('../res/fonts/righteous-latin-regular.woff') format('woff'); } @font-face { font-display: swap; font-family: NotoMono; src: url('../res/fonts/NotoMono-Regular.woff') format("woff"); font-weight: 400; font-style: normal; } @font-face { font-display: swap; font-family: NotoMono; src: url('../res/fonts/CourierNew-Regular.woff2') format("woff2"); font-weight: 400; font-style: normal; unicode-range: U+0590-06FF; } ================================================ FILE: src/styles/keyframes.scss ================================================ @keyframes circular-loader-animation { to { transform: rotate(360deg); } } @keyframes linear-loader-animation { 0% { background-color: rgb(0, 0, 0); transform: scale3d(0, 1, 1); } 50% { background-color: rgb(255, 255, 255); transform: scale3d(4, 1, 1); } 100% { background-color: rgb(0, 0, 0); transform: scale3d(0, 1, 1); } } @keyframes show-searchbar { from { opacity: 0; transform: translate3d(0, -100%, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } } @keyframes show-sidebar { 0% { transform: translate(-100%, 0); } 100% { transform: translate(0, 0); } } @keyframes menu-grow { 0% { transform: scale(0) translateZ(0); opacity: 0; } 80% { opacity: 1; } 100% { transform: scale(1) translateZ(0); } } @keyframes scrollbar-show { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes slide-up { 0% { transform: translateY(100%); } 100% { transform: translateY(0); } } @keyframes hide-loader { from { transform: translateX(-50%) translateY(0) scale3d(1, 1, 1); opacity: 1; } to { transform: translateX(-50%) translateY(-100%) scale3d(0.5, 0.5, 1); opacity: 0; } } @keyframes appear { from { transform: translateX(-50%) translateY(-100%) scale3d(0.5, 0.5, 1); opacity: 0.5; } to { transform: translateX(-50%) translateY(0) scale3d(1, 1, 1); opacity: 1; } } @keyframes sake { 90% { transform: translate3d(0, 0, 0); } 93% { transform: translate3d(-10px, 0, 0); } 97% { transform: translate3d(10px, 0, 0); } 100% { transform: translate3d(0, 0, 0); } } @keyframes move-around { 0% { transform: scaleX(1) translate3d(-100px, 0, 0); background-color: rgb(211, 106, 106); } 25% { background-color: rgb(157, 211, 106); } 50% { transform: scaleX(1) translate3d(100px, 0, 0); background-color: rgb(211, 106, 197); } 75% { background-color: rgb(130, 106, 211); } 100% { transform: scaleX(1) translate3d(-100px, 0, 0); background-color: rgb(211, 106, 106); } } @keyframes slow-appear { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes strech { from { opacity: 0; transform: scale(0, 1); } to { opacity: 1; transform: scale(1, 1); } } @keyframes page-transition { 0% { opacity: 0; transform: translate3d(0, 50%, 0); } 100% { opacity: 1; transform: translate3d(0, 0, 0); } } @keyframes page-transition-opacity { 0% { opacity: 0; } 100% { opacity: 1; } } @keyframes float-appear { 0% { opacity: 0; box-shadow: none; transform: scale(0.6) rotate(360deg) translateZ(0); } 100% { opacity: 1; box-shadow: none; transform: scale(1) rotate(0deg) translateZ(0); } } ================================================ FILE: src/styles/list.scss ================================================ @use "./mixins.scss"; .list { width: 100%; &:not(.collapsible) { overflow-y: auto; } &.collapsible { &.hidden { ul { display: none; } >.tile { >.folder::before { content: "\e92c" !important; } >.indicator::before { content: "\e9bd" !important; } } } >.tile { position: relative; height: 36px; font-size: 1em; background-color: rgba($color: #000000, $alpha: 0.1); &.loading { @include mixins.linear-loader(30%, 2px); } >.folder::before { content: "\e92d"; } >.indicator::before { content: "\e9a6"; } .icon { height: 36px; min-width: 36px; font-size: 1.15em; } } ul { list-style: none; padding-left: 10px; box-sizing: border-box; >.tile { height: 34px; .icon { height: 34px; min-width: 34px; font-size: 1.1em; } } .collapsible { >.tile { background-color: transparent; } } } .icon.lang { padding-right: 5px; font-family: var(--app-font-family); font-weight: bolder; color: rgb(37, 37, 37); color: var(--secondary-text-color); font-weight: 900; text-transform: uppercase; } } >.list-item { display: flex; min-height: 60px; text-decoration: none; margin: auto; box-sizing: border-box; &.disabled { pointer-events: none; opacity: 0.8; } &.no-transform { .value { text-transform: none !important; } } &:not(:last-of-type) { border-bottom: solid 1px rgba(122, 122, 122, 0.227); border-bottom: solid 1px var(--border-color); } &:first-child { .container .value { text-transform: none; } } .container { flex: 1; display: flex; flex-direction: column; overflow: hidden; .text { flex: 1.2; display: flex; align-items: center; .info-button { opacity: 0.5; width: fit-content; height: fit-content; margin-left: 10px; font-size: 0.8rem; } button { background-color: rgb(51, 153, 255); background-color: var(--button-background-color); color: rgb(255, 255, 255); color: var(--button-text-color); border: none; border-radius: 4px; margin: 0 10px; font-size: 0.6rem; padding: 5px; box-sizing: border-box; } .text { span { width: 100%; overflow: hidden; text-overflow: ellipsis; } } } .value { flex: 0.8; display: flex; align-items: center; opacity: 0.6; &.nc { text-transform: none; } } } .icon { height: 60px; width: 60px; &.no-icon { width: 20px; } } .icon { font-size: 1.4em; } * { pointer-events: none; } [data-action], [action], a { pointer-events: all !important; } } } ul { list-style: none; &.list { overflow-x: hidden; overflow-y: auto; li { // max-width: 600px; margin: auto; box-sizing: border-box; &.tile { .icon { &.file { background-position: center; background-size: 34px; } &.folder { color: rgb(206, 206, 53); } } .text { span { width: 100%; overflow: hidden; text-overflow: ellipsis; } } } * { pointer-events: none; overflow: hidden; } [data-action], [action], a { pointer-events: all !important; overflow: auto !important; } &.tile:active { transition: all 300ms ease; background-color: rgba($color: #000000, $alpha: 0.2); } &:last-child { border-bottom: solid 4px rgba(122, 122, 122, 0.227); border-bottom: solid 4px var(--border-color); } } li:last-child { border-bottom: none; } } } .list { &:empty, &.empty { display: flex; align-items: center; justify-content: center; min-height: 40px; &::after { content: attr(empty-msg); color: rgb(37, 37, 37); color: var(--secondary-text-color); text-align: center; font-weight: 900; background: transparent; } } } .list-item, .tile { &:focus { background-color: rgba($color: #000000, $alpha: 0.2); } } ================================================ FILE: src/styles/markdown.scss ================================================ :root { --color-note: #2f81f7; --color-tip: #3fb950; --color-warning: #d29922; --color-severe: #db6d28; --color-caution: #f85149; --color-important: #a371f7; } .md { a { color: #0645ad; text-decoration: none; } a:visited { color: #0b0080; } a:hover { color: #06e; } a:active { color: #faa700; } a:focus { outline: thin dotted; } a:hover, a:active { outline: 0; } ::-moz-selection { background: rgba(255, 255, 0, 0.3); color: #000; } ::selection { background: rgba(255, 255, 0, 0.3); color: #000; } a::-moz-selection { background: rgba(255, 255, 0, 0.3); color: #0645ad; } a::selection { background: rgba(255, 255, 0, 0.3); color: #0645ad; } p { margin: 1em 0; } img { max-width: 100%; } h1 { margin: 0.67em 0; font-weight: 600; padding-bottom: 0.3em; font-size: 2em; border-bottom: 1px solid #bb800926; } h2 { font-weight: 600; padding-bottom: 0.3em; font-size: 1.5em; border-bottom: 1px solid #3d444db3; } h1, h2, h3, h4, h5, h6 { margin-top: 1.5rem; margin-bottom: 1rem; font-weight: 600; line-height: 1.25; } h3 { font-weight: 600; font-size: 1.25em; } h4 { font-weight: 600; font-size: 1em; } h5 { font-weight: 600; font-size: 0.875em; } h6 { font-weight: 600; font-size: 0.85em; color: #9198a1; } blockquote { margin: 0; padding: 0 1em; color: #9198a1; border-left: 0.25em solid #3d444d; } hr { display: block; height: 0; border: 0; border-top: 1px solid var(--border-color, #3d444d); margin: 1.5em 0; padding: 0; } pre, code, samp { color: rgb(107, 107, 107); font-family: monospace, monospace; _font-family: "courier new", monospace; font-size: 0.98em; padding: 0.25rem; } kbd { display: inline-block; padding: 0.25rem; font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; line-height: 10px; color: #f0f6fc; vertical-align: middle; background-color: #151b23; border: solid 1px #3d444db3; border-bottom-color: #3d444db3; border-radius: 6px; box-shadow: inset 0 -1px 0 #3d444db3; } pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; position: relative !important; } pre:hover .copy-button { opacity: 1; } .copy-button { position: absolute; top: 8px; right: 5px; padding: 4px 8px; background: rgba(0, 0, 0, 0.8); border: none; border-radius: 4px; cursor: pointer; opacity: 0; transition: opacity 0.2s; font-size: 12px; color: inherit; &:hover { background: rgba(0, 0, 0, 0.9); } } b, strong { font-weight: bold; } dfn { font-style: italic; } ins { background: #ff9; color: #000; text-decoration: none; } mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } ul, ol { margin: 1em 0; padding: 0 0 0 2em; } ul { list-style-type: disc; } li p:last-child { margin: 0; } dd { margin: 0 0 0 2em; } img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } table { border-collapse: collapse; border-spacing: 0; width: 100%; margin: 1em 0; display: block; overflow-x: auto; th, td { padding: 8px 12px; border: 1px solid var(--border-color, #3d444d); text-align: left; } th { background-color: rgba(255, 255, 255, 0.05); font-weight: 600; } tr:nth-child(even) { background-color: rgba(255, 255, 255, 0.02); } tr:hover { background-color: rgba(255, 255, 255, 0.05); } } td { vertical-align: top; } @media only screen and (min-width: 480px) { body { font-size: 14px; } } @media only screen and (min-width: 768px) { body { font-size: 16px; } } @media print { * { background: transparent !important; color: black !important; filter: none !important; -ms-filter: none !important; } body { font-size: 12pt; max-width: 100%; } a, a:visited { text-decoration: underline; } hr { height: 1px; border: 0; border-bottom: 1px solid black; } a[href]:after { content: " (" attr(href) ")"; } abbr[title]:after { content: " (" attr(title) ")"; } .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } blockquote { border: 1px solid #999; padding-right: 1em; page-break-inside: avoid; } tr, img { page-break-inside: avoid; } img { max-width: 100% !important; } @page :left { margin: 15mm 20mm 15mm 10mm; } @page :right { margin: 15mm 10mm 15mm 20mm; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } } .markdown-alert { padding: 0.5rem 1rem; margin-bottom: 16px; color: inherit; border-left: 0.25em solid #888; } .markdown-alert> :first-child { margin-top: 0; } .markdown-alert> :last-child { margin-bottom: 0; } .markdown-alert .markdown-alert-title { display: flex; font-weight: 500; align-items: center; line-height: 1; } .markdown-alert .markdown-alert-title .octicon { margin-right: 0.5rem; display: inline-block; overflow: visible !important; vertical-align: text-bottom; fill: currentColor; } .markdown-alert.markdown-alert-note { border-left-color: var(--color-note); } .markdown-alert.markdown-alert-note .markdown-alert-title { color: var(--color-note); } .markdown-alert.markdown-alert-important { border-left-color: var(--color-important); } .markdown-alert.markdown-alert-important .markdown-alert-title { color: var(--color-important); } .markdown-alert.markdown-alert-warning { border-left-color: var(--color-warning); } .markdown-alert.markdown-alert-warning .markdown-alert-title { color: var(--color-warning); } .markdown-alert.markdown-alert-tip { border-left-color: var(--color-tip); } .markdown-alert.markdown-alert-tip .markdown-alert-title { color: var(--color-tip); } .markdown-alert.markdown-alert-caution { border-left-color: var(--color-caution); } .markdown-alert.markdown-alert-caution .markdown-alert-title { color: var(--color-caution); } .task-list-item { list-style-type: none; label { font-weight: 400; } &.enabled label { cursor: pointer; } &+.task-list-item { margin-top: 0.25rem; } .handle { display: none; } &-checkbox { margin: 0 0.2em 0.25em -1.4em; vertical-align: middle; border-radius: 4px; cursor: pointer; } } ul:dir(rtl) .task-list-item-checkbox, ol:dir(rtl) .task-list-item-checkbox { margin: 0 -1.6em 0.25em 0.2em; } .contains-task-list { &:hover .task-list-item-convert-container, &:focus-within .task-list-item-convert-container { display: block; width: auto; height: 24px; overflow: visible; clip: auto; } } } ================================================ FILE: src/styles/mixins.scss ================================================ @mixin circular-loader($size) { display: block; width: $size; height: $size; border-radius: 50%; border: 3px solid rgb(153, 153, 255); border: 3px solid var(--popup-icon-color); border-top-color: transparent; animation: circular-loader-animation 1s linear infinite; box-sizing: border-box; } @mixin loader($size) { display: flex; align-items: center; justify-content: center; &::after { content: ''; @include circular-loader($size); } } @mixin linear-loader($width, $height) { &::after { content: ''; display: block; position: absolute; bottom: 0; left: 0; right: 0; margin: 0 auto; width: 30%; height: 2px; background-color: rgb(0, 0, 0); animation: linear-loader-animation ease 1s infinite; border-radius: 1px; } } @mixin active-icon() { background-color: inherit !important; color: rgba(0, 0, 0, 0.2); color: var(--active-color); text-shadow: 0 0 0.5rem var(--box-shadow-color); } @mixin icon-badge() { position: relative; &::after { content: '•'; position: absolute; top: 5px; right: 5px; color: #ffda0c; font-size: 1.4em; height: fit-content; line-height: 4px; text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5); } } ================================================ FILE: src/styles/overrideAceStyle.scss ================================================ .ace_mobile-menu, .ace_tooltip.ace_doc-tooltip { display: none !important; } .ace_editor { &[data-font="Fira Code"] { font-feature-settings: "liga" on, "calt" on; -webkit-font-feature-settings: "liga" on, "calt" on; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; unicode-bidi: isolate; } } .ace_tooltip { background-color: rgb(255, 255, 255); background-color: var(--secondary-color); color: rgb(37, 37, 37); color: var(--secondary-text-color); max-width: 68%; white-space: pre-wrap; } main .ace_editor { textarea { user-select: none !important; pointer-events: none !important; transform: translate(-100000px, -10000px) !important; } } .ace-container { height: 100%; } .ace_dark.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line { background-color: rgba(from var(--active-color) r g b / 0.3); } .ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight { color: var(--popup-active-color); } .ace_dark.ace_editor.ace_autocomplete { border: 1px solid var(--popup-border-color); box-shadow: 2px 3px 5px var(--box-shadow-color); line-height: 1.4; background: var(--primary-color); color: var(--primary-text-color); } .ace_hidden-cursors .ace_cursor { opacity: 0.8 !important; } ================================================ FILE: src/styles/page.scss ================================================ body { &.no-animation { footer { border-top: solid 1px rgba(0, 0, 0, 0.2); } .header, header { &.tile { border-bottom: solid 1px rgba(0, 0, 0, 0.2); box-sizing: border-box; } } } &.fullscreen-mode { .open-file-list { height: 30px; left: 40px; } wc-page { &#root { &[open-file-list-pos="bottom"] { &[footer-height="1"] { header { bottom: 40px; } #sidebar-toggler { bottom: 40px; } } &[footer-height="2"] { header { bottom: 80px; } #sidebar-toggler { bottom: 80px; } } &[footer-height="3"] { header { bottom: 120px; } #sidebar-toggler { bottom: 120px; } } header { bottom: 0; top: auto !important; z-index: 99; } #sidebar-toggler { bottom: 0; top: auto !important; z-index: 99; transition: none !important; } } &:not(.top-bar) { &.show-header { #header-toggler { opacity: 1; transform: translate3d(-110px, 0, 0); } >header { top: 10px; right: 10px; height: 40px; display: flex; border-radius: 20px; border: solid 1px rgb(255, 255, 255); border: solid 1px var(--primary-text-color); } } #header-toggler { display: flex; } } &.top-bar { #sidebar-toggler { top: 0; left: 0; height: 30px !important; width: 40px !important; opacity: 1; border-radius: 0; border: none; box-shadow: none !important; } .open-file-list { top: 0; width: calc(100% - 140px); } >header { display: flex; justify-content: flex-end; .icon { margin: 0 5px; } } } &.primary { &.top-bar { >main { height: calc(100vh - 30px); min-height: calc(100vh - 30px); } } >main { height: 100vh; min-height: 100vh; } &[footer-height="1"] { &.top-bar { >main { height: calc(100vh - 70px); min-height: calc(100vh - 70px); } } >main { height: calc(100vh - 40px); min-height: calc(100vh - 40px); } } &[footer-height="2"] { &.top-bar { main { height: calc(100vh - 110px); min-height: calc(100vh - 110px); } } main { height: calc(100vh - 80px); min-height: calc(100vh - 80px); } } &[footer-height="3"] { &.top-bar { main { height: calc(100vh - 150px); min-height: calc(100vh - 150px); } } main { height: calc(100vh - 120px); min-height: calc(100vh - 120px); } } } header { display: none; height: 30px; position: fixed; top: 0; right: 0; left: auto; width: 100px; margin: 0; box-shadow: none; .text { display: none; } .icon { height: 28px; width: 28px; margin: auto; &.menu { display: none; } } } #sidebar-toggler { display: flex; } } } } header { .icon { font-size: 1.5em !important; } } } wc-page { position: fixed !important; top: 0; left: 0; transform: rotate(0) translate3d(0, 0, 0); overflow-x: hidden; overflow-y: auto; background-color: rgb(255, 255, 255); background-color: var(--secondary-color); z-index: 108; height: 100%; width: 100%; box-sizing: border-box; &[open-file-list-pos="bottom"] { &#root { footer { box-shadow: 0 -32px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 -32px 4px var(--box-shadow-color); } .open-file-list { top: auto !important; bottom: 0; z-index: 99; position: absolute; } } #quicktools-toggler { margin-bottom: 30px; } } .main { >.options { display: flex; height: 40px; min-height: 40px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 4px var(--box-shadow-color); >* { position: relative; display: flex; flex: 1; align-items: center; justify-content: center; box-sizing: border-box; &.active::after { content: ""; position: absolute; bottom: 0; left: 0; height: 2px; width: 100%; animation: strech 300ms ease 1; background-color: rgb(51, 153, 255); background-color: var(--active-color); } } } } &.hide-floating-button { #quicktools-toggler { display: none; } } &[footer-height] { #quicktools-toggler { opacity: 1; } } &[footer-height="1"] { &[open-file-list-pos="bottom"] { &#root { .open-file-list { bottom: 40px; } } } #quicktools-toggler { transform: translate3d(0, -40px, 0); } &.top-bar { main, .main { height: calc(100vh - 115px); min-height: calc(100vh - 115px); } } main, .main { height: calc(100vh - 85px); min-height: calc(100vh - 85px); } } &[footer-height="2"] { &[open-file-list-pos="bottom"] { &#root { .open-file-list { bottom: 80px; } } } #quicktools-toggler { transform: translate3d(0, -80px, 0); } &.top-bar { .main, main { height: calc(100vh - 155px); min-height: calc(100vh - 155px); } } .main, main { height: calc(100vh - 125px); min-height: calc(100vh - 125px); } } &[footer-height="3"] { &[open-file-list-pos="bottom"] { &#root { .open-file-list { bottom: 120px; } } } #quicktools-toggler { transform: translate3d(0, -140px, 0); } &.top-bar { .main, main { height: calc(100vh - 195px); min-height: calc(100vh - 195px); } } .main, main { height: calc(100vh - 165px); min-height: calc(100vh - 165px); } } header { position: sticky; top: 0; left: 0; width: 100%; } &.top-bar { main { height: calc(100vh - 75px); min-height: calc(100vh - 75px); } } main { position: relative; &:empty { position: fixed; top: 45px; height: calc(100% - 45px); width: 100%; display: flex; &::after { content: attr(data-empty-msg); font-weight: 900; color: rgb(153, 153, 153); height: fit-content; max-width: 60vw; margin: auto; text-align: center; font-size: 1.6em; } } } footer { position: fixed; bottom: 0; left: 0; width: 100%; z-index: 98; background-color: rgb(153, 153, 255); background-color: var(--primary-color); padding: 0 1px; box-sizing: border-box; box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 -2px 4px var(--box-shadow-color); .icon { border-radius: 0; } } &.primary { z-index: auto; } &:not(.primary):not(.no-transition) { animation: page-transition 200ms ease 1; } &.no-transition { animation: page-transition-opacity 200ms ease 1; } &:not(#root) { z-index: 110; } &.hide { transition: 300ms ease; opacity: 0; transform: translate3d(0, 50%, 0); } main, .main { height: calc(100vh - 45px); min-height: calc(100vh - 45px); margin: auto; z-index: 1; .navigation { display: flex; height: 30px; border-bottom: solid 1px rgba(122, 122, 122, 0.227); border-bottom: solid 1px var(--border-color); box-sizing: border-box; overflow-x: auto; &::after { content: ""; display: block; min-width: 10px; } .nav { display: flex; height: 25px; margin: auto 1.5px; font-size: 0.9em; box-sizing: border-box; align-items: center; color: color-mix(in srgb, var(--primary-text-color) 60%, transparent); transition: color 0.2s; &::after { content: attr(text); padding: 2.5px; box-sizing: border-box; white-space: nowrap; word-break: keep-all; cursor: pointer; } &:first-of-type { margin-left: 10px; } &:last-of-type::after { color: var(--active-color); font-weight: 500; } &:not(:first-of-type)::before { font-family: "code-editor-icon"; content: "\E99E"; margin: auto 1.5px; } } } } .editor-container { height: 100%; } } ================================================ FILE: src/styles/wideScreen.scss ================================================ @media screen and (min-width: 1024px) { html, body { font-size: 15px; } .header, header { height: 60px; >.text { font-size: 1.4em; } } .section, .button-container { min-height: 45px; .icon { font-size: 1.4em; } } input { height: 45px; font-size: 1.1em; text-indent: 12px; } .floating.icon { height: 65px; width: 65px; font-size: 2rem; top: 15px; right: 15px; box-shadow: 0 0 2px 4px rgba(0, 0, 0, 0.2); } .context-menu { min-width: 260px; max-width: 400px; min-height: 48px; li { height: 55px; font-size: 1.05em; &.notice::after { font-size: 1.3em; } .text { .value { font-size: 0.85em; } } .icon { font-size: 1.1em; } } } #search-bar { height: 50px; input { margin: 8px; border-radius: 6px; font-size: 1.1em; padding: 0 12px; } .icon { height: 50px; width: 50px; font-size: 1.3em; } } .tile { .icon { height: 60px; width: 60px; font-size: 2.5em; background-size: 2em; } } .prompt { max-width: 400px; &.select { min-width: 280px; max-width: 420px; ul { padding: 15px; li { height: 48px; font-size: 1.1em; margin: 2px 0; padding: 0 8px; .icon { font-size: 1.1em; margin-right: 8px; } } } } ul li { height: 48px; font-size: 1.1em; } .title { font-size: 1.35em; &:not(:empty) { min-height: 48px; margin: 8px 15px 0 15px; } } .message { .message, &:not(.loader) { padding: 15px; min-height: 48px; font-size: 1.3em; } } .input-group { min-height: 48px; margin: 4px auto; max-width: 360px; .hero { height: 48px; font-size: 1.3em; } .input-checkbox { height: 48px; } } .input { max-width: 360px; font-size: 1.1em; margin: 4px auto; padding: 4px 0; } } #plugin { .list-item { margin: 0 1rem 0.75rem 1rem; padding: 1.25rem; border-radius: 16px; .plugin-header { gap: 1.25rem; min-height: 56px; .plugin-icon { width: 56px; height: 56px; border-radius: 14px; } .plugin-info { gap: 1.25rem; .plugin-main { .plugin-title { gap: 1rem; margin-bottom: 0.5rem; .plugin-name { font-size: 1.1rem; font-weight: 700; } .plugin-version { font-size: 0.8rem; padding: 0.3rem 0.6rem; } } .plugin-meta { font-size: 0.95rem; gap: 0.75rem; .plugin-stats { font-size: 0.95rem; gap: 0.5rem; .icon { width: 16px; height: 16px; font-size: 16px; } } } } .plugin-price { padding: 0.5rem 1rem; font-size: 0.95rem; border-radius: 10px; } } } .plugin-toggle-switch { min-width: auto; padding: 0; .plugin-toggle-track { width: 2.8rem; height: 1.65rem; border-radius: 999px; } .plugin-toggle-thumb { width: 1.25rem; height: 1.25rem; left: 0; top: 0; } &[data-enabled="true"] .plugin-toggle-thumb { transform: translateX(1.12rem); } } } } #sidebar { .container { .header { height: 100px; } >.list[class] { &.hidden[class] { max-height: 40px; min-height: 40px; } >ul { max-height: calc(100% - 40px); height: calc(100% - 40px); } } &.extensions { .header { padding: 12px; .title { min-height: 40px; font-size: 1.25rem; .icon-button { padding: 0.5rem; margin-left: 0.5rem; border-radius: 8px; min-width: 36px; min-height: 36px; font-size: 1.1em; } } input[type="search"] { padding: 0.6rem; font-size: 0.95rem; } } .tile { min-height: 40px; padding: 5px 12px; .text { &.sub-text { font-size: 0.85rem; &::after { font-size: 0.75rem; } } } } .icon { background-size: 26px !important; } } &.notifications { .header { height: unset; } } &.search-in-files { .header { min-height: 100px; height: fit-content; } } } } .list { &.collapsible { >.tile { height: 35px; font-size: 1.1em; .icon { height: 35px; min-width: 35px; } } } ul { >.tile { height: 35px; .icon { height: 32px; min-width: 32px; font-size: 1.1em; } } } >.list-item { min-height: 65px; font-size: 1.1em; .icon { height: 65px; width: 65px; font-size: 1.6em; &.no-icon { width: 25px; } } } } w-page { .main { >.options { height: 45px; min-height: 45px; font-size: 1.1em; >* { &.active::after { height: 3px; } } } } &[footer-height="1"] { #quicktools-toggler { transform: translate3d(0, -45px, 0); } } &[footer-height="2"] { #quicktools-toggler { transform: translate3d(0, -100px, 0); } } &[footer-height="3"] { #quicktools-toggler { transform: translate3d(0, -150px, 0); } } main, .main { .navigation { height: 35px; .nav { height: 30px; font-size: 1em; margin: auto 2px; &::after { padding: 3px; } &:first-of-type { margin-left: 15px; } &:not(:first-of-type)::before { margin: auto 2px; } } } } } } ================================================ FILE: src/test/ace.test.js ================================================ import { TestRunner } from "./tester"; /** * Ace Editor API Compatibility Tests * * These tests validate that the CodeMirror-based editor (from editorManager) * properly implements the Ace Editor API compatibility layer. */ export async function runAceCompatibilityTests(writeOutput) { const runner = new TestRunner("Ace API Compatibility"); function getEditor() { return editorManager?.editor; } async function createTestFile(text = "") { const EditorFile = acode.require("editorFile"); const file = new EditorFile("__ace_test__.txt", { text, render: true, }); await new Promise((r) => setTimeout(r, 100)); return file; } runner.test("editorManager.editor exists", (test) => { test.assert( typeof editorManager !== "undefined", "editorManager should exist", ); test.assert( editorManager.editor != null, "editorManager.editor should exist", ); }); runner.test("editorManager isCodeMirror flag", (test) => { test.assertEqual(editorManager.isCodeMirror, true); }); runner.test("editor.getValue()", (test) => { const editor = getEditor(); test.assert( typeof editor.getValue === "function", "getValue should be a function", ); const value = editor.getValue(); test.assert(typeof value === "string", "getValue should return string"); }); runner.test("editor.insert()", (test) => { const editor = getEditor(); test.assert( typeof editor.insert === "function", "insert should be a function", ); }); runner.test("editor.getCursorPosition()", (test) => { const editor = getEditor(); test.assert( typeof editor.getCursorPosition === "function", "getCursorPosition should exist", ); const pos = editor.getCursorPosition(); test.assert(typeof pos.row === "number", "row should be number"); test.assert(typeof pos.column === "number", "column should be number"); }); runner.test("editor.gotoLine()", (test) => { const editor = getEditor(); test.assert( typeof editor.gotoLine === "function", "gotoLine should be a function", ); }); runner.test("editor.moveCursorToPosition()", (test) => { const editor = getEditor(); test.assert( typeof editor.moveCursorToPosition === "function", "moveCursorToPosition should exist", ); }); runner.test("editor.selection object", (test) => { const editor = getEditor(); test.assert(editor.selection != null, "selection should exist"); }); runner.test("editor.selection.getRange()", (test) => { const editor = getEditor(); test.assert( typeof editor.selection.getRange === "function", "getRange should be a function", ); const range = editor.selection.getRange(); test.assert(range.start != null, "range should have start"); test.assert(range.end != null, "range should have end"); }); runner.test("editor.getSelectionRange()", (test) => { const editor = getEditor(); test.assert( typeof editor.getSelectionRange === "function", "getSelectionRange should be a function", ); const range = editor.getSelectionRange(); test.assert(range.start != null, "range should have start"); test.assert(range.end != null, "range should have end"); }); runner.test("editor.scrollToRow()", (test) => { const editor = getEditor(); test.assert( typeof editor.scrollToRow === "function", "scrollToRow should be a function", ); const ok = editor.scrollToRow(0); test.assert(ok === true || ok === undefined, "scrollToRow should not fail"); }); runner.test("editor.selection.getCursor()", (test) => { const editor = getEditor(); test.assert( typeof editor.selection.getCursor === "function", "getCursor should be a function", ); const pos = editor.selection.getCursor(); test.assert(typeof pos.row === "number", "row should be number"); test.assert(typeof pos.column === "number", "column should be number"); }); runner.test("editor.getCopyText()", (test) => { const editor = getEditor(); test.assert( typeof editor.getCopyText === "function", "getCopyText should exist", ); const text = editor.getCopyText(); test.assert(typeof text === "string", "should return string"); }); runner.test("editor.session exists", async (test) => { const testFile = await createTestFile("test"); const editor = getEditor(); test.assert(editor.session != null, "session should exist"); testFile.remove(false); }); runner.test("editor.setTheme()", (test) => { const editor = getEditor(); test.assert( typeof editor.setTheme === "function", "setTheme should be a function", ); }); runner.test("editor.commands object", (test) => { const editor = getEditor(); test.assert(editor.commands != null, "commands should exist"); }); runner.test("editor.commands.addCommand()", (test) => { const editor = getEditor(); test.assert( typeof editor.commands.addCommand === "function", "addCommand should be a function", ); }); runner.test("editor.commands.removeCommand()", (test) => { const editor = getEditor(); test.assert( typeof editor.commands.removeCommand === "function", "removeCommand should exist", ); }); runner.test("editor.commands.commands getter", (test) => { const editor = getEditor(); const cmds = editor.commands.commands; test.assert( typeof cmds === "object" && cmds !== null, "commands should return object", ); }); runner.test("editor.execCommand()", (test) => { const editor = getEditor(); test.assert( typeof editor.execCommand === "function", "execCommand should be a function", ); }); runner.test("editor.focus()", (test) => { const editor = getEditor(); test.assert( typeof editor.focus === "function", "focus should be a function", ); }); runner.test("editor.state (CodeMirror)", (test) => { const editor = getEditor(); test.assert(editor.state != null, "state should exist"); }); runner.test("editor.dispatch (CodeMirror)", (test) => { const editor = getEditor(); test.assert( typeof editor.dispatch === "function", "dispatch should be a function", ); }); runner.test("editor.contentDOM (CodeMirror)", (test) => { const editor = getEditor(); test.assert(editor.contentDOM != null, "contentDOM should exist"); }); runner.test("ace.require('ace/ext/modelist')", (test) => { test.assert(window.ace != null, "window.ace should exist"); test.assert( typeof window.ace.require === "function", "ace.require should be a function", ); const modelist = window.ace.require("ace/ext/modelist"); test.assert(modelist != null, "modelist should be available"); test.assert( typeof modelist.getModeForPath === "function", "modelist.getModeForPath should be a function", ); }); // Session API tests runner.test("session.getValue()", async (test) => { const testFile = await createTestFile("test content"); const editor = getEditor(); test.assert( typeof editor.session.getValue === "function", "getValue should exist", ); const value = editor.session.getValue(); test.assert(typeof value === "string", "should return string"); test.assertEqual(value, "test content"); testFile.remove(false); }); runner.test("session.setValue()", async (test) => { const testFile = await createTestFile("original"); const editor = getEditor(); test.assert( typeof editor.session.setValue === "function", "setValue should exist", ); editor.session.setValue("modified"); test.assertEqual(editor.session.getValue(), "modified"); testFile.remove(false); }); runner.test("session.getLength()", async (test) => { const testFile = await createTestFile("line1\nline2\nline3"); const editor = getEditor(); test.assert( typeof editor.session.getLength === "function", "getLength should exist", ); const len = editor.session.getLength(); test.assert(typeof len === "number", "should return number"); test.assertEqual(len, 3); testFile.remove(false); }); runner.test("session.getLine()", async (test) => { const testFile = await createTestFile("first\nsecond\nthird"); const editor = getEditor(); test.assert( typeof editor.session.getLine === "function", "getLine should exist", ); test.assertEqual(editor.session.getLine(0), "first"); test.assertEqual(editor.session.getLine(1), "second"); test.assertEqual(editor.session.getLine(2), "third"); testFile.remove(false); }); return await runner.run(writeOutput); } ================================================ FILE: src/test/editor.tests.js ================================================ import { history, isolateHistory, redo, undo } from "@codemirror/commands"; import { bracketMatching, defaultHighlightStyle, foldGutter, syntaxHighlighting, } from "@codemirror/language"; import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; import { EditorSelection, EditorState } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import createBaseExtensions from "cm/baseExtensions"; import indentGuides from "cm/indentGuides"; import { getEdgeScrollDirections } from "cm/touchSelectionMenu"; import { TestRunner } from "./tester"; export async function runCodeMirrorTests(writeOutput) { const runner = new TestRunner("CodeMirror 6 Editor Tests"); function createEditor(doc = "", extensions = []) { const container = document.createElement("div"); container.style.width = "500px"; container.style.height = "300px"; container.style.backgroundColor = "#1e1e1e"; document.body.appendChild(container); const state = EditorState.create({ doc, extensions: [...createBaseExtensions(), ...extensions], }); const view = new EditorView({ state, parent: container }); return { view, container }; } async function withEditor(test, fn, initialDoc = "", extensions = []) { let view, container; try { ({ view, container } = createEditor(initialDoc, extensions)); test.assert(view != null, "EditorView instance should be created"); await new Promise((resolve) => setTimeout(resolve, 100)); await fn(view); await new Promise((resolve) => setTimeout(resolve, 200)); } finally { if (view) view.destroy(); if (container) container.remove(); } } // ========================================= // BASIC EDITOR TESTS // ========================================= runner.test("CodeMirror imports available", async (test) => { test.assert( typeof EditorView !== "undefined", "EditorView should be defined", ); test.assert( typeof EditorState !== "undefined", "EditorState should be defined", ); test.assert( typeof EditorState.create === "function", "EditorState.create should be a function", ); }); runner.test("Acode exposes shared CodeMirror modules", async (test) => { const codemirror = acode.require("codemirror"); const language = acode.require("@codemirror/language"); const lezer = acode.require("@lezer/highlight"); const state = acode.require("@codemirror/state"); const view = acode.require("@codemirror/view"); test.assert(codemirror != null, "codemirror namespace should exist"); test.assert(language != null, "@codemirror/language should exist"); test.assert(lezer != null, "@lezer/highlight should exist"); test.assert(state != null, "@codemirror/state should exist"); test.assert(view != null, "@codemirror/view should exist"); test.assert( language.StreamLanguage != null, "@codemirror/language should export StreamLanguage", ); test.assert(lezer.tags != null, "@lezer/highlight should export tags"); test.assert( state.EditorState != null, "@codemirror/state should export EditorState", ); test.assert( view.EditorView != null, "@codemirror/view should export EditorView", ); test.assertEqual( language.StreamLanguage, codemirror.language.StreamLanguage, "language exports should share the same singleton instance", ); test.assertEqual( lezer.tags, codemirror.lezer.tags, "lezer exports should share the same singleton instance", ); test.assertEqual( state.EditorState, codemirror.state.EditorState, "state exports should share the same singleton instance", ); test.assertEqual( view.EditorView, codemirror.view.EditorView, "view exports should share the same singleton instance", ); }); runner.test("Editor creation", async (test) => { const { view, container } = createEditor(); test.assert(view != null, "EditorView instance should be created"); test.assert(view.dom instanceof HTMLElement, "Editor should have DOM"); test.assert(view.state instanceof EditorState, "Editor should have state"); view.destroy(); container.remove(); }); runner.test("State access", async (test) => { await withEditor(test, async (view) => { const state = view.state; test.assert(state != null, "Editor state should exist"); test.assert(typeof state.doc !== "undefined", "State should have doc"); test.assert( typeof state.doc.toString === "function", "Doc should have toString", ); }); }); runner.test("Set and get document content", async (test) => { await withEditor(test, async (view) => { const text = "Hello CodeMirror 6"; view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text }, }); test.assertEqual(view.state.doc.toString(), text); }); }); // ========================================= // CURSOR AND SELECTION TESTS // ========================================= runner.test("Cursor movement", async (test) => { await withEditor(test, async (view) => { const doc = "line1\nline2\nline3"; view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: doc }, }); const line2 = view.state.doc.line(2); const targetPos = line2.from + 2; view.dispatch({ selection: { anchor: targetPos, head: targetPos }, }); const pos = view.state.selection.main.head; const lineInfo = view.state.doc.lineAt(pos); test.assertEqual(lineInfo.number, 2); test.assertEqual(pos - lineInfo.from, 2); }); }); runner.test("Selection handling", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "abc\ndef" }, }); view.dispatch({ selection: { anchor: 0, head: view.state.doc.length }, }); const { from, to } = view.state.selection.main; const selectedText = view.state.doc.sliceString(from, to); test.assert(selectedText.length > 0, "Should have selected text"); test.assertEqual(selectedText, "abc\ndef"); }); }); runner.test("Multiple selections", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "foo bar foo" }, }); view.dispatch({ selection: EditorSelection.create([ EditorSelection.range(0, 3), EditorSelection.range(8, 11), ]), }); test.assertEqual(view.state.selection.ranges.length, 2); test.assertEqual(view.state.doc.sliceString(0, 3), "foo"); test.assertEqual(view.state.doc.sliceString(8, 11), "foo"); }); }); runner.test("Selection with cursor (empty range)", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "hello world" }, }); view.dispatch({ selection: EditorSelection.cursor(5), }); const main = view.state.selection.main; test.assertEqual(main.from, 5); test.assertEqual(main.to, 5); test.assert(main.empty, "Cursor selection should be empty"); }); }); // ========================================= // HISTORY (UNDO/REDO) TESTS // ========================================= runner.test("Undo works", async (test) => { const { view, container } = createEditor("one"); try { view.dispatch({ changes: { from: 3, insert: "\ntwo" }, }); test.assertEqual(view.state.doc.toString(), "one\ntwo"); undo(view); test.assertEqual(view.state.doc.toString(), "one"); } finally { view.destroy(); container.remove(); } }); runner.test("Redo works", async (test) => { const { view, container } = createEditor("one"); try { view.dispatch({ changes: { from: 3, insert: "\ntwo" }, }); undo(view); test.assertEqual(view.state.doc.toString(), "one"); redo(view); test.assertEqual(view.state.doc.toString(), "one\ntwo"); } finally { view.destroy(); container.remove(); } }); runner.test("Multiple undo steps", async (test) => { const { view, container } = createEditor(""); try { // Use isolateHistory to force each change into separate history entries view.dispatch({ changes: { from: 0, insert: "a" }, annotations: isolateHistory.of("full"), }); view.dispatch({ changes: { from: 1, insert: "b" }, annotations: isolateHistory.of("full"), }); view.dispatch({ changes: { from: 2, insert: "c" }, annotations: isolateHistory.of("full"), }); test.assertEqual(view.state.doc.toString(), "abc"); undo(view); undo(view); test.assertEqual(view.state.doc.toString(), "a"); } finally { view.destroy(); container.remove(); } }); // ========================================= // DOCUMENT MANIPULATION TESTS // ========================================= runner.test("Line count", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "a\nb\nc\nd" }, }); test.assertEqual(view.state.doc.lines, 4); }); }); runner.test("Insert text at position", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "hello world" }, }); view.dispatch({ changes: { from: 5, to: 5, insert: " there" }, }); test.assertEqual(view.state.doc.toString(), "hello there world"); }); }); runner.test("Replace text range", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "hello world" }, }); view.dispatch({ changes: { from: 6, to: 11, insert: "cm6" }, }); test.assertEqual(view.state.doc.toString(), "hello cm6"); }); }); runner.test("Delete text", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "hello world" }, }); view.dispatch({ changes: { from: 5, to: 11, insert: "" }, }); test.assertEqual(view.state.doc.toString(), "hello"); }); }); runner.test("Batch changes", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "aaa bbb ccc" }, }); view.dispatch({ changes: [ { from: 0, to: 3, insert: "xxx" }, { from: 4, to: 7, insert: "yyy" }, { from: 8, to: 11, insert: "zzz" }, ], }); test.assertEqual(view.state.doc.toString(), "xxx yyy zzz"); }); }); runner.test("Line information", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "line one\nline two\nline three", }, }); const line2 = view.state.doc.line(2); test.assertEqual(line2.number, 2); test.assertEqual(line2.text, "line two"); test.assert(line2.from > 0, "Line 2 should have positive from"); }); }); runner.test("Position conversions", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: "abc\ndefgh\nij", }, }); const pos = 7; // 'g' in "defgh" const lineInfo = view.state.doc.lineAt(pos); test.assertEqual(lineInfo.number, 2); test.assertEqual(lineInfo.text, "defgh"); test.assertEqual(pos - lineInfo.from, 3); }); }); runner.test("Empty document handling", async (test) => { await withEditor(test, async (view) => { test.assertEqual(view.state.doc.length, 0); test.assertEqual(view.state.doc.lines, 1); test.assertEqual(view.state.doc.toString(), ""); }); }); // ========================================= // DOM AND VIEW TESTS // ========================================= runner.test("DOM elements exist", async (test) => { await withEditor(test, async (view) => { test.assert(view.dom != null, "view.dom should exist"); test.assert(view.scrollDOM != null, "view.scrollDOM should exist"); test.assert(view.contentDOM != null, "view.contentDOM should exist"); }); }); runner.test("Indent guides render as indentation spans", async (test) => { const doc = "function x() {\n if (true) {\n return 1;\n }\n}"; await withEditor( test, async (view) => { const guideLine = view.dom.querySelector(".cm-indent-guides"); const legacyWidget = view.dom.querySelector( ".cm-indent-guides-wrapper", ); test.assert(guideLine != null, "Indent guide span should exist"); test.assert( legacyWidget == null, "Indent guides should not create widget wrapper DOM", ); }, doc, [indentGuides()], ); }); runner.test("Focus and blur", async (test) => { await withEditor(test, async (view) => { view.focus(); await new Promise((resolve) => setTimeout(resolve, 50)); test.assert(view.hasFocus, "Editor should have focus"); view.contentDOM.blur(); await new Promise((resolve) => setTimeout(resolve, 50)); test.assert(!view.hasFocus, "Editor should not have focus after blur"); }); }); runner.test("Scroll API", async (test) => { await withEditor(test, async (view) => { const longDoc = Array(100).fill("line").join("\n"); view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: longDoc }, }); const line50 = view.state.doc.line(50); view.dispatch({ effects: EditorView.scrollIntoView(line50.from, { y: "center" }), }); await new Promise((resolve) => setTimeout(resolve, 100)); test.assert( view.scrollDOM.scrollTop >= 0, "scrollTop should be accessible", ); }); }); runner.test("Viewport info", async (test) => { await withEditor(test, async (view) => { const longDoc = Array(200).fill("some text content").join("\n"); view.dispatch({ changes: { from: 0, insert: longDoc }, }); const viewport = view.viewport; test.assert(typeof viewport.from === "number", "viewport.from exists"); test.assert(typeof viewport.to === "number", "viewport.to exists"); test.assert(viewport.to > viewport.from, "viewport has range"); }); }); // ========================================= // CODEMIRROR-SPECIFIC FEATURES // ========================================= runner.test("EditorState facets", async (test) => { const { view, container } = createEditor("test"); try { const readOnly = view.state.facet(EditorState.readOnly); test.assert(typeof readOnly === "boolean", "readOnly facet exists"); test.assertEqual(readOnly, false); } finally { view.destroy(); container.remove(); } }); runner.test("Read-only facet value", async (test) => { const container = document.createElement("div"); container.style.width = "500px"; container.style.height = "300px"; document.body.appendChild(container); const state = EditorState.create({ doc: "read only content", extensions: [EditorState.readOnly.of(true)], }); const view = new EditorView({ state, parent: container }); try { const isReadOnly = view.state.facet(EditorState.readOnly); test.assertEqual(isReadOnly, true, "Should report as read-only"); } finally { view.destroy(); container.remove(); } }); runner.test("Transaction filtering", async (test) => { let filterCalled = false; const container = document.createElement("div"); container.style.width = "500px"; container.style.height = "300px"; document.body.appendChild(container); const state = EditorState.create({ doc: "original", extensions: [ EditorState.transactionFilter.of((tr) => { if (tr.docChanged) filterCalled = true; return tr; }), ], }); const view = new EditorView({ state, parent: container }); try { view.dispatch({ changes: { from: 0, to: 8, insert: "modified" }, }); test.assert(filterCalled, "Transaction filter should be called"); test.assertEqual(view.state.doc.toString(), "modified"); } finally { view.destroy(); container.remove(); } }); runner.test("Update listener", async (test) => { let updateCount = 0; let docChanged = false; const container = document.createElement("div"); container.style.width = "500px"; container.style.height = "300px"; document.body.appendChild(container); const state = EditorState.create({ doc: "", extensions: [ EditorView.updateListener.of((update) => { updateCount++; if (update.docChanged) docChanged = true; }), ], }); const view = new EditorView({ state, parent: container }); try { view.dispatch({ changes: { from: 0, insert: "hello" }, }); test.assert(updateCount > 0, "Update listener should fire"); test.assert(docChanged, "docChanged should be true"); } finally { view.destroy(); container.remove(); } }); runner.test("State effects", async (test) => { const { StateEffect } = await import("@codemirror/state"); const myEffect = StateEffect.define(); let effectReceived = false; const container = document.createElement("div"); container.style.width = "500px"; container.style.height = "300px"; document.body.appendChild(container); const state = EditorState.create({ doc: "", extensions: [ EditorView.updateListener.of((update) => { for (const tr of update.transactions) { for (const effect of tr.effects) { if (effect.is(myEffect)) { effectReceived = true; } } } }), ], }); const view = new EditorView({ state, parent: container }); try { view.dispatch({ effects: myEffect.of("test-value"), }); test.assert(effectReceived, "Custom state effect should be received"); } finally { view.destroy(); container.remove(); } }); runner.test("Compartments for dynamic config", async (test) => { const { Compartment } = await import("@codemirror/state"); const readOnlyComp = new Compartment(); const container = document.createElement("div"); container.style.width = "500px"; container.style.height = "300px"; document.body.appendChild(container); const state = EditorState.create({ doc: "test", extensions: [readOnlyComp.of(EditorState.readOnly.of(false))], }); const view = new EditorView({ state, parent: container }); try { test.assertEqual(view.state.facet(EditorState.readOnly), false); view.dispatch({ effects: readOnlyComp.reconfigure(EditorState.readOnly.of(true)), }); test.assertEqual(view.state.facet(EditorState.readOnly), true); } finally { view.destroy(); container.remove(); } }); runner.test("Document iteration", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "line1\nline2\nline3" }, }); const lines = []; for (let i = 1; i <= view.state.doc.lines; i++) { lines.push(view.state.doc.line(i).text); } test.assertEqual(lines.length, 3); test.assertEqual(lines[0], "line1"); test.assertEqual(lines[1], "line2"); test.assertEqual(lines[2], "line3"); }); }); runner.test("Text iterator", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "hello world" }, }); const iter = view.state.doc.iter(); let text = ""; while (!iter.done) { text += iter.value; iter.next(); } test.assertEqual(text, "hello world"); }); }); runner.test("Slice string", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "hello world" }, }); test.assertEqual(view.state.doc.sliceString(0, 5), "hello"); test.assertEqual(view.state.doc.sliceString(6, 11), "world"); test.assertEqual(view.state.doc.sliceString(6), "world"); }); }); runner.test("Line at position", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "aaa\nbbb\nccc" }, }); const lineAtStart = view.state.doc.lineAt(0); test.assertEqual(lineAtStart.number, 1); const lineAtMiddle = view.state.doc.lineAt(5); test.assertEqual(lineAtMiddle.number, 2); const lineAtEnd = view.state.doc.lineAt(10); test.assertEqual(lineAtEnd.number, 3); }); }); runner.test("Visible ranges", async (test) => { await withEditor(test, async (view) => { const longDoc = Array(100).fill("content").join("\n"); view.dispatch({ changes: { from: 0, insert: longDoc }, }); const visibleRanges = view.visibleRanges; test.assert(Array.isArray(visibleRanges), "visibleRanges is an array"); test.assert(visibleRanges.length > 0, "Should have visible ranges"); for (const range of visibleRanges) { test.assert(typeof range.from === "number", "range.from exists"); test.assert(typeof range.to === "number", "range.to exists"); } }); }); runner.test("coordsAtPos", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "hello" }, }); const coords = view.coordsAtPos(0); test.assert(coords != null, "coords should exist"); test.assert(typeof coords.left === "number", "coords.left exists"); test.assert(typeof coords.top === "number", "coords.top exists"); }); }); runner.test("posAtCoords", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "hello world" }, }); const rect = view.contentDOM.getBoundingClientRect(); const pos = view.posAtCoords({ x: rect.left + 10, y: rect.top + 10 }); test.assert(pos != null || pos === null, "posAtCoords should return"); }); }); runner.test("Edge scroll direction helper", async (test) => { const rect = { left: 100, right: 300, top: 200, bottom: 400, }; const leftTop = getEdgeScrollDirections({ x: 110, y: 210, rect, allowHorizontal: true, }); test.assertEqual(leftTop.horizontal, -1); test.assertEqual(leftTop.vertical, -1); const rightBottom = getEdgeScrollDirections({ x: 295, y: 395, rect, allowHorizontal: true, }); test.assertEqual(rightBottom.horizontal, 1); test.assertEqual(rightBottom.vertical, 1); const noHorizontal = getEdgeScrollDirections({ x: 110, y: 395, rect, allowHorizontal: false, }); test.assertEqual(noHorizontal.horizontal, 0); test.assertEqual(noHorizontal.vertical, 1); }); runner.test("lineBlockAt", async (test) => { await withEditor(test, async (view) => { view.dispatch({ changes: { from: 0, insert: "line1\nline2\nline3" }, }); const line2Start = view.state.doc.line(2).from; const block = view.lineBlockAt(line2Start); test.assert(block != null, "lineBlockAt should return block"); test.assert(typeof block.from === "number", "block.from exists"); test.assert(typeof block.to === "number", "block.to exists"); test.assert(typeof block.height === "number", "block.height exists"); }); }); return await runner.run(writeOutput); } export { runCodeMirrorTests as runAceEditorTests }; ================================================ FILE: src/test/exec.tests.js ================================================ import { TestRunner } from "./tester"; export async function runExecutorTests(writeOutput) { const runner = new TestRunner("Executor API Tests"); runner.test("Executor available", async (test) => { test.assert( typeof Executor !== "undefined", "Executor should be available globally", ); }); runner.test("Background Executor available", async (test) => { test.assert( typeof Executor.BackgroundExecutor !== "undefined", "Background Executor should be available globally", ); }); runner.test("execute()", async (test) => { test.assert( (await Executor.execute("echo test123")).includes("test123"), "Command output should match", ); }); runner.test("execute() (BackgroundExecutor)", async (test) => { test.assert( (await Executor.BackgroundExecutor.execute("echo test123")).includes( "test123", ), "Command output should match", ); }); runner.test("start()", async (test) => { let stdout = ""; const uuid = await Executor.start("sh", (type, data) => { if (type === "stdout") stdout += data; }); await Executor.write(uuid, "echo hello\n"); await new Promise((r) => setTimeout(r, 200)); await Executor.stop(uuid); await new Promise((r) => setTimeout(r, 200)); test.assert(stdout.includes("hello"), "Shell should echo output"); }); runner.test("start() (BackgroundExecutor)", async (test) => { let stdout = ""; const uuid = await Executor.BackgroundExecutor.start("sh", (type, data) => { if (type === "stdout") stdout += data; }); await Executor.BackgroundExecutor.write(uuid, "echo hello\n"); await new Promise((r) => setTimeout(r, 200)); await Executor.BackgroundExecutor.stop(uuid); await new Promise((r) => setTimeout(r, 200)); test.assert(stdout.includes("hello"), "Shell should echo output"); }); runner.test("start/stop() (BackgroundExecutor)", async (test) => { let stdout = ""; const uuid = await Executor.BackgroundExecutor.start( "sh", (type, data) => {}, ); await new Promise((r) => setTimeout(r, 200)); const isRunning = await Executor.BackgroundExecutor.isRunning(uuid); test.assert(isRunning === true, "Executor must be running"); await new Promise((r) => setTimeout(r, 200)); await Executor.BackgroundExecutor.stop(uuid); await new Promise((r) => setTimeout(r, 200)); test.assert( isRunning !== (await Executor.BackgroundExecutor.isRunning(uuid)), "Executor must be stopped", ); test.assert( (await Executor.BackgroundExecutor.isRunning(uuid)) === false, "Executor must be stopped", ); }); runner.test("start/stop()", async (test) => { let stdout = ""; const uuid = await Executor.start("sh", (type, data) => {}); await new Promise((r) => setTimeout(r, 200)); const isRunning = await Executor.isRunning(uuid); test.assert(isRunning === true, "Executor must be running"); await new Promise((r) => setTimeout(r, 200)); await Executor.stop(uuid); await new Promise((r) => setTimeout(r, 200)); test.assert( (await Executor.isRunning(uuid)) === false, "Executor must be stopped", ); }); runner.test("FDROID env variable", async (test) => { const result = await Executor.execute("echo $FDROID"); const isSet = result.trim().length > 0; test.assert(isSet, "FDROID env variable should be set"); }); return await runner.run(writeOutput); } ================================================ FILE: src/test/sanity.tests.js ================================================ import { TestRunner } from "./tester"; export async function runSanityTests(writeOutput) { const runner = new TestRunner("JS (WebView) Sanity Tests"); // Test 1: String operations runner.test("String concatenation", (test) => { const result = "Hello" + " " + "World"; test.assertEqual(result, "Hello World", "String concatenation should work"); }); // Test 2: Number operations runner.test("Basic arithmetic", (test) => { const sum = 5 + 3; test.assertEqual(sum, 8, "Addition should work correctly"); }); // Test 3: Array operations runner.test("Array operations", (test) => { const arr = [1, 2, 3]; test.assertEqual(arr.length, 3, "Array length should be correct"); test.assert(arr.includes(2), "Array should include 2"); }); // Test 4: Object operations runner.test("Object operations", (test) => { const obj = { name: "Test", value: 42 }; test.assertEqual(obj.name, "Test", "Object property should be accessible"); test.assertEqual(obj.value, 42, "Object value should be correct"); }); // Test 5: Function execution runner.test("Function execution", (test) => { const add = (a, b) => a + b; const result = add(10, 20); test.assertEqual(result, 30, "Function should return correct value"); }); // Test 6: Async function runner.test("Async function handling", async (test) => { const asyncFunc = async () => { return new Promise((resolve) => { setTimeout(() => resolve("done"), 10); }); }; const result = await asyncFunc(); test.assertEqual(result, "done", "Async function should work correctly"); }); // Test 7: Error handling runner.test("Error handling", (test) => { try { throw new Error("Test error"); } catch (e) { test.assert(e instanceof Error, "Should catch Error instances"); } }); // Test 8: Conditional logic runner.test("Conditional logic", (test) => { const value = 10; test.assert(value > 5, "Condition should be true"); test.assert(!(value < 5), "Negation should work"); }); // Run all tests return await runner.run(writeOutput); } ================================================ FILE: src/test/tester.js ================================================ import { runAceCompatibilityTests } from "./ace.test"; import { runCodeMirrorTests } from "./editor.tests"; import { runExecutorTests } from "./exec.tests"; import { runSanityTests } from "./sanity.tests"; import { runUrlTests } from "./url.tests"; export async function runAllTests() { const terminal = acode.require("terminal"); const local = await terminal.createLocal({ name: "TestCode" }); function write(data) { terminal.write(local.id, data); } // Run tests at runtime write("\x1b[36m\x1b[1m🚀 TestCode Plugin Loaded\x1b[0m\n"); write("\x1b[36m\x1b[1mStarting test execution...\x1b[0m\n"); try { // Run unit tests await runSanityTests(write); await runCodeMirrorTests(write); await runAceCompatibilityTests(write); await runExecutorTests(write); await runUrlTests(write); write("\x1b[36m\x1b[1mTests completed!\x1b[0m\n"); } catch (error) { write(`\x1b[31m⚠️ Test execution error: ${error.message}\x1b[0m\n`); } } // ANSI color codes for terminal output const COLORS = { RESET: "\x1b[0m", BRIGHT: "\x1b[1m", DIM: "\x1b[2m", ITALIC: "\x1b[3m", // Foreground colors RED: "\x1b[31m", GREEN: "\x1b[32m", YELLOW: "\x1b[33m", BLUE: "\x1b[34m", MAGENTA: "\x1b[35m", CYAN: "\x1b[36m", WHITE: "\x1b[37m", GRAY: "\x1b[90m", // Background colors BG_RED: "\x1b[41m", BG_GREEN: "\x1b[42m", BG_YELLOW: "\x1b[43m", BG_BLUE: "\x1b[44m", }; function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function startSpinner(writeOutput, text) { let index = 0; let active = true; const timer = setInterval(() => { if (!active) return; const frame = SPINNER_FRAMES[index++ % SPINNER_FRAMES.length]; // \r moves cursor to start, \x1b[K clears the line to the right writeOutput(`\r ${COLORS.CYAN}${frame}${COLORS.RESET} ${text}`); }, 80); return () => { active = false; clearInterval(timer); // Clear the line so the "Success/Fail" message can take its place writeOutput("\r\x1b[K"); }; } // Spinner frames const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; class TestRunner { constructor(name = "Test Suite") { this.name = name; this.tests = []; this.passed = 0; this.failed = 0; this.results = []; this.skipped = 0; } /** * Register a test */ test(testName, testFn) { this.tests.push({ name: testName, fn: testFn }); } /** * Assertions */ assert(condition, message) { if (!condition) { throw new Error(message || "Assertion failed"); } } assertEqual(actual, expected, message) { if (actual !== expected) { throw new Error(message || `Expected ${expected}, got ${actual}`); } } skip(reason = "Skipped") { throw new SkipTest(reason); } async _runWithTimeout(fn, ctx, timeoutMs) { return new Promise((resolve, reject) => { let finished = false; const timer = setTimeout(() => { if (finished) return; finished = true; reject(new Error(`Test timed out after ${timeoutMs}ms`)); }, timeoutMs); Promise.resolve() .then(() => fn(ctx)) .then((result) => { if (finished) return; finished = true; clearTimeout(timer); resolve(result); }) .catch((err) => { if (finished) return; finished = true; clearTimeout(timer); reject(err); }); }); } /** * Run all tests */ async run(writeOutput) { const line = (text = "", color = "") => { writeOutput(`${color}${text}${COLORS.RESET}\n`); }; // Header line(); line( "╔════════════════════════════════════════════╗", COLORS.CYAN + COLORS.BRIGHT, ); line( `║ 🧪 ${this._padCenter(this.name, 35)} │`, COLORS.CYAN + COLORS.BRIGHT, ); line( "╚════════════════════════════════════════════╝", COLORS.CYAN + COLORS.BRIGHT, ); line(); // Run tests with spinner for (const test of this.tests) { const stopSpinner = startSpinner(writeOutput, `Running ${test.name}...`); try { await delay(200); await this._runWithTimeout(test.fn, this, 10000); stopSpinner(); this.passed++; this.results.push({ name: test.name, status: "PASS" }); line(` ${COLORS.GREEN}✓${COLORS.RESET} ${test.name}`, COLORS.GREEN); } catch (error) { stopSpinner(); if (error instanceof SkipTest) { this.skipped++; this.results.push({ name: test.name, status: "SKIP", reason: error.message, }); line( ` ${COLORS.YELLOW}?${COLORS.RESET} ${test.name}`, COLORS.YELLOW + COLORS.BRIGHT, ); line( ` ${COLORS.DIM}└─ ${error.message}${COLORS.RESET}`, COLORS.YELLOW + COLORS.DIM, ); } else { this.failed++; this.results.push({ name: test.name, status: "FAIL", error: error.message, }); line( ` ${COLORS.RED}✗${COLORS.RESET} ${test.name}`, COLORS.RED + COLORS.BRIGHT, ); line( ` ${COLORS.DIM}└─ ${error.message}${COLORS.RESET}`, COLORS.RED + COLORS.DIM, ); } } } // Summary line(); line("─────────────────────────────────────────────", COLORS.GRAY); const total = this.tests.length; const effectiveTotal = total - this.skipped; const percentage = effectiveTotal ? ((this.passed / effectiveTotal) * 100).toFixed(1) : "0.0"; const statusColor = this.failed === 0 ? COLORS.GREEN : COLORS.YELLOW; line( ` Tests: ${COLORS.BRIGHT}${total}${COLORS.RESET} | ` + `${COLORS.GREEN}Passed: ${this.passed}${COLORS.RESET} | ` + `${COLORS.YELLOW}Skipped: ${this.skipped}${COLORS.RESET} | ` + `${COLORS.RED}Failed: ${this.failed}${COLORS.RESET}`, ); line( ` Success Rate: ${statusColor}${percentage}%${COLORS.RESET}`, statusColor, ); line("─────────────────────────────────────────────", COLORS.GRAY); line(); return this.results; } /** * Center text helper */ _padCenter(text, width) { const pad = Math.max(0, width - text.length); return ( " ".repeat(Math.floor(pad / 2)) + text + " ".repeat(Math.ceil(pad / 2)) ); } } class SkipTest extends Error { constructor(message = "Skipped") { super(message); this.name = "SkipTest"; } } export { TestRunner }; ================================================ FILE: src/test/url.tests.js ================================================ import Url from "../utils/Url"; import { TestRunner } from "./tester"; const JOIN_CASES = [ { name: "Android SAF join", folderUrl: "content://com.android.externalstorage.documents/tree/primary%3ATesthtml", activeLocation: "content://com.android.externalstorage.documents/tree/primary%3ATesthtml::primary:Testhtml/Styles/", expectedJoined: "content://com.android.externalstorage.documents/tree/primary%3ATesthtml::primary:Testhtml/Styles/index.html", }, { name: "Termux SAF join", folderUrl: "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome%2Facode-site-ui", activeLocation: "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome%2Facode-site-ui::/data/data/com.termux/files/home/acode-site-ui/", expectedJoined: "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome%2Facode-site-ui::/data/data/com.termux/files/home/acode-site-ui/index.html", }, { name: "Acode SAF join", folderUrl: "content://com.foxdebug.acode.documents/tree/%2Fdata%2Fuser%2F0%2Fcom.foxdebug.acode%2Ffiles%2Fpublic", activeLocation: "content://com.foxdebug.acode.documents/tree/%2Fdata%2Fuser%2F0%2Fcom.foxdebug.acode%2Ffiles%2Fpublic::/data/user/0/com.foxdebug.acode/files/public/", expectedJoined: "content://com.foxdebug.acode.documents/tree/%2Fdata%2Fuser%2F0%2Fcom.foxdebug.acode%2Ffiles%2Fpublic::/data/user/0/com.foxdebug.acode/files/public/index.html", }, ]; const TRAILING_SLASH_CASES = [ { name: "Android SAF trailing slash", a: "content://com.android.externalstorage.documents/tree/primary%3ATesthtml/", b: "content://com.android.externalstorage.documents/tree/primary%3ATesthtml", }, { name: "Termux SAF trailing slash", a: "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome%2Facode-site-ui/", b: "content://com.termux.documents/tree/%2Fdata%2Fdata%2Fcom.termux%2Ffiles%2Fhome%2Facode-site-ui", }, { name: "Acode SAF trailing slash", a: "content://com.foxdebug.acode.documents/tree/%2Fdata%2Fuser%2F0%2Fcom.foxdebug.acode%2Ffiles%2Fpublic/", b: "content://com.foxdebug.acode.documents/tree/%2Fdata%2Fuser%2F0%2Fcom.foxdebug.acode%2Ffiles%2Fpublic", }, ]; function assertJoinCase( test, { folderUrl, activeLocation, expectedJoined, segment }, ) { const joined = Url.join(activeLocation, segment || "index.html"); test.assert(joined !== null, "Joining the SAF URL should return a value"); test.assertEqual( joined, expectedJoined, "Joined URL should match the expected SAF file URI", ); test.assert( !Url.areSame(folderUrl, joined), "Folder URL and joined file URL should not be considered the same", ); } export async function runUrlTests(writeOutput) { const runner = new TestRunner("URL / SAF URIs"); for (const joinCase of JOIN_CASES) { runner.test(joinCase.name, (test) => { assertJoinCase(test, joinCase); }); } for (const trailingSlashCase of TRAILING_SLASH_CASES) { runner.test(trailingSlashCase.name, (test) => { test.assert( Url.areSame(trailingSlashCase.a, trailingSlashCase.b), "Folder URLs differing only by a trailing slash should be same", ); }); } runner.test("Android SAF leading slash", (test) => { assertJoinCase(test, { ...JOIN_CASES[0], segment: "/index.html", }); }); return await runner.run(writeOutput); } ================================================ FILE: src/theme/builder.js ================================================ import Color from "utils/color"; export default class ThemeBuilder { #theme = { "--popup-border-radius": "4px", "--active-color": "rgb(51, 153, 255)", "--active-text-color": "rgb(255, 215, 0)", "--active-icon-color": "rgba(0, 0, 0, 0.2)", "--border-color": "rgba(122, 122, 122, 0.2)", "--box-shadow-color": "rgba(0, 0, 0, 0.2)", "--button-active-color": "rgb(44, 142, 240)", "--button-background-color": "rgb(51, 153, 255)", "--button-text-color": "rgb(255, 255, 255)", "--error-text-color": "rgb(255, 185, 92)", "--primary-color": "rgb(153, 153, 255)", "--primary-text-color": "rgb(255, 255, 255)", "--secondary-color": "rgb(255, 255, 255)", "--secondary-text-color": "rgb(37, 37, 37)", "--link-text-color": "rgb(97, 94, 253)", "--scrollbar-color": "rgba(0, 0, 0, 0.3)", "--popup-border-color": "rgba(0, 0, 0, 0)", "--popup-icon-color": "rgb(153, 153, 255)", "--popup-background-color": "rgb(255, 255, 255)", "--popup-text-color": "rgb(37, 37, 37)", "--popup-active-color": "rgb(169, 0, 0)", "--danger-color": "rgb(160, 51, 0)", "--danger-text-color": "rgb(255, 255, 255)", "--file-tab-width": "120px", }; version = "free"; name = "Default"; type = "light"; /** Whether Auto update darkened primary color when primary color is updated. */ autoDarkened = true; preferredEditorTheme = null; preferredFont = null; /** * Creates a theme * @param {string} [name] name of the theme * @param {'dark'|'light'} [type] type of the theme * @param {'free'|'paid'} [version] version of the theme */ constructor(name = "", type = "dark", version = "free") { this.name = name; this.type = type; this.version = version; } get id() { return this.name.toLowerCase(); } get popupBorderRadius() { return this.#theme["--popup-border-radius"]; } set popupBorderRadius(value) { this.#theme["--popup-border-radius"] = value; } get activeColor() { return this.#theme["--active-color"]; } set activeColor(value) { this.#theme["--active-color"] = value; } get activeIconColor() { return this.#theme["--active-icon-color"]; } set activeIconColor(value) { this.#theme["--active-icon-color"] = value; } get borderColor() { return this.#theme["--border-color"]; } set borderColor(value) { this.#theme["--border-color"] = value; } get boxShadowColor() { return this.#theme["--box-shadow-color"]; } set boxShadowColor(value) { this.#theme["--box-shadow-color"] = value; } get buttonActiveColor() { return this.#theme["--button-active-color"]; } set buttonActiveColor(value) { this.#theme["--button-active-color"] = value; } get buttonBackgroundColor() { return this.#theme["--button-background-color"]; } set buttonBackgroundColor(value) { this.#theme["--button-background-color"] = value; } get buttonTextColor() { return this.#theme["--button-text-color"]; } set buttonTextColor(value) { this.#theme["--button-text-color"] = value; } get errorTextColor() { return this.#theme["--error-text-color"]; } set errorTextColor(value) { this.#theme["--error-text-color"] = value; } get primaryColor() { return this.#theme["--primary-color"]; } set primaryColor(value) { if (this.autoDarkened) { this.darkenedPrimaryColor = Color(value).darken(0.4).hex.toString(); } this.#theme["--primary-color"] = value; } get primaryTextColor() { return this.#theme["--primary-text-color"]; } set primaryTextColor(value) { this.#theme["--primary-text-color"] = value; } get secondaryColor() { return this.#theme["--secondary-color"]; } set secondaryColor(value) { this.#theme["--secondary-color"] = value; } get secondaryTextColor() { return this.#theme["--secondary-text-color"]; } set secondaryTextColor(value) { this.#theme["--secondary-text-color"] = value; } get linkTextColor() { return this.#theme["--link-text-color"]; } set linkTextColor(value) { this.#theme["--link-text-color"] = value; } get scrollbarColor() { return this.#theme["--scrollbar-color"]; } set scrollbarColor(value) { this.#theme["--scrollbar-color"] = value; } get popupBorderColor() { return this.#theme["--popup-border-color"]; } set popupBorderColor(value) { this.#theme["--popup-border-color"] = value; } get popupIconColor() { return this.#theme["--popup-icon-color"]; } set popupIconColor(value) { this.#theme["--popup-icon-color"] = value; } get popupBackgroundColor() { return this.#theme["--popup-background-color"]; } set popupBackgroundColor(value) { this.#theme["--popup-background-color"] = value; } get popupTextColor() { return this.#theme["--popup-text-color"]; } set popupTextColor(value) { this.#theme["--popup-text-color"] = value; } get popupActiveColor() { return this.#theme["--popup-active-color"]; } set popupActiveColor(value) { this.#theme["--popup-active-color"] = value; } get dangerColor() { return this.#theme["--danger-color"]; } set dangerColor(value) { this.#theme["--danger-color"] = value; } get fileTabWidth() { return this.#theme["--file-tab-width"]; } set fileTabWidth(value) { this.#theme["--file-tab-width"] = value; } get activeTextColor() { return this.#theme["--active-text-color"]; } set activeTextColor(value) { this.#theme["--active-text-color"] = value; } get css() { let css = ""; Object.keys(this.#theme).forEach((key) => { css += `${key}: ${this.#theme[key]};`; }); return `:root {${css}}`; } /** * Gets the theme as an object * @param {'rgba'|'hex' | 'none'} colorType * @returns {Object} */ toJSON(colorType = "none") { const res = { name: this.name, type: this.type, version: this.version, }; Object.keys(this.#theme).forEach((key) => { const color = colorType === "hex" ? Color(this.#theme[key]).hex.toString() : colorType === "rgba" ? Color(this.#theme[key]).rgba.toString() : this.#theme[key]; res[ThemeBuilder.#toPascal(key)] = color; }); return res; } toString() { return JSON.stringify(this.toJSON()); } /** * This method is used to set a darkened primary color. */ darkenPrimaryColor() { this.darkenedPrimaryColor = Color(this.primaryColor) .darken(0.4) .hex.toString(); } matches(id) { return this.id.toLowerCase() === id.toLowerCase(); } /** * Creates a theme from a CSS string * @param {string} name * @param {string} css * @returns {ThemeBuilder} */ static fromCSS(name, css) { const themeBuilder = new ThemeBuilder(name); // get rules using regex const rules = css.match(/:root\s*{([^}]*)}/); if (!rules) throw new Error("Invalid CSS string"); // get variables using regex const variables = rules[1].match(/--[\w-]+:\s*[^;]+/g); if (!variables) throw new Error("Invalid CSS string"); // set variables variables.forEach((variable) => { const [key, value] = variable.split(":"); themeBuilder(ThemeBuilder.#toPascal(key.trim()), value.trim()); }); return themeBuilder; } static fromJSON(theme) { if (!theme) throw new Error("Theme is required"); if (typeof theme !== "object") throw new Error("Theme must be an object"); if (!theme.name) throw new Error("Theme name is required"); if (!theme.type) throw new Error("Theme type is required"); if (!theme.version) throw new Error("Theme version is required"); const themeBuilder = new ThemeBuilder( theme.name, theme.type, theme.version, ); Object.keys(theme).forEach((key) => { if (!Object.getOwnPropertyDescriptor(ThemeBuilder.prototype, key)) return; themeBuilder[key] = theme[key]; }); return themeBuilder; } /** * * @param {string} str */ static #toPascal(str) { // e.g. '--primary-color' => 'PrimaryColor' return str .replace(/^--/, "") .replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } } export function createBuiltInTheme(name, type, version = "paid") { const theme = new ThemeBuilder(name, type, version); theme.autoDarkened = false; return theme; } ================================================ FILE: src/theme/list.js ================================================ import fsOperation from "fileSystem"; import { isDeviceDarkTheme } from "lib/systemConfiguration"; import color from "utils/color"; import Url from "utils/Url"; import fonts from "../lib/fonts"; import settings from "../lib/settings"; import ThemeBuilder from "./builder"; import themes, { updateSystemTheme } from "./preInstalled"; /** @type {Map} */ const appThemes = new Map(); let themeApplied = false; let firstTime = true; function init() { themes.forEach((theme) => add(theme)); } /** * @typedef {object} Theme * @property {string} id * @property {string} name * @property {string} type * @property {string} version * @property {string} primaryColor */ /** * Returns a list of all themes * @returns {Theme[]} */ function list() { return Array.from(appThemes.keys()).map((name) => { const { id, type, primaryColor, version } = appThemes.get(name); return { id, type, version, primaryColor, name: name.capitalize(), }; }); } /** * * @param {string} name * @returns {ThemeBuilder} */ function get(name) { return appThemes.get(name.toLowerCase()); } /** * * @param {ThemeBuilder} theme * @returns */ function add(theme) { if (!(theme instanceof ThemeBuilder)) return; if (appThemes.has(theme.id)) return; appThemes.set(theme.id, theme); const { appTheme } = settings.value; if (theme.matches(appTheme)) { if (appTheme !== "system") { apply(appTheme); } else { updateSystemTheme(isDeviceDarkTheme()); themeApplied = true; } } } /** * Apply a theme * @param {string} id The name of the theme to apply * @param {boolean} init Whether or not this is the first time the theme is being applied */ export async function apply(id, init) { if (!DOES_SUPPORT_THEME) { id = "default"; } themeApplied = true; const loaderFile = Url.join(ASSETS_DIRECTORY, "res/tail-spin.svg"); const svgName = "__tail-spin__.svg"; const img = Url.join(DATA_STORAGE, svgName); const theme = get(id); const $style = document.head.get("style#app-theme") ?? ( ); const update = { appTheme: id, }; if (id === "custom") { update.customTheme = theme.toJSON(); } if (init && theme.preferredEditorTheme) { update.editorTheme = theme.preferredEditorTheme; if (editorManager != null && editorManager.editor != null) { editorManager.editor.setTheme(theme.preferredEditorTheme); } } if (init && theme.preferredFont) { update.editorFont = theme.preferredFont; fonts.setFont(theme.preferredFont); } settings.update(update, false); localStorage.__primary_color = theme.primaryColor; document.body.setAttribute("theme-type", theme.type); $style.textContent = theme.css; document.head.append($style); const primaryColor = color(theme.primaryColor).hex.toString(); const scheme = theme.toJSON("hex"); // Set status bar and navigation bar color system.setUiTheme(primaryColor, scheme); if (firstTime) { // To make sure system bars are updated setTimeout(() => { system.setUiTheme(primaryColor, scheme); }, 1000); firstTime = false; } try { let fs = fsOperation(loaderFile); const svg = await fs.readFile("utf8"); fs = fsOperation(img); if (!(await fs.exists())) { await fsOperation(DATA_STORAGE).createFile(svgName); } await fs.writeFile(svg.replace(/#fff/g, theme.primaryColor)); } catch (error) { window.log("error", error); } } /** * Update a theme * @param {ThemeBuilder} theme */ export function update(theme) { if (!(theme instanceof ThemeBuilder)) return; const oldTheme = get(theme.id); if (!oldTheme) { add(theme); return; } const json = theme.toJSON(); Object.keys(json).forEach((key) => { oldTheme[key] = json[key]; }); } export default { get applied() { return themeApplied; }, init, list, get, add, apply, update, }; ================================================ FILE: src/theme/preInstalled.js ================================================ import appSettings from "lib/settings"; import { createBuiltInTheme } from "./builder"; import { apply } from "./list"; const WHITE = "rgb(255, 255, 255)"; const dark = createBuiltInTheme("Dark", "dark", "free"); dark.primaryColor = "rgb(35, 39, 42)"; dark.primaryTextColor = "rgb(245, 245, 245)"; dark.secondaryColor = "rgb(45, 49, 52)"; dark.secondaryTextColor = "rgb(228, 228, 228)"; dark.activeColor = "rgb(66, 133, 244)"; dark.linkTextColor = "rgb(138, 180, 248)"; dark.borderColor = "rgba(188, 188, 188, 0.15)"; dark.popupIconColor = "rgb(245, 245, 245)"; dark.popupBackgroundColor = "rgb(35, 39, 42)"; dark.popupTextColor = "rgb(245, 245, 245)"; dark.popupActiveColor = "rgb(66, 133, 244)"; dark.activeTextColor = "rgb(255, 255, 255)"; dark.errorTextColor = "rgb(255, 185, 92)"; dark.dangerColor = "rgb(220, 38, 38)"; dark.scrollbarColor = "rgba(255, 255, 255, 0.2)"; dark.preferredEditorTheme = getSystemEditorTheme(true); const oled = createBuiltInTheme("OLED"); oled.primaryColor = "rgb(0, 0, 0)"; oled.primaryTextColor = "rgb(255, 255, 255)"; oled.darkenedPrimaryColor = "rgb(0, 0, 0)"; oled.secondaryColor = "rgb(8, 8, 8)"; oled.secondaryTextColor = "rgb(240, 240, 240)"; oled.activeColor = "rgb(0, 122, 255)"; oled.activeIconColor = "rgba(0, 122, 255, 0.8)"; oled.linkTextColor = "rgb(10, 132, 255)"; oled.borderColor = "rgba(255, 255, 255, 0.08)"; oled.popupIconColor = "rgb(255, 255, 255)"; oled.popupBackgroundColor = "rgb(0, 0, 0)"; oled.popupTextColor = "rgb(255, 255, 255)"; oled.popupActiveColor = "rgb(0, 122, 255)"; oled.popupBorderColor = "rgba(255, 255, 255, 0.1)"; oled.boxShadowColor = "rgba(0, 0, 0, 0.8)"; oled.buttonBackgroundColor = "rgb(0, 122, 255)"; oled.buttonTextColor = "rgb(255, 255, 255)"; oled.buttonActiveColor = "rgb(10, 132, 255)"; oled.activeTextColor = "rgb(255, 255, 255)"; oled.errorTextColor = "rgb(255, 69, 58)"; oled.dangerColor = "rgb(255, 69, 58)"; oled.scrollbarColor = "rgba(255, 255, 255, 0.1)"; oled.preferredEditorTheme = "tokyoNight"; const ocean = createBuiltInTheme("Ocean"); ocean.darkenedPrimaryColor = "rgb(19, 19, 26)"; ocean.primaryColor = "rgb(32, 32, 44)"; ocean.primaryTextColor = WHITE; ocean.secondaryColor = "rgb(38, 38, 53)"; ocean.secondaryTextColor = WHITE; ocean.activeColor = "rgb(51, 153, 255)"; ocean.linkTextColor = "rgb(181, 180, 233)"; ocean.borderColor = "rgb(122, 122, 163)"; ocean.popupIconColor = WHITE; ocean.popupBackgroundColor = "rgb(32, 32, 44)"; ocean.popupTextColor = WHITE; ocean.popupActiveColor = "rgb(255, 215, 0)"; ocean.boxShadowColor = "rgba(0, 0, 0, 0.5)"; ocean.preferredEditorTheme = "solarizedDark"; ocean.preferredFont = "Fira Code"; const bump = createBuiltInTheme("Bump"); bump.darkenedPrimaryColor = "rgb(24, 28, 36)"; bump.primaryColor = "rgb(36, 40, 52)"; bump.primaryTextColor = "rgb(230, 232, 238)"; bump.secondaryColor = "rgb(44, 50, 64)"; bump.secondaryTextColor = "rgb(175, 180, 192)"; bump.activeColor = "rgb(240, 113, 103)"; bump.linkTextColor = "rgb(255, 150, 130)"; bump.borderColor = "rgba(175, 180, 192, 0.2)"; bump.popupIconColor = "rgb(230, 232, 238)"; bump.popupBackgroundColor = "rgb(40, 44, 58)"; bump.popupTextColor = "rgb(230, 232, 238)"; bump.popupActiveColor = "rgb(240, 113, 103)"; bump.buttonBackgroundColor = "rgb(240, 113, 103)"; bump.buttonTextColor = "rgb(255, 255, 255)"; bump.buttonActiveColor = "rgb(210, 90, 80)"; bump.boxShadowColor = "rgba(0, 0, 0, 0.35)"; bump.activeTextColor = "rgb(255, 255, 255)"; bump.errorTextColor = "rgb(255, 180, 100)"; bump.dangerColor = "rgb(240, 70, 60)"; bump.scrollbarColor = "rgba(230, 232, 238, 0.12)"; bump.preferredEditorTheme = "one_dark"; const bling = createBuiltInTheme("Bling"); bling.darkenedPrimaryColor = "rgb(16, 12, 28)"; bling.primaryColor = "rgb(25, 20, 40)"; bling.primaryTextColor = "rgb(228, 225, 240)"; bling.secondaryColor = "rgb(35, 28, 55)"; bling.secondaryTextColor = "rgb(170, 165, 190)"; bling.activeColor = "rgb(80, 200, 155)"; bling.linkTextColor = "rgb(120, 220, 180)"; bling.borderColor = "rgba(80, 200, 155, 0.2)"; bling.popupIconColor = "rgb(228, 225, 240)"; bling.popupBackgroundColor = "rgb(30, 24, 48)"; bling.popupTextColor = "rgb(228, 225, 240)"; bling.popupActiveColor = "rgb(80, 200, 155)"; bling.buttonBackgroundColor = "rgb(80, 200, 155)"; bling.buttonTextColor = "rgb(16, 12, 28)"; bling.buttonActiveColor = "rgb(55, 170, 130)"; bling.boxShadowColor = "rgba(0, 0, 0, 0.45)"; bling.activeTextColor = "rgb(16, 12, 28)"; bling.errorTextColor = "rgb(255, 170, 100)"; bling.dangerColor = "rgb(240, 85, 85)"; bling.scrollbarColor = "rgba(228, 225, 240, 0.1)"; bling.preferredEditorTheme = "tokyoNight"; const moon = createBuiltInTheme("Moon"); moon.darkenedPrimaryColor = "rgb(16, 20, 26)"; moon.primaryColor = "rgb(26, 32, 42)"; moon.primaryTextColor = "rgb(210, 225, 230)"; moon.secondaryColor = "rgb(34, 42, 54)"; moon.secondaryTextColor = "rgb(150, 170, 180)"; moon.activeColor = "rgb(0, 188, 194)"; moon.linkTextColor = "rgb(80, 220, 225)"; moon.borderColor = "rgba(0, 188, 194, 0.2)"; moon.popupIconColor = "rgb(210, 225, 230)"; moon.popupBackgroundColor = "rgb(30, 38, 48)"; moon.popupTextColor = "rgb(210, 225, 230)"; moon.popupActiveColor = "rgb(0, 188, 194)"; moon.buttonBackgroundColor = "rgb(0, 188, 194)"; moon.buttonTextColor = "rgb(16, 20, 26)"; moon.buttonActiveColor = "rgb(0, 155, 160)"; moon.boxShadowColor = "rgba(0, 0, 0, 0.4)"; moon.activeTextColor = "rgb(16, 20, 26)"; moon.errorTextColor = "rgb(255, 170, 105)"; moon.dangerColor = "rgb(235, 75, 70)"; moon.scrollbarColor = "rgba(210, 225, 230, 0.12)"; moon.preferredEditorTheme = "tokyoNight"; const atticus = createBuiltInTheme("Atticus"); atticus.darkenedPrimaryColor = "rgb(26, 24, 22)"; atticus.primaryColor = "rgb(38, 36, 33)"; atticus.primaryTextColor = "rgb(228, 222, 212)"; atticus.secondaryColor = "rgb(48, 45, 40)"; atticus.secondaryTextColor = "rgb(175, 168, 155)"; atticus.activeColor = "rgb(130, 170, 90)"; atticus.linkTextColor = "rgb(155, 195, 115)"; atticus.borderColor = "rgba(130, 170, 90, 0.2)"; atticus.popupIconColor = "rgb(228, 222, 212)"; atticus.popupBackgroundColor = "rgb(42, 40, 36)"; atticus.popupTextColor = "rgb(228, 222, 212)"; atticus.popupActiveColor = "rgb(130, 170, 90)"; atticus.buttonBackgroundColor = "rgb(130, 170, 90)"; atticus.buttonTextColor = "rgb(38, 36, 33)"; atticus.buttonActiveColor = "rgb(105, 145, 70)"; atticus.boxShadowColor = "rgba(0, 0, 0, 0.35)"; atticus.activeTextColor = "rgb(38, 36, 33)"; atticus.errorTextColor = "rgb(240, 160, 90)"; atticus.dangerColor = "rgb(210, 65, 55)"; atticus.scrollbarColor = "rgba(228, 222, 212, 0.12)"; atticus.preferredEditorTheme = "monokai"; const tomyris = createBuiltInTheme("Tomyris"); tomyris.darkenedPrimaryColor = "rgb(22, 12, 20)"; tomyris.primaryColor = "rgb(32, 18, 28)"; tomyris.primaryTextColor = "rgb(235, 225, 232)"; tomyris.secondaryColor = "rgb(45, 26, 38)"; tomyris.secondaryTextColor = "rgb(185, 170, 178)"; tomyris.activeColor = "rgb(232, 75, 145)"; tomyris.linkTextColor = "rgb(250, 130, 180)"; tomyris.borderColor = "rgba(232, 75, 145, 0.2)"; tomyris.popupIconColor = "rgb(235, 225, 232)"; tomyris.popupBackgroundColor = "rgb(38, 22, 33)"; tomyris.popupTextColor = "rgb(235, 225, 232)"; tomyris.popupActiveColor = "rgb(232, 75, 145)"; tomyris.buttonBackgroundColor = "rgb(232, 75, 145)"; tomyris.buttonTextColor = "rgb(255, 255, 255)"; tomyris.buttonActiveColor = "rgb(200, 55, 120)"; tomyris.boxShadowColor = "rgba(0, 0, 0, 0.45)"; tomyris.activeTextColor = "rgb(255, 255, 255)"; tomyris.errorTextColor = "rgb(255, 175, 100)"; tomyris.dangerColor = "rgb(235, 65, 65)"; tomyris.scrollbarColor = "rgba(235, 225, 232, 0.1)"; tomyris.preferredEditorTheme = "monokai"; const menes = createBuiltInTheme("Menes"); menes.darkenedPrimaryColor = "rgb(18, 22, 28)"; menes.primaryColor = "rgb(28, 32, 40)"; menes.primaryTextColor = "rgb(225, 230, 240)"; menes.secondaryColor = "rgb(36, 42, 52)"; menes.secondaryTextColor = "rgb(140, 155, 175)"; menes.activeColor = "rgb(72, 210, 120)"; menes.linkTextColor = "rgb(100, 230, 150)"; menes.borderColor = "rgba(72, 210, 120, 0.18)"; menes.popupIconColor = "rgb(225, 230, 240)"; menes.popupBackgroundColor = "rgb(32, 38, 48)"; menes.popupTextColor = "rgb(225, 230, 240)"; menes.popupActiveColor = "rgb(72, 210, 120)"; menes.buttonBackgroundColor = "rgb(72, 210, 120)"; menes.buttonTextColor = "rgb(18, 22, 30)"; menes.buttonActiveColor = "rgb(50, 180, 95)"; menes.boxShadowColor = "rgba(0, 0, 0, 0.4)"; menes.activeTextColor = "rgb(18, 22, 30)"; menes.errorTextColor = "rgb(255, 165, 95)"; menes.dangerColor = "rgb(240, 75, 65)"; menes.scrollbarColor = "rgba(225, 230, 240, 0.12)"; menes.preferredEditorTheme = "one_dark"; const light = createBuiltInTheme("Light", "light"); light.primaryColor = "rgb(255, 255, 255)"; light.primaryTextColor = "rgb(15, 23, 42)"; light.secondaryColor = "rgb(248, 250, 252)"; light.secondaryTextColor = "rgb(51, 65, 85)"; light.activeColor = "rgb(59, 130, 246)"; light.linkTextColor = "rgb(37, 99, 235)"; light.borderColor = "rgb(226, 232, 240)"; light.popupIconColor = "rgb(15, 23, 42)"; light.popupBackgroundColor = "rgb(255, 255, 255)"; light.popupTextColor = "rgb(15, 23, 42)"; light.popupActiveColor = "rgb(59, 130, 246)"; light.activeTextColor = "rgb(255, 255, 255)"; light.errorTextColor = "rgb(185, 28, 28)"; light.dangerColor = "rgb(220, 38, 38)"; light.scrollbarColor = "rgba(0, 0, 0, 0.2)"; light.preferredEditorTheme = getSystemEditorTheme(false); const system = createBuiltInTheme("System", "dark", "free"); export function getSystemEditorTheme(darkTheme) { if (darkTheme) { return "one_dark"; } else { return "noctisLilac"; } } /** * Update the system theme based on the user's preference. * @param {boolean} darkTheme Whether the user prefers a dark theme. */ export function updateSystemTheme(darkTheme) { if (darkTheme) { system.type = "dark"; system.primaryColor = "rgb(35, 39, 42)"; system.primaryTextColor = "rgb(245, 245, 245)"; system.darkenedPrimaryColor = "rgb(24, 27, 30)"; system.secondaryColor = "rgb(45, 49, 52)"; system.secondaryTextColor = "rgb(228, 228, 228)"; system.activeColor = "rgb(66, 133, 244)"; system.linkTextColor = "rgb(138, 180, 248)"; system.borderColor = "rgba(188, 188, 188, 0.15)"; system.popupIconColor = "rgb(245, 245, 245)"; system.popupBackgroundColor = "rgb(35, 39, 42)"; system.popupTextColor = "rgb(245, 245, 245)"; system.popupActiveColor = "rgb(66, 133, 244)"; } else { system.type = "light"; system.primaryColor = "rgb(255, 255, 255)"; system.primaryTextColor = "rgb(15, 23, 42)"; system.secondaryColor = "rgb(248, 250, 252)"; system.secondaryTextColor = "rgb(51, 65, 85)"; system.activeColor = "rgb(59, 130, 246)"; system.linkTextColor = "rgb(37, 99, 235)"; system.borderColor = "rgb(226, 232, 240)"; system.popupIconColor = "rgb(15, 23, 42)"; system.popupBackgroundColor = "rgb(255, 255, 255)"; system.popupTextColor = "rgb(15, 23, 42)"; system.popupActiveColor = "rgb(59, 130, 246)"; } system.preferredEditorTheme = getSystemEditorTheme(darkTheme); if (appSettings?.value?.appTheme === "system") { apply(system.id); } } const glass = createBuiltInTheme("Glass"); glass.darkenedPrimaryColor = "rgb(250, 250, 255)"; glass.primaryColor = "rgb(255, 255, 255)"; glass.primaryTextColor = "rgb(17, 24, 39)"; glass.secondaryColor = "rgba(255, 255, 255, 0.8)"; glass.secondaryTextColor = "rgb(55, 65, 81)"; glass.activeColor = "rgb(99, 102, 241)"; glass.linkTextColor = "rgb(79, 70, 229)"; glass.borderColor = "rgba(99, 102, 241, 0.2)"; glass.popupIconColor = "rgb(17, 24, 39)"; glass.popupBackgroundColor = "rgba(255, 255, 255, 0.95)"; glass.popupTextColor = "rgb(17, 24, 39)"; glass.popupActiveColor = "rgb(99, 102, 241)"; glass.buttonBackgroundColor = "rgb(99, 102, 241)"; glass.buttonTextColor = "rgb(255, 255, 255)"; glass.buttonActiveColor = "rgb(79, 70, 229)"; glass.boxShadowColor = "rgba(0, 0, 0, 0.1)"; glass.activeTextColor = "rgb(255, 255, 255)"; glass.errorTextColor = "rgb(185, 28, 28)"; glass.dangerColor = "rgb(220, 38, 38)"; glass.scrollbarColor = "rgba(0, 0, 0, 0.15)"; const neon = createBuiltInTheme("Neon"); neon.darkenedPrimaryColor = "rgb(9, 9, 11)"; neon.primaryColor = "rgb(15, 15, 17)"; neon.primaryTextColor = "rgb(10, 255, 200)"; neon.secondaryColor = "rgb(24, 24, 27)"; neon.secondaryTextColor = "rgb(255, 255, 255)"; neon.activeColor = "rgb(255, 20, 147)"; neon.linkTextColor = "rgb(0, 255, 255)"; neon.borderColor = "rgba(10, 255, 200, 0.3)"; neon.popupIconColor = "rgb(10, 255, 200)"; neon.popupBackgroundColor = "rgb(15, 15, 17)"; neon.popupTextColor = "rgb(10, 255, 200)"; neon.popupActiveColor = "rgb(255, 20, 147)"; neon.buttonBackgroundColor = "rgb(255, 20, 147)"; neon.buttonTextColor = "rgb(0, 0, 0)"; neon.buttonActiveColor = "rgb(0, 255, 255)"; neon.boxShadowColor = "rgba(10, 255, 200, 0.2)"; neon.preferredEditorTheme = "monokai"; neon.activeTextColor = "rgb(0, 0, 0)"; neon.errorTextColor = "rgb(255, 20, 147)"; neon.dangerColor = "rgb(255, 20, 147)"; neon.scrollbarColor = "rgba(10, 255, 200, 0.3)"; const glassDark = createBuiltInTheme("Glass Dark", "dark"); glassDark.darkenedPrimaryColor = "rgb(15, 15, 20)"; glassDark.primaryColor = "rgb(24, 24, 32)"; glassDark.primaryTextColor = "rgb(229, 231, 235)"; glassDark.secondaryColor = "rgba(31, 31, 42, 0.8)"; glassDark.secondaryTextColor = "rgb(156, 163, 175)"; glassDark.activeColor = "rgb(99, 102, 241)"; glassDark.linkTextColor = "rgb(129, 140, 248)"; glassDark.borderColor = "rgba(99, 102, 241, 0.3)"; glassDark.popupIconColor = "rgb(229, 231, 235)"; glassDark.popupBackgroundColor = "rgba(31, 31, 42, 0.95)"; glassDark.popupTextColor = "rgb(229, 231, 235)"; glassDark.popupActiveColor = "rgb(99, 102, 241)"; glassDark.buttonBackgroundColor = "rgb(99, 102, 241)"; glassDark.buttonTextColor = "rgb(255, 255, 255)"; glassDark.buttonActiveColor = "rgb(79, 70, 229)"; glassDark.boxShadowColor = "rgba(0, 0, 0, 0.4)"; glassDark.activeTextColor = "rgb(255, 255, 255)"; glassDark.errorTextColor = "rgb(248, 113, 113)"; glassDark.dangerColor = "rgb(239, 68, 68)"; glassDark.scrollbarColor = "rgba(255, 255, 255, 0.2)"; glassDark.preferredEditorTheme = "tokyoNight"; const sunset = createBuiltInTheme("Sunset"); sunset.darkenedPrimaryColor = "rgb(251, 243, 235)"; sunset.primaryColor = "rgb(255, 251, 247)"; sunset.primaryTextColor = "rgb(124, 45, 18)"; sunset.secondaryColor = "rgb(254, 235, 217)"; sunset.secondaryTextColor = "rgb(154, 52, 18)"; sunset.activeColor = "rgb(251, 146, 60)"; sunset.linkTextColor = "rgb(234, 88, 12)"; sunset.borderColor = "rgb(253, 186, 116)"; sunset.popupIconColor = "rgb(124, 45, 18)"; sunset.popupBackgroundColor = "rgb(255, 251, 247)"; sunset.popupTextColor = "rgb(124, 45, 18)"; sunset.popupActiveColor = "rgb(251, 146, 60)"; sunset.buttonBackgroundColor = "rgb(251, 146, 60)"; sunset.buttonTextColor = "rgb(255, 255, 255)"; sunset.buttonActiveColor = "rgb(234, 88, 12)"; sunset.activeTextColor = "rgb(255, 255, 255)"; sunset.errorTextColor = "rgb(185, 28, 28)"; sunset.dangerColor = "rgb(220, 38, 38)"; sunset.scrollbarColor = "rgba(124, 45, 18, 0.2)"; const obsidian = createBuiltInTheme("Obsidian"); obsidian.darkenedPrimaryColor = "rgb(18, 17, 21)"; obsidian.primaryColor = "rgb(28, 27, 31)"; obsidian.primaryTextColor = "rgb(232, 228, 220)"; obsidian.secondaryColor = "rgb(38, 37, 42)"; obsidian.secondaryTextColor = "rgb(185, 180, 172)"; obsidian.activeColor = "rgb(212, 175, 55)"; obsidian.linkTextColor = "rgb(230, 200, 120)"; obsidian.borderColor = "rgba(212, 175, 55, 0.18)"; obsidian.popupIconColor = "rgb(232, 228, 220)"; obsidian.popupBackgroundColor = "rgb(32, 31, 36)"; obsidian.popupTextColor = "rgb(232, 228, 220)"; obsidian.popupActiveColor = "rgb(212, 175, 55)"; obsidian.buttonBackgroundColor = "rgb(212, 175, 55)"; obsidian.buttonTextColor = "rgb(28, 27, 31)"; obsidian.buttonActiveColor = "rgb(184, 148, 36)"; obsidian.boxShadowColor = "rgba(0, 0, 0, 0.45)"; obsidian.activeTextColor = "rgb(28, 27, 31)"; obsidian.errorTextColor = "rgb(255, 152, 100)"; obsidian.dangerColor = "rgb(220, 80, 60)"; obsidian.scrollbarColor = "rgba(212, 175, 55, 0.18)"; obsidian.preferredEditorTheme = "one_dark"; const ember = createBuiltInTheme("Ember"); ember.darkenedPrimaryColor = "rgb(22, 16, 13)"; ember.primaryColor = "rgb(32, 24, 20)"; ember.primaryTextColor = "rgb(240, 228, 210)"; ember.secondaryColor = "rgb(45, 35, 28)"; ember.secondaryTextColor = "rgb(200, 185, 165)"; ember.activeColor = "rgb(217, 130, 60)"; ember.linkTextColor = "rgb(240, 170, 100)"; ember.borderColor = "rgba(217, 130, 60, 0.22)"; ember.popupIconColor = "rgb(240, 228, 210)"; ember.popupBackgroundColor = "rgb(38, 30, 24)"; ember.popupTextColor = "rgb(240, 228, 210)"; ember.popupActiveColor = "rgb(217, 130, 60)"; ember.buttonBackgroundColor = "rgb(217, 130, 60)"; ember.buttonTextColor = "rgb(32, 24, 20)"; ember.buttonActiveColor = "rgb(190, 105, 40)"; ember.boxShadowColor = "rgba(0, 0, 0, 0.4)"; ember.activeTextColor = "rgb(32, 24, 20)"; ember.errorTextColor = "rgb(255, 160, 85)"; ember.dangerColor = "rgb(220, 60, 50)"; ember.scrollbarColor = "rgba(240, 228, 210, 0.12)"; ember.preferredEditorTheme = "monokai"; const dusk = createBuiltInTheme("Dusk"); dusk.darkenedPrimaryColor = "rgb(13, 11, 24)"; dusk.primaryColor = "rgb(20, 18, 35)"; dusk.primaryTextColor = "rgb(215, 210, 235)"; dusk.secondaryColor = "rgb(30, 27, 50)"; dusk.secondaryTextColor = "rgb(160, 155, 185)"; dusk.activeColor = "rgb(167, 105, 220)"; dusk.linkTextColor = "rgb(190, 150, 240)"; dusk.borderColor = "rgba(167, 105, 220, 0.2)"; dusk.popupIconColor = "rgb(215, 210, 235)"; dusk.popupBackgroundColor = "rgb(25, 23, 42)"; dusk.popupTextColor = "rgb(215, 210, 235)"; dusk.popupActiveColor = "rgb(167, 105, 220)"; dusk.buttonBackgroundColor = "rgb(167, 105, 220)"; dusk.buttonTextColor = "rgb(255, 255, 255)"; dusk.buttonActiveColor = "rgb(140, 80, 195)"; dusk.boxShadowColor = "rgba(0, 0, 0, 0.5)"; dusk.activeTextColor = "rgb(255, 255, 255)"; dusk.errorTextColor = "rgb(255, 170, 110)"; dusk.dangerColor = "rgb(235, 80, 100)"; dusk.scrollbarColor = "rgba(215, 210, 235, 0.12)"; dusk.preferredEditorTheme = "tokyoNight"; const carbon = createBuiltInTheme("Carbon"); carbon.darkenedPrimaryColor = "rgb(14, 14, 16)"; carbon.primaryColor = "rgb(22, 22, 24)"; carbon.primaryTextColor = "rgb(235, 235, 240)"; carbon.secondaryColor = "rgb(32, 32, 35)"; carbon.secondaryTextColor = "rgb(155, 155, 165)"; carbon.activeColor = "rgb(55, 142, 240)"; carbon.linkTextColor = "rgb(85, 165, 255)"; carbon.borderColor = "rgba(255, 255, 255, 0.08)"; carbon.popupIconColor = "rgb(235, 235, 240)"; carbon.popupBackgroundColor = "rgb(28, 28, 31)"; carbon.popupTextColor = "rgb(235, 235, 240)"; carbon.popupActiveColor = "rgb(55, 142, 240)"; carbon.buttonBackgroundColor = "rgb(55, 142, 240)"; carbon.buttonTextColor = "rgb(255, 255, 255)"; carbon.buttonActiveColor = "rgb(38, 118, 210)"; carbon.boxShadowColor = "rgba(0, 0, 0, 0.5)"; carbon.activeTextColor = "rgb(255, 255, 255)"; carbon.errorTextColor = "rgb(255, 140, 100)"; carbon.dangerColor = "rgb(235, 70, 60)"; carbon.scrollbarColor = "rgba(255, 255, 255, 0.1)"; carbon.preferredEditorTheme = "one_dark"; const mint = createBuiltInTheme("Mint", "light"); mint.darkenedPrimaryColor = "rgb(235, 245, 240)"; mint.primaryColor = "rgb(250, 253, 252)"; mint.primaryTextColor = "rgb(28, 42, 38)"; mint.secondaryColor = "rgb(240, 248, 245)"; mint.secondaryTextColor = "rgb(72, 92, 85)"; mint.activeColor = "rgb(4, 120, 87)"; mint.linkTextColor = "rgb(2, 100, 72)"; mint.borderColor = "rgb(209, 233, 225)"; mint.popupIconColor = "rgb(28, 42, 38)"; mint.popupBackgroundColor = "rgb(250, 253, 252)"; mint.popupTextColor = "rgb(28, 42, 38)"; mint.popupActiveColor = "rgb(4, 120, 87)"; mint.buttonBackgroundColor = "rgb(4, 120, 87)"; mint.buttonTextColor = "rgb(255, 255, 255)"; mint.buttonActiveColor = "rgb(2, 100, 72)"; mint.boxShadowColor = "rgba(0, 0, 0, 0.06)"; mint.activeTextColor = "rgb(255, 255, 255)"; mint.errorTextColor = "rgb(190, 40, 40)"; mint.dangerColor = "rgb(220, 38, 38)"; mint.scrollbarColor = "rgba(28, 42, 38, 0.12)"; mint.preferredEditorTheme = "noctisLilac"; const sandstone = createBuiltInTheme("Sandstone", "light"); sandstone.darkenedPrimaryColor = "rgb(238, 230, 218)"; sandstone.primaryColor = "rgb(252, 248, 242)"; sandstone.primaryTextColor = "rgb(60, 45, 35)"; sandstone.secondaryColor = "rgb(244, 238, 228)"; sandstone.secondaryTextColor = "rgb(110, 90, 70)"; sandstone.activeColor = "rgb(192, 92, 52)"; sandstone.linkTextColor = "rgb(175, 75, 40)"; sandstone.borderColor = "rgb(222, 210, 195)"; sandstone.popupIconColor = "rgb(60, 45, 35)"; sandstone.popupBackgroundColor = "rgb(252, 248, 242)"; sandstone.popupTextColor = "rgb(60, 45, 35)"; sandstone.popupActiveColor = "rgb(192, 92, 52)"; sandstone.buttonBackgroundColor = "rgb(192, 92, 52)"; sandstone.buttonTextColor = "rgb(255, 255, 255)"; sandstone.buttonActiveColor = "rgb(165, 72, 38)"; sandstone.boxShadowColor = "rgba(0, 0, 0, 0.06)"; sandstone.activeTextColor = "rgb(255, 255, 255)"; sandstone.errorTextColor = "rgb(180, 40, 35)"; sandstone.dangerColor = "rgb(200, 50, 45)"; sandstone.scrollbarColor = "rgba(60, 45, 35, 0.12)"; sandstone.preferredEditorTheme = "noctisLilac"; const blossom = createBuiltInTheme("Blossom", "light"); blossom.darkenedPrimaryColor = "rgb(242, 234, 237)"; blossom.primaryColor = "rgb(254, 250, 251)"; blossom.primaryTextColor = "rgb(48, 38, 42)"; blossom.secondaryColor = "rgb(248, 240, 243)"; blossom.secondaryTextColor = "rgb(100, 80, 88)"; blossom.activeColor = "rgb(190, 75, 115)"; blossom.linkTextColor = "rgb(170, 55, 95)"; blossom.borderColor = "rgb(232, 218, 223)"; blossom.popupIconColor = "rgb(48, 38, 42)"; blossom.popupBackgroundColor = "rgb(254, 250, 251)"; blossom.popupTextColor = "rgb(48, 38, 42)"; blossom.popupActiveColor = "rgb(190, 75, 115)"; blossom.buttonBackgroundColor = "rgb(190, 75, 115)"; blossom.buttonTextColor = "rgb(255, 255, 255)"; blossom.buttonActiveColor = "rgb(160, 55, 95)"; blossom.boxShadowColor = "rgba(0, 0, 0, 0.06)"; blossom.activeTextColor = "rgb(255, 255, 255)"; blossom.errorTextColor = "rgb(200, 45, 40)"; blossom.dangerColor = "rgb(210, 50, 45)"; blossom.scrollbarColor = "rgba(48, 38, 42, 0.12)"; blossom.preferredEditorTheme = "noctisLilac"; const custom = createBuiltInTheme("Custom"); custom.autoDarkened = true; export default [ system, createBuiltInTheme("Legacy", "dark", "free"), dark, light, glass, glassDark, neon, sunset, oled, ocean, bump, bling, moon, atticus, tomyris, menes, obsidian, ember, dusk, carbon, mint, sandstone, blossom, custom, ]; ================================================ FILE: src/utils/Path.js ================================================ export default { /** * The path.dirname() method returns the directory name of a path, * similar to the Unix dirname command. * Trailing directory separators are ignored. * @param {string} path * @returns {string} */ dirname(path) { if (path.endsWith("/")) path = path.slice(0, -1); const parts = path.split("/").slice(0, -1); if (!/^(\.|\.\.|)$/.test(parts[0])) parts.unshift("."); const res = parts.join("/"); if (!res) return "/"; else return res; }, /** * The path.basename() methods returns the last portion of a path, * similar to the Unix basename command. * Trailing directory separators are ignored, see path.sep. * @param {string} path * @returns {string} */ basename(path, ext = "") { ext = ext || ""; if (path === "" || path === "/") return path; const ar = path.split("/"); const last = ar.slice(-1)[0]; if (!last) return ar.slice(-2)[0]; let res = decodeURI(last.split("?")[0] || ""); if (this.extname(res) === ext) res = res.replace(new RegExp(ext + "$"), ""); return res; }, /** * returns the extension of the path, from the last occurrence of the . (period) * character to end of string in the last portion of the path. * If there is no . in the last portion of the path, or if there are no . characters * other than the first character of the basename of path (see path.basename()) , an * empty string is returned. * @param {string} path */ extname(path) { const filename = path.split("/").slice(-1)[0]; if (/.+\..*$/.test(filename)) { return /(?:\.([^.]*))?$/.exec(filename)[0] || ""; } return ""; }, /** * returns a path string from an object. * @param {PathObject} pathObject */ format(pathObject) { let { root, dir, ext, name, base } = pathObject; if (base || !ext.startsWith(".")) { ext = ""; if (base) name = ""; } dir = dir || root; if (!dir.endsWith("/")) dir += "/"; return dir + (base || name) + ext; }, /** * The path.isAbsolute() method determines if path is an absolute path. * @param {string} path */ isAbsolute(path) { return path.startsWith("/"); }, /** * Joins the given number of paths * @param {...string} paths */ join(...paths) { let res = paths.join("/"); return this.normalize(res); }, /** * Normalizes the given path, resolving '..' and '.' segments. * @param {string} path */ normalize(path) { path = path.replace(/\.\/+/g, "./"); path = path.replace(/\/+/g, "/"); const resolved = []; const pathAr = path.split("/"); pathAr.forEach((dir) => { if (dir === "..") { if (resolved.length) resolved.pop(); } else if (dir === ".") { return; } else { resolved.push(dir); } }); return resolved.join("/"); }, /** * * @param {string} path * @returns {PathObject} */ parse(path) { const root = path.startsWith("/") ? "/" : ""; const dir = this.dirname(path); const ext = this.extname(path); const name = this.basename(path, ext); const base = this.basename(path); return { root, dir, base, ext, name, }; }, /** * Resolve the path eg. ```js resolvePath('path/to/some/dir/', '../../dir') //returns 'path/to/dir' ``` * @param {...string} paths */ resolve(...paths) { if (!paths.length) throw new Error("resolve(...path) : Arguments missing!"); let result = ""; paths.forEach((path) => { if (path.startsWith("/")) { result = path; return; } result = this.normalize(this.join(result, path)); }); if (result.startsWith("/")) return result; else return "/" + result; }, /** * Gets path for path2 relative to path1 * @param {String} path1 * @param {String} path2 */ convertToRelative(path1, path2) { path1 = this.normalize(path1).split("/"); path2 = this.normalize(path2).split("/"); const p1len = path1.length; const p2len = path2.length; let flag = false; let path = []; path1.forEach((dir, i) => { if (dir === path2[i] && !flag) return; path.push(path2[i]); if (!flag) { flag = true; return; } if (flag) path.unshift(".."); }); if (p2len > p1len) path.push(...path2.slice(p1len)); return path.join("/"); }, }; ================================================ FILE: src/utils/Uri.js ================================================ import escapeStringRegexp from "escape-string-regexp"; import path from "./Path"; function parseStorageList() { try { const storageList = JSON.parse(localStorage.storageList || "[]"); return Array.isArray(storageList) ? storageList : []; } catch (_) { return []; } } export default { /** * Parse content uri to rootUri and docID * * eg. *```js * parse("content://.../AA98-181D%3A::.../index.html") *``` * `returns` {rootUri: "content://.../AA98-181D%3A", docId: "...index.html"} * * @param {string} contentUri * @returns {{rootUri: string, docId: string, isFileUri: boolean}} */ parse(contentUri) { let rootUri, docId = ""; const DOC_PROVIDER = /^content:\/\/com\.((?![:<>"\/\\\|\?\*]).)*\.documents\//; const TREE_URI = /^content:\/\/com\.((?![:<>"\/\\\|\?\*]).)*\.documents\/tree\//; const SINGLE_URI = /^content:\/\/com\.(((?![:<>"\/\\\|\?\*]).)*)\.documents\/document/; if (DOC_PROVIDER.test(contentUri)) { if (TREE_URI.test(contentUri)) { if (/::/.test(contentUri)) { [rootUri, docId] = contentUri.split("::"); } else { rootUri = contentUri; docId = decodeURIComponent(contentUri.split("/").slice(-1)[0]); } } else if (SINGLE_URI.test(contentUri)) { const [provider, providerId] = SINGLE_URI.exec(contentUri); docId = decodeURIComponent(contentUri); //DecodeUri docId = docId.replace(provider, ""); //replace single to tree docId = path.normalize(docId); //normalize docid if (docId.startsWith("/")) docId = docId.slice(1); // remove leading '/' rootUri = `content://com.${providerId}.documents/tree/` + docId.split(":")[0] + "%3A"; } return { rootUri, docId, isFileUri: /^file:\/\/\//.test(rootUri), }; } else { throw new Error("Invalid uri format."); } }, /** * Formats the five contentUri object to string * @param {{rootUri: string, docId: string} | String} contentUriObject or rootId * @param {string} [docId] * @returns {string} */ format(contentUriObject, docId) { let rootUri; if (typeof contentUriObject === "string") { rootUri = contentUriObject; } else { rootUri = contentUriObject.rootUri; docId = contentUriObject.docId; } if (docId) return [rootUri, docId].join("::"); else return rootUri; }, /** * Gets virtual address by replacing root with name i.e. added in file explorer * @param {string} url */ getVirtualAddress(url) { try { const storageList = parseStorageList(); const matches = []; for (let storage of storageList) { const regex = new RegExp( "^" + escapeStringRegexp(storage.uri ?? storage.url), ); matches.push({ regex, charMatched: url.length - url.replace(regex, "").length, storage, }); } const matched = matches.sort((a, b) => { return b.charMatched - a.charMatched; })[0]; if (matched) { const { storage, regex } = matched; const { name } = storage; const [base, paths] = url.split("::"); url = base + "/" + paths.split("/").slice(1).join("/"); return url.replace(regex, name).replace(/\/+/g, "/"); } return url; } catch (e) { return url; } }, /** * Gets primary address of a content url. * @param {string} url * @returns {string} */ getPrimaryAddress(url) { const [, primary] = url.split("::primary:"); return primary; }, }; ================================================ FILE: src/utils/Url.js ================================================ import URLParse from "url-parse"; import path from "./Path"; import Uri from "./Uri"; export default { /** * Returns basename from a url eg. 'index.html' from 'ftp://localhost/foo/bar/index.html' * @param {string} url * @returns {string} */ basename(url) { url = this.parse(url).url; const protocol = this.getProtocol(url); if (protocol === "content:") { try { let { rootUri, docId, isFileUri } = Uri.parse(url); if (isFileUri) return this.basename(rootUri); if (docId.endsWith("/")) docId = docId.slice(0, -1); docId = docId.split(":").pop(); return this.pathname(docId).split("/").pop(); } catch (error) { return null; } } else { if (url.endsWith("/")) url = url.slice(0, -1); return this.pathname(url).split("/").pop(); } }, /** * Checks if given urls are same or not * @param {...String} urls * @returns {Boolean} */ areSame(...urls) { let firstUrl = urls[0]; if (firstUrl.endsWith("/")) firstUrl = firstUrl.slice(0, -1); return urls.every((url) => { if (url.endsWith("/")) url = url.slice(0, -1); return firstUrl === url; }); }, /** * * @param {String} url * returns the extension of the path, from the last occurrence of the . (period) * character to end of string in the last portion of the path. * If there is no . in the last portion of the path, or if there are no . * characters other than the first character of the basename of path (see path.basename()), * an empty string is returned. * @returns {String} */ extname(url) { const name = this.basename(url); if (name) return path.extname(name); else return null; }, /** * Join all arguments together and normalize the resulting path. * @param {...string} pathnames * @returns {String} */ join(...pathnames) { if (pathnames.length < 2) throw new Error("Join(), requires at least two parameters"); let { url, query } = this.parse(pathnames[0]); const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ""; if (protocol === "content://") { try { if (pathnames[1].startsWith("/")) pathnames[1] = pathnames[1].slice(1); const contentUri = Uri.parse(url); let [root, pathname] = contentUri.docId.split(":"); let newDocId = path.join(pathname, ...pathnames.slice(1)); if (/^content:\/\/com.termux/.test(url)) { const rootCondition = root.endsWith("/"); const newDocIdCondition = newDocId.startsWith("/"); if (rootCondition === newDocIdCondition) { root = root.slice(0, -1); } else if (!rootCondition === !newDocIdCondition) { root += "/"; } return `${contentUri.rootUri}::${root}${newDocId}${query}`; } // if pathname is undefined, meaning a docId/volume (e.g :primary:) // has not been detected, so no newDocId's ":" will be added. if (!pathname) { // Ensure proper path separator between root and newDocId let separator = ""; if (root.endsWith("/") && newDocId.startsWith("/")) { // Both have separator, strip one from newDocId newDocId = newDocId.slice(1); } else if (!root.endsWith("/") && !newDocId.startsWith("/")) { // Neither has separator, add one separator = "/"; } return `${contentUri.rootUri}::${root}${separator}${newDocId}${query}`; } return `${contentUri.rootUri}::${root}:${newDocId}${query}`; } catch (error) { return null; } } else if (protocol) { url = url.replace(new RegExp("^" + protocol), ""); pathnames[0] = url; return protocol + path.join(...pathnames) + query; } else { return path.join(url, ...pathnames.slice(1)) + query; } }, /** * Make url safe by encoding url components * @param {string} url * @returns {string} */ safe(url) { let { url: uri, query } = this.parse(url); url = uri; const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ""; if (protocol) url = url.replace(new RegExp("^" + protocol), ""); const parts = url.split("/").map((part, i) => { if (i === 0) return part; return fixedEncodeURIComponent(part); }); return protocol + parts.join("/") + query; function fixedEncodeURIComponent(str) { return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { return "%" + c.charCodeAt(0).toString(16); }); } }, /** * Gets pathname from url eg. gets '/foo/bar' from 'ftp://myhost.com/foo/bar' * @param {string} url * @returns {string} */ pathname(url) { if (typeof url !== "string" || !this.PROTOCOL_PATTERN.test(url)) return url; url = url.split("?")[0]; const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ""; if (protocol === "content://") { try { const { rootUri, docId, isFileUri } = Uri.parse(url); if (isFileUri) return this.pathname(rootUri); else return "/" + (docId.split(":")[1] || docId); } catch (error) { return null; } } else { if (protocol) url = url.replace(new RegExp("^" + protocol), ""); if (protocol !== "file:///") return "/" + url.split("/").slice(1).join("/"); return "/" + url; } }, /** * Returns dirname from url eg. 'ftp://localhost/foo/' from 'ftp://localhost/foo/bar' * @param {string} url * @returns {string} */ dirname(url) { if (typeof url !== "string") throw new Error("URL must be string"); const urlObj = this.parse(url); url = urlObj.url; const protocol = this.getProtocol(url); if (protocol === "content:") { try { let { rootUri, docId, isFileUri } = Uri.parse(url); if (isFileUri) return this.dirname(rootUri); else { if (docId.endsWith("/")) docId = docId.slice(0, -1); docId = [...docId.split("/").slice(0, -1), ""].join("/"); return Uri.format(rootUri, docId); } } catch (error) { return null; } } else { if (url.endsWith("/")) url = url.slice(0, -1); return [...url.split("/").slice(0, -1), ""].join("/") + urlObj.query; } }, /** * Parse given url into url and query * @param {string} url * @returns {{url:string, query:string}}} */ parse(url) { const [uri, query = ""] = url.split(/(?=\?)/); return { url: uri, query, }; }, /** * Formate Url object to string * @param {object} urlObj * @param {"ftp:"|"sftp:"|"http:"|"https:"} urlObj.protocol * @param {string|number} urlObj.hostname * @param {string} [urlObj.path] * @param {string} [urlObj.username] * @param {string} [urlObj.password] * @param {string|number} [urlObj.port] * @param {object} [urlObj.query] * @returns {string} */ formate(urlObj) { let { protocol, hostname, username, password, path, port, query } = urlObj; const enc = (str) => encodeURIComponent(str); if (!protocol || !hostname) throw new Error("Cannot formate url. Missing 'protocol' and 'hostname'."); let string = `${protocol}//`; if (username && password) string += `${enc(username)}:${enc(password)}@`; else if (username) string += `${username}@`; string += hostname; if (port) string += `:${port}`; if (path) { if (!path.startsWith("/")) path = "/" + path; string += path; } if (query && typeof query === "object") { string += "?"; for (let key in query) string += `${enc(key)}=${enc(query[key])}&`; string = string.slice(0, -1); } return string; }, /** * Returns protocol of a url e.g. 'ftp:' from 'ftp://localhost/foo/bar' * @param {string} url * @returns {"ftp:"|"sftp:"|"http:"|"https:"} */ getProtocol(url) { return (/^([a-z]+:)\/\/\/?/i.exec(url) || [])[1] || ""; }, /** * * @param {string} url * @returns {string} */ hidePassword(url) { const { protocol, username, hostname, pathname } = URLParse(url); if (protocol === "file:") { return url; } else { return `${protocol}//${username}@${hostname}${pathname}`; } }, /** * Decodes url and returns username, password, hostname, pathname, port and query * @param {string} url * @returns {URLObject} */ decodeUrl(url) { const uuid = "uuid" + Math.floor(Math.random() + Date.now() * 1000000); if (/#/.test(url)) { url = url.replace(/#/g, uuid); } let { username, password, hostname, pathname, port, query } = URLParse( url, true, ); if (pathname) { pathname = decodeURIComponent(pathname); pathname = pathname.replace(new RegExp(uuid, "g"), "#"); } if (username) { username = decodeURIComponent(username); } if (password) { password = decodeURIComponent(password); } if (port) { port = Number.parseInt(port); } let { keyFile, passPhrase } = query; if (keyFile) { query.keyFile = decodeURIComponent(keyFile); } if (passPhrase) { query.passPhrase = decodeURIComponent(passPhrase); } return { username, password, hostname, pathname, port, query }; }, /** * Removes trailing slash from url * @param {string} url * @returns */ trimSlash(url) { const parsed = this.parse(url); if (parsed.url.endsWith("/")) { parsed.url = parsed.url.slice(0, -1); } return this.join(parsed.url, parsed.query); }, PROTOCOL_PATTERN: /^[a-z]+:\/\/\/?/i, }; ================================================ FILE: src/utils/codeHighlight.js ================================================ import { classHighlighter, highlightCode } from "@lezer/highlight"; import { getModeForPath, getModesByName } from "cm/modelist"; import { getThemeConfig } from "cm/themes"; import DOMPurify from "dompurify"; import settings from "lib/settings"; const highlightCache = new Map(); const MAX_CACHE_SIZE = 500; let styleElement = null; let currentThemeId = null; export function sanitize(text) { if (!text) return ""; return DOMPurify.sanitize(text, { ALLOWED_TAGS: [] }); } function escapeHtml(text) { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function addSymbolHighlight(html, symbol) { if (!symbol) return html; const escapedSymbol = escapeRegExp(sanitize(symbol)); const regex = new RegExp(`(${escapedSymbol})`, "gi"); return html.replace(regex, '$1'); } function setCache(key, value) { if (highlightCache.size >= MAX_CACHE_SIZE) { const firstKey = highlightCache.keys().next().value; highlightCache.delete(firstKey); } highlightCache.set(key, value); } /** * Generates CSS styles for syntax highlighting tokens * @param {Object} config - Theme config with color values * @param {string} selector - CSS selector to scope styles * @param {boolean} includeBackground - Whether to include background/foreground base styles */ function generateStyles(config, selector, includeBackground = true) { const c = config; const keyword = c.keyword || "#c678dd"; const string = c.string || "#98c379"; const number = c.number || "#d19a66"; const comment = c.comment || "#5c6370"; const func = c.function || "#61afef"; const variable = c.variable || "#e06c75"; const type = c.type || "#e5c07b"; const className = c.class || type; const constant = c.constant || number; const operator = c.operator || keyword; const invalid = c.invalid || "#ff6b6b"; const foreground = c.foreground || "#abb2bf"; const background = c.background || "#282c34"; const baseStyles = includeBackground ? ` ${selector} { background: ${background}; color: ${foreground}; }` : ""; return `${baseStyles} ${selector} .tok-keyword { color: ${keyword}; } ${selector} .tok-operator { color: ${operator}; } ${selector} .tok-number { color: ${number}; } ${selector} .tok-string { color: ${string}; } ${selector} .tok-comment { color: ${comment}; font-style: italic; } ${selector} .tok-variableName { color: ${variable}; } ${selector} .tok-propertyName { color: ${func}; } ${selector} .tok-typeName { color: ${type}; } ${selector} .tok-className { color: ${className}; } ${selector} .tok-function { color: ${func}; } ${selector} .tok-bool { color: ${constant}; } ${selector} .tok-null { color: ${constant}; } ${selector} .tok-punctuation { color: ${foreground}; } ${selector} .tok-definition { color: ${variable}; } ${selector} .tok-labelName { color: ${variable}; } ${selector} .tok-namespace { color: ${type}; } ${selector} .tok-macroName { color: ${keyword}; } ${selector} .tok-atom { color: ${constant}; } ${selector} .tok-meta { color: ${foreground}; } ${selector} .tok-heading { color: ${variable}; font-weight: bold; } ${selector} .tok-link { color: ${func}; text-decoration: underline; } ${selector} .tok-strikethrough { text-decoration: line-through; } ${selector} .tok-emphasis { font-style: italic; } ${selector} .tok-strong { font-weight: bold; } ${selector} .tok-invalid { color: ${invalid}; } ${selector} .tok-name { color: ${variable}; } ${selector} .tok-deleted { color: ${invalid}; } ${selector} .tok-inserted { color: ${string}; } ${selector} .tok-changed { color: ${number}; } `.trim(); } /** * Injects dynamic CSS for syntax highlighting based on current editor theme */ function injectStyles() { const themeId = settings?.value?.editorTheme || "one_dark"; const config = getThemeConfig(themeId); // Code blocks need background, references panel uses parent's background const codeBlockStyles = generateStyles(config, ".cm-highlighted", true); const refPreviewStyles = generateStyles(config, ".ref-preview", false); const allStyles = `${codeBlockStyles}\n${refPreviewStyles}`; if (!styleElement) { styleElement = document.createElement("style"); styleElement.id = "cm-static-highlight-styles"; document.head.appendChild(styleElement); } styleElement.textContent = allStyles; currentThemeId = themeId; } /** * Gets the language parser for a given URI using the modelist */ async function getLanguageParser(uri) { const mode = getModeForPath(uri); if (!mode?.languageExtension) return null; try { const langExt = await mode.languageExtension(); if (!langExt) return null; const langArray = Array.isArray(langExt) ? langExt : [langExt]; for (const ext of langArray) { if (ext && typeof ext === "object" && "language" in ext) { return ext.language.parser; } } } catch (e) { console.warn("Failed to get language parser for", uri, e); } return null; } /** * Gets language parser by language name (e.g., "javascript", "python") * Uses modelist to find the mode and get first valid extension for file matching */ async function getParserForLanguage(langName) { if (!langName) return null; const modesByName = getModesByName(); const normalizedName = langName.toLowerCase(); // Try to find mode by name (case-insensitive) const mode = modesByName[normalizedName]; if (mode?.languageExtension) { try { const langExt = await mode.languageExtension(); if (!langExt) return null; const langArray = Array.isArray(langExt) ? langExt : [langExt]; for (const ext of langArray) { if (ext && typeof ext === "object" && "language" in ext) { return ext.language.parser; } } } catch (e) { console.warn("Failed to get parser for language:", langName, e); } } // Fallback: create a fake filename and use getModeForPath // This handles cases where the language name doesn't match mode name exactly const fakeUri = `file.${normalizedName}`; return await getLanguageParser(fakeUri); } /** * Highlights a single line of code for display in references panel * @param {string} text - The line of code to highlight * @param {string} uri - File URI for language detection * @param {string|null} symbolName - Optional symbol to highlight with special styling */ export async function highlightLine(text, uri, symbolName = null) { if (!text || !text.trim()) return ""; const themeId = settings?.value?.editorTheme || "one_dark"; const cacheKey = `line:${themeId}:${uri}:${text}:${symbolName || ""}`; if (highlightCache.has(cacheKey)) { return highlightCache.get(cacheKey); } const trimmedText = text.trim(); try { const parser = await getLanguageParser(uri); if (parser) { const tree = parser.parse(trimmedText); let result = ""; highlightCode( trimmedText, tree, classHighlighter, (code, classes) => { if (classes) { result += `${escapeHtml(code)}`; } else { result += escapeHtml(code); } }, () => {}, ); if (result) { const highlighted = symbolName ? addSymbolHighlight(result, symbolName) : result; setCache(cacheKey, highlighted); return highlighted; } } } catch (e) { console.warn("Highlighting failed for", uri, e); } const escaped = escapeHtml(trimmedText); const highlighted = symbolName ? addSymbolHighlight(escaped, symbolName) : escaped; setCache(cacheKey, highlighted); return highlighted; } /** * Highlights a code block for display in markdown/plugin pages * @param {string} code - The code to highlight * @param {string} language - Language identifier from markdown fence (e.g., "javascript", "python") */ export async function highlightCodeBlock(code, language) { if (!code) return ""; const themeId = settings?.value?.editorTheme || "one_dark"; const langKey = (language || "text").toLowerCase(); const cacheKey = `block:${themeId}:${langKey}:${code}`; if (highlightCache.has(cacheKey)) { return highlightCache.get(cacheKey); } try { const parser = await getParserForLanguage(langKey); if (parser) { const tree = parser.parse(code); let result = ""; highlightCode( code, tree, classHighlighter, (text, classes) => { if (classes) { result += `${escapeHtml(text)}`; } else { result += escapeHtml(text); } }, () => { result += "\n"; }, ); if (result) { setCache(cacheKey, result); return result; } } } catch (e) { console.warn("Code block highlighting failed for", language, e); } const escaped = escapeHtml(code); setCache(cacheKey, escaped); return escaped; } export function clearHighlightCache() { highlightCache.clear(); } /** * Initializes the static code highlighting system. * Injects theme-based CSS and sets up listener for theme changes. */ export function initHighlighting() { injectStyles(); settings.on("update:editorTheme:after", () => { const newThemeId = settings?.value?.editorTheme || "one_dark"; if (newThemeId !== currentThemeId) { injectStyles(); highlightCache.clear(); } }); } export default { sanitize, highlightLine, highlightCodeBlock, clearHighlightCache, initHighlighting, }; ================================================ FILE: src/utils/color/hex.js ================================================ import Rgb from "./rgb"; export default class Hex { r = 0; g = 0; b = 0; a = 1; /** * Hex color constructor * @param {number} r Red value in hexadecimals * @param {number} g Green value in hexadecimals * @param {number} b Blue value in hexadecimals * @param {number} a Alpha value between 0 and 1 */ constructor(r, g, b, a = 1) { this.r = r; this.g = g; this.b = b; this.a = a; } /** * Creates a Hex color from an RGB color * @param {Rgb} rgb */ static fromRgb(rgb) { const { r, g, b, a } = rgb; return new Hex(r, g, b, a * 255); } /** * Gets the color as a string * @param {boolean} alpha Whether to include alpha */ toString(alpha) { let r = this.r.toString(16); let g = this.g.toString(16); let b = this.b.toString(16); let a = this.a.toString(16); if (r.length === 1) r = `0${r}`; if (g.length === 1) g = `0${g}`; if (b.length === 1) b = `0${b}`; if (a.length === 1) a = `0${a}`; const hex = () => `#${r}${g}${b}`.toUpperCase(); const hexA = () => `#${r}${g}${b}${a}`.toUpperCase(); if (alpha === undefined) { return this.a === 255 ? hex() : hexA(); } return alpha ? hexA() : hex(); } /** * Gets the color as an RGB object * @returns {Rgb} */ get rgb() { return new Rgb(this.r, this.g, this.b, this.a); } } ================================================ FILE: src/utils/color/hsl.js ================================================ import Rgb from "./rgb"; export default class Hsl { h = 0; s = 0; l = 0; a = 1; /** * HSL color constructor * @param {number} h Hue value between 0 and 1 * @param {number} s Saturation value between 0 and 1 * @param {number} l Lightness value between 0 and 1 * @param {number} a Alpha value between 0 and 1 */ constructor(h, s, l, a = 1) { this.h = h; this.s = s; this.l = l; this.a = a; } /** * Creates an HSL color from an RGB color * @param {Rgb} rgb * @returns */ static fromRgb(rgb) { let { r, g, b, a } = rgb; r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const l = (max + min) / 2; let h = 0; let s = 0; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return new Hsl(h, s, l, a); } /** * Gets the color as a string * @param {boolean} [alpha] Whether to include alpha * @returns */ toString(alpha) { const hsl = () => `hsl(${this.hValue}, ${this.sValue}%, ${this.lValue}%)`; const hsla = () => `hsla(${this.hValue}, ${this.sValue}%, ${this.lValue}%, ${this.a})`; if (alpha === undefined) { return this.a === 1 ? hsl() : hsla(); } return alpha ? hsla() : hsl(); } get hValue() { return this.h * 360; } get sValue() { return this.s * 100; } get lValue() { return this.l * 100; } get lightness() { return this.lValue; } get hue() { return this.hValue; } get saturation() { return this.sValue; } /** * Gets the color as an rgb object * @returns {Rgb} */ get rgb() { if (this.l === 0) { return new Rgb(0, 0, 0, this.a); } if (this.l === 1) { return new Rgb(255, 255, 255, this.a); } // now convert hsl value to rgb let c = (1 - Math.abs(2 * this.l - 1)) * this.s; let x = c * (1 - Math.abs(((this.h * 6) % 2) - 1)); let m = this.l - c / 2; let r = 0; let g = 0; let b = 0; if (this.h < 1 / 6) { r = c; g = x; } else if (this.h < 2 / 6) { r = x; g = c; } else if (this.h < 3 / 6) { g = c; b = x; } else if (this.h < 4 / 6) { g = x; b = c; } else if (this.h < 5 / 6) { r = x; b = c; } else { r = c; b = x; } r = Math.round((r + m) * 255); g = Math.round((g + m) * 255); b = Math.round((b + m) * 255); return new Rgb(r, g, b, this.a); } } ================================================ FILE: src/utils/color/index.js ================================================ import Hex from "./hex"; import Hsl from "./hsl"; import Rgb from "./rgb"; /**@type {CanvasRenderingContext2D} */ const ctx = ().getContext("2d", { willReadFrequently: true, }); export default (/**@type {string}*/ color) => { return new Color(color); }; class Color { rgb = new Rgb(0, 0, 0, 1); /** * Create a color from a string * @param {string} color */ constructor(color) { const { canvas } = ctx; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = color; ctx.fillRect(0, 0, 1, 1); const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; this.rgb = new Rgb(r, g, b, a / 255); } darken(ratio) { const hsl = Hsl.fromRgb(this.rgb); hsl.l = Math.max(0, hsl.l - ratio * hsl.l); this.rgb = hsl.rgb; return this; } lighten(ratio) { const hsl = Hsl.fromRgb(this.rgb); hsl.l = Math.min(1, hsl.l + ratio * hsl.l); this.rgb = hsl.rgb; return this; } get isDark() { return this.luminance < 0.5; } get isLight() { return this.luminance >= 0.5; } get lightness() { return this.hsl.l; } /** * Get the luminance of the color * Returns a value between 0 and 1 */ get luminance() { let { r, g, b } = this.rgb; r /= 255; g /= 255; b /= 255; return 0.2126 * r + 0.7152 * g + 0.0722 * b; } get hex() { return Hex.fromRgb(this.rgb); } get hsl() { return Hsl.fromRgb(this.rgb); } } ================================================ FILE: src/utils/color/regex.js ================================================ export const MAX_16_BASE = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"; export const HUE_VALUE = "(360(\\.0+)?|(3[0-5][0-9]|([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-9][0-9])(\\.\\d+)?)|(\\.\\d+))"; export const MAX_PERCENTAGE = "((100(\\.0+)?|[1-9]?[0-9](\\.\\d+)?|\\.\\d+)%)"; export const MAX_ALPHA = `((0(\\.\\d+)?|1(\\.0+)?|\\.\\d+)|${MAX_PERCENTAGE})`; export const RGB_VALUE = `(\\s*${MAX_16_BASE}|${MAX_PERCENTAGE}\\s*)`; export const RGB_VALUES = `${RGB_VALUE},${RGB_VALUE},${RGB_VALUE}`; export const RGB_VALUES_NO_COMMA = `${RGB_VALUE}{3}`; export const RGB = `(rgb\\s*\\(${RGB_VALUES}\\)|rgb\\s*\\(${RGB_VALUES_NO_COMMA}\\))`; export const RGBA = `(rgba\\s*\\(${RGB_VALUES}\\s*,\\s*${MAX_ALPHA}\\s*\\)|rgba\\s*\\(${RGB_VALUES_NO_COMMA}\\s+${MAX_ALPHA}\\s*\\))`; export const HSL_VALUE = `\\s*${HUE_VALUE}\\s*,\\s*${MAX_PERCENTAGE}\\s*,\\s*${MAX_PERCENTAGE}\\s*`; export const HSL_VALUE_NO_COMMA = `\\s*${HUE_VALUE}\\s+${MAX_PERCENTAGE}\\s+${MAX_PERCENTAGE}\\s*`; export const HSL = `(hsl\\s*\\(${HSL_VALUE}\\)|hsl\\s*\\(${HSL_VALUE_NO_COMMA}\\))`; export const HSLA = `(hsla\\s*\\(${HSL_VALUE}\\s*,\\s*${MAX_ALPHA}\\s*\\)|hsla\\s*\\(${HSL_VALUE_NO_COMMA}\\s+\/\/\\s+${MAX_ALPHA}\\s*\\)|hsla\\s*\\(${HSL_VALUE_NO_COMMA}\\s+${MAX_ALPHA}\\s*\\))`; export const HEX = "(#[0-9a-f]{3,8})"; export const NAMED_COLORS = { black: "rgb(0, 0, 0)", silver: "rgb(192, 192, 192)", gray: "rgb(128, 128, 128)", white: "rgb(255, 255, 255)", maroon: "rgb(128, 0, 0)", red: "rgb(255, 0, 0)", purple: "rgb(128, 0, 128)", fuchsia: "rgb(255, 0, 255)", green: "rgb(0, 128, 0)", lime: "rgb(0, 255, 0)", olive: "rgb(128, 128, 0)", yellow: "rgb(255, 255, 0)", navy: "rgb(0, 0, 128)", blue: "rgb(0, 0, 255)", teal: "rgb(0, 128, 128)", aqua: "rgb(0, 255, 255)", aliceblue: "rgb(240, 248, 255)", antiquewhite: "rgb(250, 235, 215)", aquamarine: "rgb(127, 255, 212)", azure: "rgb(240, 255, 255)", beige: "rgb(245, 245, 220)", bisque: "rgb(255, 228, 196)", blanchedalmond: "rgb(255, 235, 205)", blueviolet: "rgb(138, 43, 226)", brown: "rgb(165, 42, 42)", burlywood: "rgb(222, 184, 135)", cadetblue: "rgb(95, 158, 160)", chartreuse: "rgb(127, 255, 0)", chocolate: "rgb(210, 105, 30)", coral: "rgb(255, 127, 80)", cornflowerblue: "rgb(100, 149, 237)", cornsilk: "rgb(255, 248, 220)", crimson: "rgb(220, 20, 60)", cyan: "rgb(0, 255, 255)", darkblue: "rgb(0, 0, 139)", darkcyan: "rgb(0, 139, 139)", darkgoldenrod: "rgb(184, 134, 11)", darkgray: "rgb(169, 169, 169)", darkgreen: "rgb(0, 100, 0)", darkgrey: "rgb(169, 169, 169)", darkkhaki: "rgb(189, 183, 107)", darkmagenta: "rgb(139, 0, 139)", darkolivegreen: "rgb(85, 107, 47)", darkorange: "rgb(255, 140, 0)", darkorchid: "rgb(153, 50, 204)", darkred: "rgb(139, 0, 0)", darksalmon: "rgb(233, 150, 122)", darkseagreen: "rgb(143, 188, 143)", darkslateblue: "rgb(72, 61, 139)", darkslategray: "rgb(47, 79, 79)", darkslategrey: "rgb(47, 79, 79)", darkturquoise: "rgb(0, 206, 209)", darkviolet: "rgb(148, 0, 211)", deeppink: "rgb(255, 20, 147)", deepskyblue: "rgb(0, 191, 255)", dimgray: "rgb(105, 105, 105)", dimgrey: "rgb(105, 105, 105)", dodgerblue: "rgb(30, 144, 255)", firebrick: "rgb(178, 34, 34)", floralwhite: "rgb(255, 250, 240)", forestgreen: "rgb(34, 139, 34)", gainsboro: "rgb(220, 220, 220)", ghostwhite: "rgb(248, 248, 255)", gold: "rgb(255, 215, 0)", goldenrod: "rgb(218, 165, 32)", greenyellow: "rgb(173, 255, 47)", grey: "rgb(128, 128, 128)", honeydew: "rgb(240, 255, 240)", hotpink: "rgb(255, 105, 180)", indianred: "rgb(205, 92, 92)", indigo: "rgb(75, 0, 130)", ivory: "rgb(255, 255, 240)", khaki: "rgb(240, 230, 140)", lavender: "rgb(230, 230, 250)", lavenderblush: "rgb(255, 240, 245)", lawngreen: "rgb(124, 252, 0)", lemonchiffon: "rgb(255, 250, 205)", lightblue: "rgb(173, 216, 230)", lightcoral: "rgb(240, 128, 128)", lightcyan: "rgb(224, 255, 255)", lightgoldenrodyellow: "rgb(250, 250, 210)", lightgray: "rgb(211, 211, 211)", lightgreen: "rgb(144, 238, 144)", lightgrey: "rgb(211, 211, 211)", lightpink: "rgb(255, 182, 193)", lightsalmon: "rgb(255, 160, 122)", lightseagreen: "rgb(32, 178, 170)", lightskyblue: "rgb(135, 206, 250)", lightslategray: "rgb(119, 136, 153)", lightslategrey: "rgb(119, 136, 153)", lightsteelblue: "rgb(176, 196, 222)", lightyellow: "rgb(255, 255, 224)", limegreen: "rgb(50, 205, 50)", linen: "rgb(250, 240, 230)", magenta: "rgb(255, 0, 255)", mediumaquamarine: "rgb(102, 205, 170)", mediumblue: "rgb(0, 0, 205)", mediumorchid: "rgb(186, 85, 211)", mediumpurple: "rgb(147, 112, 219)", mediumseagreen: "rgb(60, 179, 113)", mediumslateblue: "rgb(123, 104, 238)", mediumspringgreen: "rgb(0, 250, 154)", mediumturquoise: "rgb(72, 209, 204)", mediumvioletred: "rgb(199, 21, 133)", midnightblue: "rgb(25, 25, 112)", mintcream: "rgb(245, 255, 250)", mistyrose: "rgb(255, 228, 225)", moccasin: "rgb(255, 228, 181)", navajowhite: "rgb(255, 222, 173)", oldlace: "rgb(253, 245, 230)", olivedrab: "rgb(107, 142, 35)", orangered: "rgb(255, 69, 0)", orchid: "rgb(218, 112, 214)", palegoldenrod: "rgb(238, 232, 170)", palegreen: "rgb(152, 251, 152)", paleturquoise: "rgb(175, 238, 238)", palevioletred: "rgb(219, 112, 147)", papayawhip: "rgb(255, 239, 213)", peachpuff: "rgb(255, 218, 185)", peru: "rgb(205, 133, 63)", pink: "rgb(255, 192, 203)", plum: "rgb(221, 160, 221)", powderblue: "rgb(176, 224, 230)", rebeccapurple: "rgb(102, 51, 153)", rosybrown: "rgb(188, 143, 143)", royalblue: "rgb(65, 105, 225)", saddlebrown: "rgb(139, 69, 19)", salmon: "rgb(250, 128, 114)", sandybrown: "rgb(244, 164, 96)", seagreen: "rgb(46, 139, 87)", seashell: "rgb(255, 245, 238)", sienna: "rgb(160, 82, 45)", skyblue: "rgb(135, 206, 235)", slateblue: "rgb(106, 90, 205)", slategray: "rgb(112, 128, 144)", slategrey: "rgb(112, 128, 144)", snow: "rgb(255, 250, 250)", springgreen: "rgb(0, 255, 127)", steelblue: "rgb(70, 130, 180)", tan: "rgb(210, 180, 140)", thistle: "rgb(216, 191, 216)", tomato: "rgb(255, 99, 71)", turquoise: "rgb(64, 224, 208)", violet: "rgb(238, 130, 238)", wheat: "rgb(245, 222, 179)", whitesmoke: "rgb(245, 245, 245)", yellowgreen: "rgb(154, 205, 50)", }; const namedColors = Object.keys(NAMED_COLORS).join("|"); export const colorRegex = { /** * Regular expression to match rgb colors * @type {RegExp} */ get rgb() { delete this.rgb; this.rgb = new RegExp(`^${RGB}$`); return this.rgb; }, /** * Regular expression to match rgba colors * @type {RegExp} */ get rgba() { delete this.rgba; this.rgba = new RegExp(`^${RGBA}$`); return this.rgba; }, /** * Regular expression to match hsl colors * @type {RegExp} */ get hsl() { delete this.hsl; this.hsl = new RegExp(`^${HSL}$`); return this.hsl; }, /** * Regular expression to match hsla colors * @type {RegExp} */ get hsla() { delete this.hsla; this.hsla = new RegExp(`^${HSLA}$`); return this.hsla; }, /** * Regular expression to match hex colors * @type {RegExp} */ get hex() { delete this.hex; this.hex = new RegExp(`^${HEX}$`); return this.hex; }, /** * Regular expression to match any color * @type {RegExp} */ get named() { delete this.named; this.named = new RegExp(`^(${namedColors})$`); return this.named; }, /** * Regular expression to match any color * @type {RegExp} */ get anyStrict() { delete this.any; this.any = new RegExp( `^(${namedColors}|${RGB}|${RGBA}|${HSL}|${HSLA}|${HEX})$`, "i", ); return this.any; }, /** * Regular expression to match any color * Always return a new RegExp instance * @type {RegExp} */ get anyGlobal() { // Negative lookbehind is not supported in older browsers return new RegExp( `(^|\\W)(${namedColors}|${RGB}|${RGBA}|${HSL}|${HSLA}|${HEX})($|\\W)`, "gi", ); }, }; /** * Select the color at current line and cursor position (CodeMirror) * @returns {{from:number,to:number}|null} */ export function getColorRange() { const { editor } = editorManager; try { const sel = editor.state.selection.main; const from = sel.from; const to = sel.to; // If there is a selection, validate and return it if (from !== to) { const text = editor.state.doc.sliceString(from, to); if (!isValidColor(text)) return null; return { from, to }; } // No selection: find color under cursor in the current line const head = sel.head; const line = editor.state.doc.lineAt(head); const lineText = line.text; const col = head - line.from; const regex = colorRegex.anyGlobal; let match; while ((match = regex.exec(lineText))) { const startCol = match.index + match[1].length; const endCol = startCol + match[2].length; if (col >= startCol && col <= endCol) { return { from: line.from + startCol, to: line.from + endCol }; } } return null; } catch { return null; } } export function isValidColor(value) { return colorRegex.anyStrict.test(value); } ================================================ FILE: src/utils/color/rgb.js ================================================ export default class Rgb { r = 0; g = 0; b = 0; a = 1; constructor(r, g, b, a = 1) { this.r = r; this.g = g; this.b = b; this.a = a; } /** * Get the color as a string * @param {boolean} alpha Whether to include alpha channel * @returns */ toString(alpha) { const rgb = () => `rgb(${this.r}, ${this.g}, ${this.b})`; const rgba = () => `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`; if (alpha === undefined) { return this.a === 1 ? rgb() : rgba(); } return alpha ? rgba() : rgb(); } } ================================================ FILE: src/utils/encodings.js ================================================ import alert from "dialogs/alert"; import settings from "lib/settings"; let encodings = {}; /** * @typedef {Object} Encoding * @property {string} label * @property {string[]} aliases * @property {string} name */ /** * Get the encoding label from the charset * @param {string} charset * @returns {Encoding|undefined} */ export function getEncoding(charset) { charset = charset.toLowerCase(); const found = Object.keys(encodings).find((key) => { if (key.toLowerCase() === charset) { return true; } const alias = encodings[key].aliases.find( (alias) => alias.toLowerCase() === charset, ); if (alias) { return true; } return false; }); if (found) { return encodings[found]; } return encodings["UTF-8"]; } function detectBOM(bytes) { if ( bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ) return "UTF-8"; if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) return "UTF-16LE"; if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) return "UTF-16BE"; return null; } function isValidUTF8(bytes) { let i = 0; while (i < bytes.length) { const byte = bytes[i]; if (byte < 0x80) { i++; } else if (byte >> 5 === 0x06) { if (i + 1 >= bytes.length || bytes[i + 1] >> 6 !== 0x02) return false; i += 2; } else if (byte >> 4 === 0x0e) { if ( i + 2 >= bytes.length || bytes[i + 1] >> 6 !== 0x02 || bytes[i + 2] >> 6 !== 0x02 ) return false; i += 3; } else if (byte >> 3 === 0x1e) { if ( i + 3 >= bytes.length || bytes[i + 1] >> 6 !== 0x02 || bytes[i + 2] >> 6 !== 0x02 || bytes[i + 3] >> 6 !== 0x02 ) return false; i += 4; } else { return false; } } return true; } export async function detectEncoding(buffer) { if (!buffer || buffer.byteLength === 0) { const def = settings.value.defaultFileEncoding; return def === "auto" ? "UTF-8" : def || "UTF-8"; } const bytes = new Uint8Array(buffer); const bomEncoding = detectBOM(bytes); if (bomEncoding) return bomEncoding; const sample = bytes.subarray(0, Math.min(2048, bytes.length)); let nulls = 0, ascii = 0; for (const byte of sample) { if (byte === 0) nulls++; else if (byte < 0x80) ascii++; } if (nulls > sample.length * 0.3) return "UTF-16LE"; if (isValidUTF8(sample)) return "UTF-8"; const encodings = [ ...new Set([ "UTF-8", settings.value.defaultFileEncoding === "auto" ? "UTF-8" : settings.value.defaultFileEncoding || "UTF-8", "windows-1252", "ISO-8859-1", ]), ]; const testSample = sample.subarray(0, 512); const testBuffer = testSample.buffer.slice( testSample.byteOffset, testSample.byteOffset + testSample.byteLength, ); for (const encoding of encodings) { try { const encodingObj = getEncoding(encoding); if (!encodingObj) continue; const text = await execDecode(testBuffer, encodingObj.name); if ( !text.includes("\uFFFD") && !/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(text) ) { return encoding; } } catch (error) { continue; } } const def = settings.value.defaultFileEncoding; return def === "auto" ? "UTF-8" : def || "UTF-8"; } /** * Decodes arrayBuffer to String according given encoding type * @param {ArrayBuffer} buffer * @param {string} [charset] * @returns {Promise} */ export async function decode(buffer, charset) { let isJson = false; if (charset === "json") { charset = null; isJson = true; } if (!charset) { charset = settings.value.defaultFileEncoding; } if (charset === "auto") charset = "UTF-8"; charset = getEncoding(charset).name; const text = await execDecode(buffer, charset); if (isJson) { return JSON.parse(text); } return text; } /** * Encodes text to ArrayBuffer according given encoding type * @param {string} text * @param {string} charset * @returns {Promise} */ export function encode(text, charset) { if (!charset) { charset = settings.value.defaultFileEncoding; } if (charset === "auto") charset = "UTF-8"; charset = getEncoding(charset).name; return execEncode(text, charset); } export async function initEncodings() { return new Promise((resolve, reject) => { cordova.exec( (map) => { Object.keys(map).forEach((key) => { const encoding = map[key]; encodings[key] = encoding; }); resolve(); }, (error) => { alert(strings.error, error.message || error); reject(error); }, "System", "get-available-encodings", [], ); }); } /** * Decodes arrayBuffer to String according given encoding type * @param {ArrayBuffer} buffer * @param {string} charset * @returns {Promise} */ function execDecode(buffer, charset) { return new Promise((resolve, reject) => { cordova.exec( (text) => { resolve(text); }, (error) => { reject(error); }, "System", "decode", [buffer, charset], ); }); } /** * Encodes text to ArrayBuffer according given encoding type * @param {string} text * @param {string} charset * @returns {Promise} */ function execEncode(text, charset) { return new Promise((resolve, reject) => { cordova.exec( (buffer) => { resolve(buffer); }, (error) => { reject(error); }, "System", "encode", [text, charset], ); }); } export default encodings; ================================================ FILE: src/utils/helpers.js ================================================ 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 path from "./Path"; import Uri from "./Uri"; import Url from "./Url"; /** * Gets programming language name according to filename * @param {String} filename * @returns */ function getFileType(filename) { const regex = { babel: /\.babelrc$/i, jsmap: /\.js\.map$/i, yarn: /^yarn\.lock$/i, testjs: /\.test\.js$/i, testts: /\.test\.ts$/i, cssmap: /\.css\.map$/i, typescriptdef: /\.d\.ts$/i, clojurescript: /\.cljs$/i, cppheader: /\.(hh|hpp)$/i, jsconfig: /^jsconfig.json$/i, tsconfig: /^tsconfig.json$/i, android: /\.(apk|aab|slim)$/i, jsbeautify: /^\.jsbeautifyrc$/i, webpack: /^webpack\.config\.js$/i, audio: /\.(mp3|wav|ogg|flac|aac)$/i, git: /(^\.gitignore$)|(^\.gitmodules$)/i, video: /\.(mp4|m4a|mov|3gp|wmv|flv|avi)$/i, image: /\.(png|jpg|jpeg|gif|bmp|ico|webp)$/i, npm: /(^package\.json$)|(^package\-lock\.json$)/i, compressed: /\.(zip|rar|7z|tar|gz|gzip|dmg|iso)$/i, eslint: /(^\.eslintrc(\.(json5?|ya?ml|toml))?$|eslint\.config\.(c?js|json)$)/i, postcssconfig: /(^\.postcssrc(\.(json5?|ya?ml|toml))?$|postcss\.config\.(c?js|json)$)/i, prettier: /(^\.prettierrc(\.(json5?|ya?ml|toml))?$|prettier\.config\.(c?js|json)$)/i, }; const fileType = Object.keys(regex).find((type) => regex[type].test(filename), ); if (fileType) return fileType; return Url.extname(filename).substring(1); } export default { /** * @deprecated This method is deprecated, use 'encodings.decode' instead. * Decodes arrayBuffer to String according given encoding type * @param {ArrayBuffer} arrayBuffer * @param {String} [encoding='utf-8'] */ decodeText(arrayBuffer, encoding = "utf-8") { const isJson = encoding === "json"; if (isJson) encoding = "utf-8"; const uint8Array = new Uint8Array(arrayBuffer); const result = new TextDecoder(encoding).decode(uint8Array); if (isJson) { return this.parseJSON(result); } return result; }, /** * Gets icon according to filename * @param {string} filename */ getIconForFile(filename) { const type = getFileType(filename); // Use CodeMirror's modelist to determine mode name let modeName = "text"; try { const mode = getCMModeForPath?.(filename); modeName = mode?.name || modeName; } catch (e) { // fallback to default if CodeMirror modelist isn't available yet } const iconForMode = `file_type_${modeName}`; const iconForType = `file_type_${type}`; return `file file_type_default ${iconForMode} ${iconForType}`; }, /** * * @param {FileEntry[]} list * @param {object} fileBrowser settings * @param {'both'|'file'|'folder'} */ sortDir(list, fileBrowser, mode = "both") { const dir = []; const file = []; const sortByName = fileBrowser.sortByName; const showHiddenFile = fileBrowser.showHiddenFiles; list.forEach((item) => { let hidden; item.name = item.name || path.basename(item.url || ""); hidden = item.name[0] === "."; if (typeof item.isDirectory !== "boolean") { if (this.isDir(item.type)) item.isDirectory = true; } if (!item.type) item.type = item.isDirectory ? "dir" : "file"; if (!item.url) item.url = item.url || item.uri; if ((hidden && showHiddenFile) || !hidden) { if (item.isDirectory) { dir.push(item); } else if (item.isFile) { file.push(item); } } if (item.isDirectory) { item.icon = "folder"; } else { if (mode === "folder") { item.disabled = true; } item.icon = this.getIconForFile(item.name); } }); if (sortByName) { dir.sort(compare); file.sort(compare); } return dir.concat(file); function compare(a, b) { return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; } }, /** * Gets error message from error object * @param {Error} err * @param {...string} args */ errorMessage(err, ...args) { args.forEach((arg, i) => { if (/^(content|file|ftp|sftp|https?):/.test(arg)) { args[i] = this.getVirtualPath(arg); } }); const extra = args.join("
            "); let msg; if (typeof err === "string" && err) { msg = err; } else if (err instanceof Error) { msg = err.message; } else { msg = strings["an error occurred"]; } return msg + (extra ? "
            " + extra : ""); }, /** * * @param {Error} err * @param {...string} args * @returns {PromiseLike} */ error(err, ...args) { if (err.code === 0) { toast(err); return; } let hide = null; const onhide = () => { if (hide) hide(); }; const msg = this.errorMessage(err, ...args); alert(strings.error, msg, onhide); return new Promise((resolve) => { hide = resolve; }); }, /** * Returns unique ID * @returns {string} */ uuid() { return ( new Date().getTime() + Number.parseInt(Math.random() * 100000000000) ).toString(36); }, /** * Parses JSON string, if fails returns null * @param {Object|Array} string */ parseJSON(string) { if (!string) return null; try { return JSON.parse(string); } catch (e) { return null; } }, /** * Checks whether given type is directory or not * @param {'dir'|'directory'|'folder'} type * @returns {Boolean} */ isDir(type) { return /^(dir|directory|folder)$/.test(type); }, /** * Checks whether given type is file or not * @param {'file'|'link'} type * @returns {Boolean} */ isFile(type) { return /^(file|link)$/.test(type); }, /** * Replace matching part of url to alias name by which storage is added * @param {String} url * @returns {String} */ getVirtualPath(url) { url = Url.parse(url).url; if (/^content:/.test(url)) { const primary = Uri.getPrimaryAddress(url); if (primary) { return primary; } } /**@type {string[]} */ const storageList = this.parseJSON(localStorage.storageList); if (!Array.isArray(storageList)) return url; const storageListLen = storageList.length; for (let i = 0; i < storageListLen; ++i) { const uuid = storageList[i]; let storageUrl = Url.parse(uuid.uri || uuid.url || "").url; if (!storageUrl) continue; if (storageUrl.endsWith("/")) { storageUrl = storageUrl.slice(0, -1); } const regex = new RegExp("^" + escapeStringRegexp(storageUrl)); if (regex.test(url)) { url = url.replace(regex, uuid.name); break; } } return url; }, /** * Updates uri of all active which matches the oldUrl as location * of the file * @param {String} oldUrl * @param {String} newUrl */ updateUriOfAllActiveFiles(oldUrl, newUrl) { const files = editorManager.files; const { url } = Url.parse(oldUrl); for (let file of files) { if (!file.uri) continue; const fileUrl = Url.parse(file.uri).url; if (new RegExp("^" + escapeStringRegexp(url)).test(fileUrl)) { if (newUrl) { file.uri = Url.join(newUrl, file.filename); } else { file.uri = null; } } } editorManager.onupdate("file-delete"); editorManager.emit("update", "file-delete"); }, canShowAds() { return Boolean(IS_FREE_VERSION && adRewards.canShowAds()); }, async showInterstitialIfReady() { if (!this.canShowAds()) return false; if (await window.iad?.isLoaded()) { window.iad.show(); return true; } return false; }, /** * Displays ad on the current page */ showAd() { const { ad } = window; if (this.canShowAds() && innerHeight * devicePixelRatio > 600 && ad) { const $page = tag.getAll("wc-page:not(#root)").pop(); if ($page) { ad.active = true; ad.show(); } } }, async toInternalUri(uri) { return new Promise((resolve, reject) => { window.resolveLocalFileSystemURL( uri, (entry) => { resolve(entry.toInternalURL()); }, reject, ); }); }, promisify(func, ...args) { return new Promise((resolve, reject) => { func(...args, resolve, reject); }); }, async checkAPIStatus() { try { const { status } = await ajax.get(Url.join(constants.API_BASE, "status")); return status === "ok"; } catch (error) { window.log("error", error); return false; } }, fixFilename(name) { if (!name) return name; return name.replace(/(\r\n)+|\r+|\n+|\t+/g, "").trim(); }, /** * Creates a debounced function that delays invoking the input function until after 'wait' milliseconds have elapsed * since the last time the debounced function was invoked. Useful for implementing behavior that should only happen * after the input is complete. * * @param {Function} func - The function to debounce. * @param {number} wait - The number of milliseconds to delay. * @returns {Function} The new debounced function. * @example * window.addEventListener('resize', debounce(myFunction, 200)); */ debounce(func, wait) { let timeout; return function debounced(...args) { const later = () => { clearTimeout(timeout); func.apply(this, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, defineDeprecatedProperty(obj, name, getter, setter) { Object.defineProperty(obj, name, { get: function () { console.warn(`Property '${name}' is deprecated.`); return getter.call(this); }, set: function (value) { console.warn(`Property '${name}' is deprecated.`); setter.call(this, value); }, }); }, parseHTML(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const children = doc.body.children; if (children.length === 1) { return children[0]; } return Array.from(children); }, async createFileStructure(uri, pathString, isFile = true) { const parts = pathString.split("/").filter(Boolean); let currentUri = uri; // Determine if it's a special case URI const isSpecialCase = currentUri.includes("::"); let baseFolder; const isExternalStorageUri = currentUri.includes( "com.android.externalstorage.documents", ); const isTermuxUri = currentUri.includes("com.termux.documents"); const isAcodeTerminalPublicSafUri = currentUri.includes( "com.foxdebug.acode.documents", ); const [, treeSegment = ""] = currentUri.split("/tree/"); const terminalBasePath = isAcodeTerminalPublicSafUri ? decodeURIComponent(treeSegment.split("::")[0] || "") : ""; const getTargetUri = (baseUri, name, index) => { if ( !(isExternalStorageUri || isTermuxUri || isAcodeTerminalPublicSafUri) ) { return Url.join(baseUri, name); } let fullUri = baseUri; if (isExternalStorageUri) { if (!isSpecialCase && index === 0) { fullUri += `::primary:${baseFolder}/${name}`; } else { fullUri += `/${name}`; } } else if (isTermuxUri) { if (!isSpecialCase && index === 0) { fullUri += `::/data/data/com.termux/files/home/${name}`; } else { fullUri += `/${name}`; } } else if (isAcodeTerminalPublicSafUri) { if (!isSpecialCase && index === 0) { const sanitizedBase = terminalBasePath.endsWith("/") ? `${terminalBasePath}${name}` : `${terminalBasePath}/${name}`; fullUri += `::${sanitizedBase}`; } else { fullUri += `/${name}`; } } return fullUri; }; const getExpectedType = (isLastPart) => isLastPart && isFile ? "file" : "folder"; const ensureEntry = async (baseUri, targetUri, name, expectedType) => { const entries = await fsOperation(baseUri).lsDir(); const existingEntry = entries.find((entry) => entry.name === name); if (existingEntry) { const actualType = existingEntry.isDirectory ? "folder" : "file"; if (actualType !== expectedType) { throw new Error( `${name} already exists as a ${actualType}, expected ${expectedType}.`, ); } return { url: existingEntry.url || existingEntry.uri || targetUri, created: false, type: expectedType, }; } const createdUrl = expectedType === "file" ? await fsOperation(baseUri).createFile(name) : await fsOperation(baseUri).createDirectory(name); return { url: createdUrl || targetUri, created: true, type: expectedType, }; }; let firstCreatedPath = null; let firstCreatedType = null; let firstTargetUri = uri; if (isExternalStorageUri) { baseFolder = decodeURIComponent(currentUri.split("%3A")[1].split("/")[0]); } if (parts[0]) { firstTargetUri = getTargetUri(uri, parts[0], 0); } for (let i = 0; i < parts.length; i++) { const isLastElement = i === parts.length - 1; const name = parts[i]; const targetUri = getTargetUri(currentUri, name, i); const expectedType = getExpectedType(isLastElement); const entry = await ensureEntry( currentUri, targetUri, name, expectedType, ); if (entry.created && firstCreatedPath === null) { firstCreatedPath = entry.url; firstCreatedType = expectedType; } currentUri = entry.url; } return { uri: firstCreatedPath || firstTargetUri, created: Boolean(firstCreatedPath), type: firstCreatedType || (isFile && parts.length === 1 ? "file" : "folder"), }; }, formatDownloadCount(downloadCount) { const units = ["", "K", "M", "B", "T"]; let index = 0; while (downloadCount >= 1000 && index < units.length - 1) { downloadCount /= 1000; index++; } const countStr = downloadCount < 10 ? downloadCount.toFixed(2) : downloadCount.toFixed(1); const trimmedCountStr = countStr.replace(/\.?0+$/, ""); return `${trimmedCountStr}${units[index]}`; }, isBinary(file) { // binary file extensions const binaryExtensions = [ "exe", "dll", "so", "dylib", "bin", "o", "apk", "aab", "zip", "rar", "7z", "gz", "tar", "tgz", "jpg", "jpeg", "png", "gif", "bmp", "ico", "mp3", "mp4", "wav", "avi", "mov", "dds", "tga", "swf", "ttf", "eot", "otf", "woff", "woff2", "pdf", "doc", "docx", "xls", "xlsx", "class", "pyc", "jar", "war", ]; const extension = Url.basename(file)?.split(".")?.pop()?.toLowerCase(); if (extension && binaryExtensions.includes(extension)) { return true; } return false; }, }; ================================================ FILE: src/utils/keyboardEvent.js ================================================ /** * @typedef {object} KeyEvent * @property {'keydown' | 'keypress' | 'keyup'} type the type of the event * @property {boolean} bubbles whether the event bubbles up through the DOM or not * @property {boolean} cancelable whether the event is cancelable or not * @property {number} which the key code of the key pressed * @property {number} keyCode the key code of the key pressed * @property {string} key the key pressed * @property {boolean} ctrlKey whether the ctrl key was pressed or not * @property {boolean} shiftKey whether the shift key was pressed or not * @property {boolean} altKey whether the alt key was pressed or not * @property {boolean} metaKey whether the meta key was pressed or not */ const keys = { // arrow keys 37: "ArrowLeft", 38: "ArrowUp", 39: "ArrowRight", 40: "ArrowDown", // special keys 8: "Backspace", 9: "Tab", 13: "Enter", 16: "ShiftLeft", 17: "ControlLeft", 18: "AltLeft", 19: "Pause", 20: "CapsLock", 27: "Escape", 32: " ", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home", 45: "Insert", 46: "Delete", }; const initKeyboardEventType = (function (event) { try { event.initKeyboardEvent( "keyup", // in DOMString typeArg false, // in boolean canBubbleArg false, // in boolean cancelableArg window, // in views::AbstractView viewArg "+", // [test]in DOMString keyIdentifierArg | webkit event.keyIdentifier | IE9 event.key 3, // [test]in unsigned long keyLocationArg | webkit event.keyIdentifier | IE9 event.location true, // [test]in boolean ctrlKeyArg | webkit event.shiftKey | old webkit event.ctrlKey | IE9 event.modifiersList false, // [test]shift | alt true, // [test]shift | alt false, // meta false, // altGraphKey ); return ( ((((event["keyIdentifier"] || event["key"]) === "+" && event["location"]) || event["keyLocation"] === 3) && (event.ctrlKey ? event.altKey ? // webkit 1 : 3 : event.shiftKey ? 2 : // webkit 4)) || // IE9 9 ); // FireFox|w3c } catch (error) { initKeyboardEventType = 0; } })(document.createEvent("KeyboardEvent")); const keyboardEventPropertiesDictionary = { char: "", key: "", location: 0, ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, repeat: false, locale: "", detail: 0, bubbles: false, cancelable: false, //legacy properties keyCode: 0, charCode: 0, which: 0, }; const own = Function.prototype.call.bind(Object.prototype.hasOwnProperty); const ObjectDefineProperty = Object.defineProperty || function (obj, prop, val) { if ("value" in val) { obj[prop] = val["value"]; } }; /** * Creates a keyboard event * @param {'keydown' | 'keyup'} type type of the event * @param {KeyEvent} dict * @returns */ export default function KeyboardEvent(type, dict) { let event; if (initKeyboardEventType) { event = document.createEvent("KeyboardEvent"); } else { event = document.createEvent("Event"); } let propName; let localDict = {}; if (!dict.key && (dict.keyCode || dict.which)) { let key = keys[dict.keyCode || dict.which]; if (!key) key = String.fromCharCode(dict.keyCode || dict.which); dict.key = key; } else if (dict.key && !dict.which && !dict.keyCode) { let keyCode = Object.keys(keys).find((key) => keys[key] === dict.key); if (!keyCode) keyCode = dict.key.charCodeAt(0); dict.keyCode = keyCode; dict.which = keyCode; } for (propName in keyboardEventPropertiesDictionary) if (own(keyboardEventPropertiesDictionary, propName)) { localDict[propName] = ((own(dict, propName) && dict) || keyboardEventPropertiesDictionary)[propName]; } const ctrlKey = localDict["ctrlKey"]; const shiftKey = localDict["shiftKey"]; const altKey = localDict["altKey"]; const metaKey = localDict["metaKey"]; const altGraphKey = localDict["altGraphKey"]; const modifiersListArg = initKeyboardEventType > 3 ? ( (ctrlKey ? "Control" : "") + (shiftKey ? " Shift" : "") + (altKey ? " Alt" : "") + (metaKey ? " Meta" : "") + (altGraphKey ? " AltGraph" : "") ).trim() : null; const key = localDict["key"] + ""; const char = localDict["char"] + ""; const location = localDict["location"]; const keyCode = localDict["keyCode"] || (localDict["keyCode"] = (key && key.charCodeAt(0)) || 0); const charCode = localDict["charCode"] || (localDict["charCode"] = (char && char.charCodeAt(0)) || 0); const bubbles = localDict["bubbles"]; const cancelable = localDict["cancelable"]; const repeat = localDict["repeat"]; const locale = localDict["locale"]; const view = window; localDict["which"] || (localDict["which"] = localDict["keyCode"]); if ("initKeyEvent" in event) { //FF //https://developer.mozilla.org/en/DOM/event.initKeyEvent event.initKeyEvent( type, bubbles, cancelable, view, ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode, ); } else if (initKeyboardEventType && "initKeyboardEvent" in event) { //https://developer.mozilla.org/en/DOM/KeyboardEvent#initKeyboardEvent() if (initKeyboardEventType === 1) { // webkit //http://stackoverflow.com/a/8490774/1437207 //https://bugs.webkit.org/show_bug.cgi?id=13368 event.initKeyboardEvent( type, bubbles, cancelable, view, key, location, ctrlKey, shiftKey, altKey, metaKey, altGraphKey, ); } else if (initKeyboardEventType === 2) { // old webkit //http://code.google.com/p/chromium/issues/detail?id=52408 event.initKeyboardEvent( type, bubbles, cancelable, view, ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode, ); } else if (initKeyboardEventType === 3) { // webkit event.initKeyboardEvent( type, bubbles, cancelable, view, key, location, ctrlKey, altKey, shiftKey, metaKey, altGraphKey, ); } else if (initKeyboardEventType === 4) { // IE9 //http://msdn.microsoft.com/en-us/library/ie/ff975297(v=vs.85).aspx event.initKeyboardEvent( type, bubbles, cancelable, view, key, location, modifiersListArg, repeat, locale, ); } else { // FireFox|w3c //http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent-initKeyboardEvent //https://developer.mozilla.org/en/DOM/KeyboardEvent#initKeyboardEvent() event.initKeyboardEvent( type, bubbles, cancelable, view, char, key, location, modifiersListArg, repeat, locale, ); } } else { event.initEvent(type, bubbles, cancelable); } for (propName in keyboardEventPropertiesDictionary) if (own(keyboardEventPropertiesDictionary, propName)) { if (event[propName] !== localDict[propName]) { try { delete event[propName]; ObjectDefineProperty(event, propName, { writable: true, value: localDict[propName], }); } catch (error) { //Some properties is read-only } } } return event; } ================================================ FILE: src/utils/polyfill.js ================================================ export default function loadPolyFill() { if (!("isConnected" in Node.prototype)) { Object.defineProperty(Node.prototype, "isConnected", { get() { return ( !this.ownerDocument || !( this.ownerDocument.compareDocumentPosition(this) & this.DOCUMENT_POSITION_DISCONNECTED ) ); }, }); } if (!DOMTokenList.prototype.replace) { DOMTokenList.prototype.replace = function (a, b) { if (this.contains(a)) { this.add(b); this.remove(a); return true; } return false; }; } if (!HTMLElement.prototype.append) { HTMLElement.prototype.append = function (...nodes) { nodes.map((node) => this.appendChild(node)); }; } if (!HTMLElement.prototype.remove) { HTMLElement.prototype.remove = function () { this.parentElement.removeChild(this); }; } if (!HTMLElement.prototype.getParent) { HTMLElement.prototype.getParent = function (queryString) { const $$ = [...document.querySelectorAll(queryString)]; for (let $ of $$) if ($.contains(this)) return $; return null; }; } if (!String.prototype.hashCode) { Object.defineProperty(String.prototype, "hashCode", { value: function () { const str = this.toString(); const len = str.length; if (len === 0) return "0"; // Produces a 48-char hex string (192 bits) const FNV_PRIME = 0x01000193; const FNV_OFFSET = 0x811c9dc5; // Generate 6 different 32-bit hashes with different seeds/offsets const hashes = []; for (let pass = 0; pass < 6; pass++) { let hash = FNV_OFFSET ^ (pass * 0x1234567); for (let i = 0; i < len; i++) { const char = str.charCodeAt(i); // XOR with byte and multiply by prime hash ^= char; hash = Math.imul(hash, FNV_PRIME); // Mix in position and pass for additional entropy hash ^= (i + pass) & 0xff; hash = Math.imul(hash, FNV_PRIME); } // Additional mixing hash ^= len; hash = Math.imul(hash, FNV_PRIME); hash ^= hash >>> 16; hashes.push((hash >>> 0).toString(16).padStart(8, "0")); } return hashes.join(""); }, }); } if (!String.prototype.subtract) { Object.defineProperty(String.prototype, "subtract", { value: function (str) { return this.replace(new RegExp("^" + str), ""); }, }); } if (!String.prototype.capitalize) { Object.defineProperty(String.prototype, "capitalize", { value: function (index) { if (typeof index === "number" && index >= 0) { const strs = [ this.slice(0, index), this.slice(index, index + 1), this.slice(index + 1), ]; return strs[0] + (strs[1] ? strs[1].toUpperCase() : "") + strs[2]; } else { let strs = this.split(" "); strs = strs.map((str) => { if (str.length > 0) return str[0].toUpperCase() + str.slice(1); return ""; }); return strs.join(" "); } }, }); } } ================================================ FILE: src/utils/taskManager.js ================================================ export default class TaskManager { /** * @typedef {'linear'|'parallel'} TaskManagerMode */ /** * @type {Array<()=>Promise>} */ #queue = []; /** * @type {TaskManagerMode} */ #mode = "linear"; /** * @type {boolean} */ #busy = false; /** * @type {TaskCallback[]} */ #listeners = []; #count = 0; /** * Create new TaskManager * @param {TaskManagerMode} mode */ constructor(mode) { this.#mode = mode; this.queueTask = this.queueTask.bind(this); } /** * Add task to queue * @param {()=>Promise} task */ async queueTask(task) { this.#queue.push(task); this.#execNext(); return new Promise((resolve, reject) => { const listener = (t, result, error) => { if (t !== task) return; this.#listeners = this.#listeners.filter((l) => l !== listener); if (error) reject(error); else resolve(result); }; this.#listeners.push(listener); }); } async #execNext() { if (this.#mode === "linear" && this.#busy) { return; } const task = this.#queue.shift(); if (!task) return; let result; let error; try { this.#busy = true; const id = this.#count++; result = await task(id); } catch (err) { error = err; } finally { this.#busy = false; } this.#listeners.forEach((l) => l(task, result, error)); if (this.#mode === "linear") this.#execNext(); } } ================================================ FILE: src/views/console.hbs ================================================ Console ================================================ FILE: src/views/file-info.hbs ================================================
            {{name}} {{extension}}
            {{lang.size}} {{length}}
            {{lang.type}} {{type}}
            {{lang.last modified}} {{lastModified}}
            {{#isEditor}}
            {{lang.line count}} {{lineCount}}
            {{lang.word count}} {{wordCount}}
            {{/isEditor}}
            {{lang.path}} {{showUri}}
            ================================================ FILE: src/views/file-menu.hbs ================================================
          • {{file properties}}

          • {{rename}}
          • {{#is_editor}}
          • {{syntax highlighting}} {{file_mode}}
          • {{encoding}} {{file_encoding}}
          • {{new line mode}} {{file_eol}}
          • {{/is_editor}} {{#is_editor}}
          • {{read only}}
          • {{format}}
          • {{#has_lsp_servers}}
          • LSP Info
          • {{/has_lsp_servers}} {{/is_editor}}
            {{#file_on_disk}}
          • {{share}}
          • {{open with}}
          • {{edit with}}
          • {{add to home screen}}
          • {{/file_on_disk}}
          • {{toggle_pin_tab_text}}
          • {{#is_editor}}
          • {{search}}
          • {{goto}}

          • {{^file_read_only}}
          • {{insert color}}

          • {{#copy_text}}
          • {{cut}}
          • {{/copy_text}}
          • {{paste}}
          • {{/file_read_only}} {{#copy_text}}
          • {{copy}}
          • {{/copy_text}}
          • {{select all}}
          • {{/is_editor}} ================================================ FILE: src/views/markdown.hbs ================================================ {{filename}} {{{html}}} ================================================ FILE: src/views/menu.hbs ================================================
          • {{new file}}
          • {{save}}
          • {{save as}}
          • {{files}}
          • {{close file}}
          • {{open recent}}
          • {{find file}}
          • {{console}}
          • {{terminal}}

          • {{settings}}
          • {{help}}

          • {{exit}}
          • ================================================ FILE: src/views/rating.hbs ================================================
            ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2020", "DOM", "DOM.Iterable"], "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "declaration": false, "allowJs": true, "checkJs": false, "paths": { "*": ["./src/*"] }, "jsx": "preserve", "types": ["node"] }, "include": ["src/**/*"], "exclude": ["node_modules", "platforms", "www", "www/js/ace/**/*"] } ================================================ FILE: utils/code-editor-icon.icomoon.json ================================================ {"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]},"formats":[{"order":0,"mOpen":false,"item":{"tag":"ItemFont","args":[{"addPalettes":false,"classPerGlyph":true,"fontFamily":{"value":"code-editor-icon"},"prefix":{"value":"icon-"},"scss":true}]}}],"glyphs":[{"extras":{"name":"text-search","codePoint":61482},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]},"xmlns:xlink":{"tag":"StringValue","args":["http://www.w3.org/1999/xlink"]}},"children":[{"tag":"Text","args":["\n "]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[21,6],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[3,6]}]}]}]]}]}},"children":[]}]},{"tag":"Text","args":["\n "]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[10,12],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[3,12]}]}]}]]}]}},"children":[]}]},{"tag":"Text","args":["\n "]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[10,18],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[3,18]}]}]}]]}]}},"children":[]}]},{"tag":"Text","args":["\n "]},{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[17]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[15]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[3]}]}]}},"children":[]}]},{"tag":"Text","args":["\n "]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[21,19],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[19.1,17.1]}]}]}]]}]}},"children":[]}]},{"tag":"Text","args":["\n"]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"wand","codePoint":61460},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-wand-icon lucide-wand"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[15,4],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[15,2]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[15,16],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[15,14]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[8,9],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[10,9]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[20,9],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[22,9]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[17.8,11.8],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[19,13]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[15,9],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[15.01,9]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[17.8,6.2],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[19,5]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[3,21],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[12,12]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12.2,6.2],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[11,5]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"wand-sparkles","codePoint":61459},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-wand-sparkles-icon lucide-wand-sparkles"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[21.64,3.64],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[20.36,2.3600000000000003]}]},{"tag":"EllipticArcTo","args":[{"rx":1.21,"ry":1.21,"phi":0,"fA":false,"fS":false,"end":[18.64,2.3600000000000003]}]},{"tag":"LineTo","args":[{"point":[2.36,18.64]}]},{"tag":"EllipticArcTo","args":[{"rx":1.21,"ry":1.21,"phi":0,"fA":false,"fS":false,"end":[2.36,20.36]}]},{"tag":"LineTo","args":[{"point":[3.6399999999999997,21.64]}]},{"tag":"EllipticArcTo","args":[{"rx":1.2,"ry":1.2,"phi":0,"fA":false,"fS":false,"end":[5.359999999999999,21.64]}]},{"tag":"LineTo","args":[{"point":[21.64,5.36]}]},{"tag":"EllipticArcTo","args":[{"rx":1.2,"ry":1.2,"phi":0,"fA":false,"fS":false,"end":[21.64,3.6400000000000006]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[14,7],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[17,10]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[5,6],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[5,10]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[19,14],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[19,18]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[10,2],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[10,4]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[7,8],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[3,8]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[21,16],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[17,16]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[11,3],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[9,3]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"link","codePoint":61458},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-link-icon lucide-link"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[10,13],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":5,"ry":5,"phi":0,"fA":false,"fS":false,"end":[17.54,13.54]}]},{"tag":"LineTo","args":[{"point":[20.54,10.54]}]},{"tag":"EllipticArcTo","args":[{"rx":5,"ry":5,"phi":0,"fA":false,"fS":false,"end":[13.469999999999999,3.469999999999999]}]},{"tag":"LineTo","args":[{"point":[11.749999999999998,5.179999999999999]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[14,11],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":5,"ry":5,"phi":0,"fA":false,"fS":false,"end":[6.46,10.46]}]},{"tag":"LineTo","args":[{"point":[3.46,13.46]}]},{"tag":"EllipticArcTo","args":[{"rx":5,"ry":5,"phi":0,"fA":false,"fS":false,"end":[10.530000000000001,20.53]}]},{"tag":"LineTo","args":[{"point":[12.240000000000002,18.82]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"brain","codePoint":61457},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-brain-icon lucide-brain"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12,18],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[12,5]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[15,13],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":4.17,"ry":4.17,"phi":0,"fA":false,"fS":true,"end":[12,9]}]},{"tag":"EllipticArcTo","args":[{"rx":4.17,"ry":4.17,"phi":0,"fA":false,"fS":true,"end":[9,13]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[17.598,6.5],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":3,"ry":3,"phi":0,"fA":true,"fS":false,"end":[12,5]}]},{"tag":"EllipticArcTo","args":[{"rx":3,"ry":3,"phi":0,"fA":true,"fS":false,"end":[6.402,6.5]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[17.997,5.125],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":4,"ry":4,"phi":0,"fA":false,"fS":true,"end":[20.523,10.895]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[18,18],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":4,"ry":4,"phi":0,"fA":false,"fS":false,"end":[20,10.536]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[19.967,17.483],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":4,"ry":4,"phi":0,"fA":true,"fS":true,"end":[12,18]}]},{"tag":"EllipticArcTo","args":[{"rx":4,"ry":4,"phi":0,"fA":true,"fS":true,"end":[4.033,17.483]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[6,18],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":4,"ry":4,"phi":0,"fA":false,"fS":true,"end":[4,10.536]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[6.003,5.125],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":4,"ry":4,"phi":0,"fA":false,"fS":false,"end":[3.4770000000000003,10.895]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"paperclip","codePoint":61456},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-paperclip-icon lucide-paperclip"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[16,6],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[7.586,14.586]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[10.415000000000001,17.415]}]},{"tag":"LineTo","args":[{"point":[18.829,8.828999999999999]}]},{"tag":"EllipticArcTo","args":[{"rx":4,"ry":4,"phi":0,"fA":true,"fS":false,"end":[13.172,3.171999999999999]}]},{"tag":"LineTo","args":[{"point":[4.793000000000001,11.722999999999999]}]},{"tag":"EllipticArcTo","args":[{"rx":6,"ry":6,"phi":0,"fA":true,"fS":false,"end":[13.278,20.208]}]},{"tag":"LineTo","args":[{"point":[21.657,11.656999999999998]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"palette","codePoint":61455},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-palette-icon lucide-palette"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12,22],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":true,"end":[12,2]}]},{"tag":"EllipticArcTo","args":[{"rx":10,"ry":9,"phi":0,"fA":false,"fS":true,"end":[22,11]}]},{"tag":"EllipticArcTo","args":[{"rx":5,"ry":5,"phi":0,"fA":false,"fS":true,"end":[17,16]}]},{"tag":"LineTo","args":[{"point":[14.75,16]}]},{"tag":"EllipticArcTo","args":[{"rx":1.75,"ry":1.75,"phi":0,"fA":false,"fS":false,"end":[13.35,18.8]}]},{"tag":"LineTo","args":[{"point":[13.65,19.2]}]},{"tag":"EllipticArcTo","args":[{"rx":1.75,"ry":1.75,"phi":0,"fA":false,"fS":true,"end":[12.25,22]}]},{"tag":"LineTo","args":[{"point":[12,22]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[13.5]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[6.5]}]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[0.5]}]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[17.5]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[10.5]}]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[0.5]}]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[6.5]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[12.5]}]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[0.5]}]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[8.5]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[7.5]}]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[0.5]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"loader","codePoint":61454},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-loader-icon lucide-loader"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12,2],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[12,6]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[16.2,7.8],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[19.099999999999998,4.9]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[18,12],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[22,12]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[16.2,16.2],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[19.099999999999998,19.099999999999998]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12,18],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[12,22]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[4.9,19.1],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[7.800000000000001,16.200000000000003]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[2,12],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[6,12]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[4.9,4.9],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[7.800000000000001,7.800000000000001]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"square-terminal","codePoint":61453},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-square-terminal-icon lucide-square-terminal"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[7,11],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[9,9]}]},{"tag":"LineTo","args":[{"point":[7,7]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[11,13],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[15,13]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"rect","attributes":{"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[18]}]}]},"rx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"ry":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[18]}]}]},"x":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[3]}]}]},"y":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[3]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"house","codePoint":61452},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-house-icon lucide-house"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[15,21],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[15,13]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[14,12]}]},{"tag":"LineTo","args":[{"point":[10,12]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[9,13]}]},{"tag":"LineTo","args":[{"point":[9,21]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[3,10],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[3.709,8.472]}]},{"tag":"LineTo","args":[{"point":[10.709,2.4719999999999995]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[13.291,2.4719999999999995]}]},{"tag":"LineTo","args":[{"point":[20.291,8.472]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[21,10]}]},{"tag":"LineTo","args":[{"point":[21,19]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[19,21]}]},{"tag":"LineTo","args":[{"point":[5,21]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[3,19]}]},{"tag":"LineTo","args":[{"point":[3,10]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"message-circle","codePoint":61451},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-message-circle-icon lucide-message-circle"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[2.992,16.342],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[3.086,17.509]}]},{"tag":"LineTo","args":[{"point":[2.021,20.799]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[3.2569999999999997,21.967]}]},{"tag":"LineTo","args":[{"point":[6.67,20.968999999999998]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[7.769,21.060999999999996]}]},{"tag":"EllipticArcTo","args":[{"rx":10,"ry":10,"phi":0,"fA":true,"fS":false,"end":[2.992,16.341999999999995]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"user-round","codePoint":61450},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-user-round-icon lucide-user-round"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[12]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[8]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[5]}]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[20,21],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":8,"ry":8,"phi":0,"fA":false,"fS":false,"end":[4,21]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"funnel","codePoint":61449},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-funnel-icon lucide-funnel"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[10,20],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[10.553,20.895]}]},{"tag":"LineTo","args":[{"point":[12.553,21.895]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[14,21]}]},{"tag":"LineTo","args":[{"point":[14,14]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[14.517,12.659]}]},{"tag":"LineTo","args":[{"point":[21.74,4.67]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[21,3]}]},{"tag":"LineTo","args":[{"point":[3,3]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[2.258,4.67]}]},{"tag":"LineTo","args":[{"point":[9.483,12.658999999999999]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[10,14]}]},{"tag":"LineTo","args":[{"point":[10,20]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"zap","codePoint":61440},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-zap-icon lucide-zap"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[4,14],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":true,"end":[3.2199999999999998,12.370000000000001]}]},{"tag":"LineTo","args":[{"point":[13.120000000000001,2.1700000000000017]}]},{"tag":"EllipticArcTo","args":[{"rx":0.5,"ry":0.5,"phi":0,"fA":false,"fS":true,"end":[13.98,2.6300000000000017]}]},{"tag":"LineTo","args":[{"point":[12.06,8.650000000000002]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[13,10]}]},{"tag":"LineTo","args":[{"point":[20,10]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":true,"end":[20.78,11.629999999999999]}]},{"tag":"LineTo","args":[{"point":[10.88,21.83]}]},{"tag":"EllipticArcTo","args":[{"rx":0.5,"ry":0.5,"phi":0,"fA":false,"fS":true,"end":[10.020000000000001,21.369999999999997]}]},{"tag":"LineTo","args":[{"point":[11.940000000000001,15.349999999999998]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[11,14]}]},{"tag":"LineTo","args":[{"point":[4,14]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"verified","codePoint":61441},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["size-6"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Text","args":["\n "]},{"tag":"Element","args":[{"tagName":"path","attributes":{"clip-rule":{"tag":"Value","args":[{"tag":"FillRule","args":[{"tag":"EvenOdd","args":[]}]}]},"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[8.603,3.799],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[12,2.25]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[13.357,2.25],"c2":[14.573,2.85],"end":[15.397,3.799]}]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[18.895,5.106]}]},{"tag":"EllipticArcTo","args":[{"rx":4.491,"ry":4.491,"phi":0,"fA":false,"fS":true,"end":[20.201999999999998,8.603]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[21.75,12]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[20.201,15.397]}]},{"tag":"EllipticArcTo","args":[{"rx":4.491,"ry":4.491,"phi":0,"fA":false,"fS":true,"end":[18.894000000000002,18.894]}]},{"tag":"EllipticArcTo","args":[{"rx":4.491,"ry":4.491,"phi":0,"fA":false,"fS":true,"end":[15.397000000000002,20.200999999999997]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[12,21.75]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[8.603,20.201]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[5.1049999999999995,18.895]}]},{"tag":"EllipticArcTo","args":[{"rx":4.491,"ry":4.491,"phi":0,"fA":false,"fS":true,"end":[3.7979999999999996,15.396999999999998]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[2.25,12]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[2.25,10.643],"c2":[2.85,9.427],"end":[3.799,8.603]}]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[5.106,5.106]}]},{"tag":"EllipticArcTo","args":[{"rx":4.49,"ry":4.49,"phi":0,"fA":false,"fS":true,"end":[8.603,3.799]}]}]},{"start":[15.61,10.186],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":0.75,"ry":0.75,"phi":0,"fA":true,"fS":false,"end":[14.389999999999999,9.314]}]},{"tag":"LineTo","args":[{"point":[11.153999999999998,13.844000000000001]}]},{"tag":"LineTo","args":[{"point":[9.53,12.22]}]},{"tag":"EllipticArcTo","args":[{"rx":0.75,"ry":0.75,"phi":0,"fA":false,"fS":false,"end":[8.469999999999999,13.280000000000001]}]},{"tag":"LineTo","args":[{"point":[10.719999999999999,15.530000000000001]}]},{"tag":"EllipticArcTo","args":[{"rx":0.75,"ry":0.75,"phi":0,"fA":false,"fS":false,"end":[11.86,15.436000000000002]}]},{"tag":"LineTo","args":[{"point":[15.61,10.186]}]}]}]]}]},"fill-rule":{"tag":"Value","args":[{"tag":"FillRule","args":[{"tag":"EvenOdd","args":[]}]}]}},"children":[]}]},{"tag":"Text","args":["\n"]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"terminal","codePoint":61442},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[1024]}]}]},"version":{"tag":"StringValue","args":["1.1"]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[1024]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Text","args":["\n"]},{"tag":"Text","args":["\n"]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[155.435,173.611],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[140.416,178.902],"c2":[128,196.86399999999998],"end":[128,213.334]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[128,216.747],"c2":[129.237,223.318],"end":[130.773,227.969]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[133.29,235.606],"c2":[144.085,246.956],"end":[249.76999999999998,352.854]}]}]},{"tag":"LineTo","args":[{"point":[366.037,469.334]}]},{"tag":"LineTo","args":[{"point":[249.76999999999998,585.814]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[144.08499999999998,691.713],"c2":[133.28999999999996,703.062],"end":[130.77299999999997,710.699]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[125.26899999999996,727.3389999999999],"c2":[128.89599999999996,743.382],"end":[140.79999999999995,755.286]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[148.42199999999997,762.954],"c2":[158.97599999999994,767.6999999999999],"end":[170.63899999999995,767.6999999999999]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[177.55899999999994,767.6999999999999],"c2":[184.08899999999994,766.0289999999999],"end":[189.84699999999995,763.069]}]}]},{"tag":"LineTo","args":[{"point":[189.61099999999996,763.179]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[195.02999999999997,760.576],"c2":[234.32599999999996,722.304],"end":[330.45399999999995,625.963]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[456.96099999999996,499.24299999999994],"c2":[463.95799999999997,491.904],"end":[466.77399999999994,483.24299999999994]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[468.18299999999994,479.1599999999999],"c2":[468.99699999999996,474.4549999999999],"end":[468.99699999999996,469.55999999999995]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[468.99699999999996,462.57599999999996],"c2":[467.34099999999995,455.97999999999996],"end":[464.4,450.14099999999996]}]}]},{"tag":"LineTo","args":[{"point":[464.513,450.38899999999995]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[461.90999999999997,444.96999999999997],"c2":[423.638,405.717],"end":[327.29699999999997,309.54599999999994]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[205.48399999999998,187.94599999999994],"c2":[192.98199999999997,175.95699999999994],"end":[185.30199999999996,173.43899999999994]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[180.92499999999995,171.84899999999993],"c2":[175.87299999999996,170.92999999999995],"end":[170.60699999999997,170.92999999999995]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[165.16199999999998,170.92999999999995],"c2":[159.94699999999997,171.91199999999995],"end":[155.12899999999996,173.70999999999995]}]}]},{"tag":"LineTo","args":[{"point":[155.435,173.611]}]}]},{"start":[497.664,770.688],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[487.908,774.264],"c2":[480.01599999999996,780.983],"end":[475.033,789.596]}]}]},{"tag":"LineTo","args":[{"point":[474.923,789.802]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[470.87,796.671],"c2":[470.187,799.743],"end":[470.187,810.666]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[470.187,821.589],"c2":[470.87,824.6610000000001],"end":[474.923,831.5300000000001]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[477.526,835.9250000000001],"c2":[482.176,841.5140000000001],"end":[485.291,843.8610000000001]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[498.219,853.7170000000001],"c2":[489.984,853.3330000000001],"end":[682.582,853.3330000000001]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[881.622,853.3330000000001],"c2":[869.718,854.1010000000001],"end":[883.2429999999999,840.5760000000001]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[890.9739999999999,832.9570000000001],"c2":[895.7639999999999,822.3710000000001],"end":[895.7639999999999,810.6670000000001]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[895.7639999999999,798.9630000000002],"c2":[890.9739999999999,788.3770000000002],"end":[883.2479999999999,780.7630000000001]}]}]},{"tag":"LineTo","args":[{"point":[883.2429999999999,780.7580000000002]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[869.718,767.2330000000002],"c2":[881.7499999999999,768.0010000000002],"end":[681.942,768.1290000000001]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[532.225,768.2140000000002],"c2":[503.254,768.6410000000001],"end":[497.664,770.688]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]},{"tag":"Text","args":["\n"]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"tag","codePoint":61443},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[16]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":16,"height":16}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[16]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1,7.775],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[1,2.75]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[1,1.784],"c2":[1.784,1],"end":[2.75,1]}]}]},{"tag":"LineTo","args":[{"point":[7.775,1]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[8.239,1],"c2":[8.685,1.184],"end":[9.013,1.513]}]}]},{"tag":"LineTo","args":[{"point":[15.263,7.763]}]},{"tag":"EllipticArcTo","args":[{"rx":1.75,"ry":1.75,"phi":0,"fA":false,"fS":true,"end":[15.263,10.237]}]},{"tag":"LineTo","args":[{"point":[10.237,15.263]}]},{"tag":"EllipticArcTo","args":[{"rx":1.75,"ry":1.75,"phi":0,"fA":false,"fS":true,"end":[7.763,15.263]}]},{"tag":"LineTo","args":[{"point":[1.513,9.013]}]},{"tag":"EllipticArcTo","args":[{"rx":1.752,"ry":1.752,"phi":0,"fA":false,"fS":true,"end":[1,7.775]}]}]},{"start":[2.5,7.775],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[2.5,7.841],"c2":[2.526,7.905],"end":[2.573,7.952]}]}]},{"tag":"LineTo","args":[{"point":[8.823,14.202]}]},{"tag":"EllipticArcTo","args":[{"rx":0.25,"ry":0.25,"phi":0,"fA":false,"fS":false,"end":[9.177,14.202]}]},{"tag":"LineTo","args":[{"point":[14.202,9.177]}]},{"tag":"EllipticArcTo","args":[{"rx":0.25,"ry":0.25,"phi":0,"fA":false,"fS":false,"end":[14.202,8.823]}]},{"tag":"LineTo","args":[{"point":[7.952,2.5730000000000004]}]},{"tag":"EllipticArcTo","args":[{"rx":0.25,"ry":0.25,"phi":0,"fA":false,"fS":false,"end":[7.775,2.5000000000000004]}]},{"tag":"LineTo","args":[{"point":[2.75,2.5000000000000004]}]},{"tag":"EllipticArcTo","args":[{"rx":0.25,"ry":0.25,"phi":0,"fA":false,"fS":false,"end":[2.5,2.7500000000000004]}]},{"tag":"LineTo","args":[{"point":[2.5,7.775]}]}]},{"start":[6,5],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":true,"fS":true,"end":[6,7]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":true,"end":[6,5]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"scale","codePoint":61444},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-scale-icon lucide-scale"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12,3],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[12,21]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[19,8],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[22,16]}]},{"tag":"EllipticArcTo","args":[{"rx":5,"ry":5,"phi":0,"fA":false,"fS":true,"end":[16,16]}]},{"tag":"LineTo","args":[{"point":[19,8]}]}]},{"start":[19,8],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[19,7]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[3,7],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[4,7]}]},{"tag":"EllipticArcTo","args":[{"rx":17,"ry":17,"phi":0,"fA":false,"fS":false,"end":[12,5]}]},{"tag":"EllipticArcTo","args":[{"rx":17,"ry":17,"phi":0,"fA":false,"fS":false,"end":[20,7]}]},{"tag":"LineTo","args":[{"point":[21,7]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[5,8],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[8,16]}]},{"tag":"EllipticArcTo","args":[{"rx":5,"ry":5,"phi":0,"fA":false,"fS":true,"end":[2,16]}]},{"tag":"LineTo","args":[{"point":[5,8]}]}]},{"start":[5,8],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[5,7]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[7,21],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[17,21]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cart","codePoint":61445},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-shopping-cart-icon lucide-shopping-cart"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[8]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[21]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[1]}]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"circle","attributes":{"cx":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[19]}]}]},"cy":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[21]}]}]},"r":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[1]}]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[2.05,2.05],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[4.05,2.05]}]},{"tag":"LineTo","args":[{"point":[6.71,14.469999999999999]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[8.71,16.049999999999997]}]},{"tag":"LineTo","args":[{"point":[18.490000000000002,16.049999999999997]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[20.44,14.479999999999997]}]},{"tag":"LineTo","args":[{"point":[22.09,7.049999999999997]}]},{"tag":"LineTo","args":[{"point":[5.12,7.049999999999997]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"lightbulb","codePoint":61446},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-lightbulb-icon lucide-lightbulb"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[15,14],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[15.2,13],"c2":[15.7,12.3],"end":[16.5,11.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[17.5,10.6],"c2":[18,9.3],"end":[18,8]}]}]},{"tag":"EllipticArcTo","args":[{"rx":6,"ry":6,"phi":0,"fA":false,"fS":false,"end":[6,8]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[6,9],"c2":[6.2,10.2],"end":[7.5,11.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"CParams","args":[{"c1":[8.2,12.2],"c2":[8.8,13],"end":[9,14]}]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[9,18],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[15,18]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[10,22],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[14,22]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"pin","codePoint":61447},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-pin-icon lucide-pin"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12,17],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[12,22]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[9,10.76],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[7.89,12.55]}]},{"tag":"LineTo","args":[{"point":[6.109999999999999,13.450000000000001]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[5,15.24]}]},{"tag":"LineTo","args":[{"point":[5,16]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[6,17]}]},{"tag":"LineTo","args":[{"point":[18,17]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[19,16]}]},{"tag":"LineTo","args":[{"point":[19,15.24]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[17.89,13.45]}]},{"tag":"LineTo","args":[{"point":[16.11,12.549999999999999]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[15,10.76]}]},{"tag":"LineTo","args":[{"point":[15,7]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":true,"end":[16,6]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[16,2]}]},{"tag":"LineTo","args":[{"point":[8,2]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[8,6]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":true,"end":[9,7]}]},{"tag":"LineTo","args":[{"point":[9,10.76]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"pin-off","codePoint":61448},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"class":{"tag":"StringValue","args":["lucide lucide-pin-off-icon lucide-pin-off"]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"NoPaint","args":[]}]}]},"height":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"stroke":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]},"stroke-linecap":{"tag":"Value","args":[{"tag":"StrokeLineCap","args":[{"tag":"RoundCap","args":[]}]}]},"stroke-linejoin":{"tag":"Value","args":[{"tag":"StrokeLineJoin","args":[{"tag":"RoundJoin","args":[]}]}]},"stroke-width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[2]}]}]},"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":24,"height":24}]}]},"width":{"tag":"Value","args":[{"tag":"Length","args":[{"tag":"Px","args":[24]}]}]},"xmlns":{"tag":"StringValue","args":["http://www.w3.org/2000/svg"]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[12,17],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[12,22]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[15,9.34],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[15,7]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":true,"end":[16,6]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[16,2]}]},{"tag":"LineTo","args":[{"point":[7.89,2]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[2,2],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[22,22]}]}]}]]}]}},"children":[]}]},{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[9,9],"endings":{"tag":"Disconnected","args":[{}]},"cmds":[{"tag":"LineTo","args":[{"point":[9,10.76]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":true,"end":[7.89,12.55]}]},{"tag":"LineTo","args":[{"point":[6.109999999999999,13.450000000000001]}]},{"tag":"EllipticArcTo","args":[{"rx":2,"ry":2,"phi":0,"fA":false,"fS":false,"end":[5,15.24]}]},{"tag":"LineTo","args":[{"point":[5,16]}]},{"tag":"EllipticArcTo","args":[{"rx":1,"ry":1,"phi":0,"fA":false,"fS":false,"end":[6,17]}]},{"tag":"LineTo","args":[{"point":[17,17]}]}]}]]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-information","codePoint":59648},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[513,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,904],"end":[488.5,875.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,847],"end":[480,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,773],"end":[496,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[513,697],"end":[541,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[569,641],"end":[607,624]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,608],"end":[688,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,608],"end":[729.5,612]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[750,616],"end":[768,624]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[224,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[160,128]}]}]},{"tag":"LineTo","args":[{"point":[160,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[224,928]}]}]},{"tag":"LineTo","args":[{"point":[513,928]}]}]},{"start":[768,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]}]},{"start":[812.5,940.5],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,743],"end":[812.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,889],"end":[563.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,992],"end":[688,992]}]}]},{"tag":"LineTo","args":[{"point":[688,992]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[812.5,940.5]}]}]}]},{"start":[704,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[704,864]}]},{"tag":"LineTo","args":[{"point":[736,864]}]},{"tag":"LineTo","args":[{"point":[736,896]}]},{"tag":"LineTo","args":[{"point":[640,896]}]},{"tag":"LineTo","args":[{"point":[640,864]}]},{"tag":"LineTo","args":[{"point":[672,864]}]},{"tag":"LineTo","args":[{"point":[672,832]}]},{"tag":"LineTo","args":[{"point":[640,832]}]},{"tag":"LineTo","args":[{"point":[640,800]}]},{"tag":"LineTo","args":[{"point":[704,800]}]}]},{"start":[704,736],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[704,768]}]},{"tag":"LineTo","args":[{"point":[672,768]}]},{"tag":"LineTo","args":[{"point":[672,736]}]},{"tag":"LineTo","args":[{"point":[704,736]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-information-outline","codePoint":59649},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[552,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,659]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,681],"end":[837.5,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,765],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[647,992],"end":[611.5,974.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,957],"end":[552,928]}]}]},{"tag":"LineTo","args":[{"point":[552,928]}]},{"tag":"LineTo","args":[{"point":[552,928]}]}]},{"start":[531,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,878],"end":[517,858]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,838],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,640],"end":[712.5,641.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,643],"end":[736,647]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[531,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[790,918],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,876],"end":[832,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,756],"end":[790,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,672],"end":[688,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,672],"end":[586,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,756],"end":[544,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,876],"end":[586,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,960],"end":[688,960]}]}]},{"tag":"LineTo","args":[{"point":[688,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,960],"end":[790,918]}]}]}]},{"start":[704,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[704,864]}]},{"tag":"LineTo","args":[{"point":[736,864]}]},{"tag":"LineTo","args":[{"point":[736,896]}]},{"tag":"LineTo","args":[{"point":[640,896]}]},{"tag":"LineTo","args":[{"point":[640,864]}]},{"tag":"LineTo","args":[{"point":[672,864]}]},{"tag":"LineTo","args":[{"point":[672,832]}]},{"tag":"LineTo","args":[{"point":[640,832]}]},{"tag":"LineTo","args":[{"point":[640,800]}]},{"tag":"LineTo","args":[{"point":[704,800]}]}]},{"start":[704,736],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[704,768]}]},{"tag":"LineTo","args":[{"point":[672,768]}]},{"tag":"LineTo","args":[{"point":[672,736]}]},{"tag":"LineTo","args":[{"point":[704,736]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-forbidden","codePoint":59650},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[825,705],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[577,953]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[600,971],"end":[628,981.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[656,992],"end":[688,992]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,784],"end":[853.5,756]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[843,728],"end":[825,705]}]}]}]},{"start":[802,682],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,930]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[534,907],"end":[523,878]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,849],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[721,640],"end":[750,651]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[779,662],"end":[802,682]}]}]}]},{"start":[513,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,904],"end":[488.5,875.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,847],"end":[480,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,773],"end":[496,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[513,697],"end":[541,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[569,641],"end":[607,624]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,608],"end":[688,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,608],"end":[729.5,612]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[750,616],"end":[768,624]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[224,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[160,128]}]}]},{"tag":"LineTo","args":[{"point":[160,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[224,928]}]}]},{"tag":"LineTo","args":[{"point":[513,928]}]}]},{"start":[768,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-forbidden-outline","codePoint":59651},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,929],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,943],"end":[640,951.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[663,960],"end":[688,960]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,960],"end":[790,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,876],"end":[832,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,790],"end":[823.5,767.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[815,745],"end":[801,726]}]}]},{"tag":"LineTo","args":[{"point":[598,929]}]}]},{"start":[736,680.5],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[713,672],"end":[688,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,672],"end":[586,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,756],"end":[544,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,842],"end":[552.5,864.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,887],"end":[575,906]}]}]},{"tag":"LineTo","args":[{"point":[778,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[759,689],"end":[736,680.5]}]}]}]},{"start":[552,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,659]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,681],"end":[837.5,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,765],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[647,992],"end":[611.5,974.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,957],"end":[552,928]}]}]},{"tag":"LineTo","args":[{"point":[552,928]}]},{"tag":"LineTo","args":[{"point":[552,928]}]}]},{"start":[531,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,878],"end":[517,858]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,838],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,640],"end":[712.5,641.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,643],"end":[736,647]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[531,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-remove","codePoint":59652},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[513,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,904],"end":[488.5,875.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,847],"end":[480,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,773],"end":[496,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[513,697],"end":[541,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[569,641],"end":[607,624]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,608],"end":[688,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,608],"end":[729.5,612]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[750,616],"end":[768,624]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[224,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[160,128]}]}]},{"tag":"LineTo","args":[{"point":[160,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[224,928]}]}]},{"tag":"LineTo","args":[{"point":[513,928]}]}]},{"start":[768,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]}]},{"start":[812.5,940.5],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,743],"end":[812.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,889],"end":[563.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,992],"end":[688,992]}]}]},{"tag":"LineTo","args":[{"point":[688,992]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[812.5,940.5]}]}]}]},{"start":[800,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[800,832]}]},{"tag":"LineTo","args":[{"point":[576,832]}]},{"tag":"LineTo","args":[{"point":[576,800]}]},{"tag":"LineTo","args":[{"point":[800,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-remove-outline","codePoint":59653},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[552,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,659]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,681],"end":[837.5,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,765],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[647,992],"end":[611.5,974.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,957],"end":[552,928]}]}]},{"tag":"LineTo","args":[{"point":[552,928]}]},{"tag":"LineTo","args":[{"point":[552,928]}]}]},{"start":[531,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,878],"end":[517,858]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,838],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,640],"end":[712.5,641.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,643],"end":[736,647]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[531,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[790,918],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,876],"end":[832,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,756],"end":[790,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,672],"end":[688,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,672],"end":[586,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,756],"end":[544,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,876],"end":[586,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,960],"end":[688,960]}]}]},{"tag":"LineTo","args":[{"point":[688,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,960],"end":[790,918]}]}]}]},{"start":[800,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[800,832]}]},{"tag":"LineTo","args":[{"point":[576,832]}]},{"tag":"LineTo","args":[{"point":[576,800]}]},{"tag":"LineTo","args":[{"point":[800,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-checked","codePoint":59654},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[513,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,904],"end":[488.5,875.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,847],"end":[480,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,773],"end":[496,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[513,697],"end":[541,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[569,641],"end":[607,624]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,608],"end":[688,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,608],"end":[729.5,612]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[750,616],"end":[768,624]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[224,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[160,128]}]}]},{"tag":"LineTo","args":[{"point":[160,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[224,928]}]}]},{"tag":"LineTo","args":[{"point":[513,928]}]}]},{"start":[768,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]}]},{"start":[688,992],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,992],"end":[563.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,889],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,640],"end":[812.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,743],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]}]},{"start":[672,904],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[795,780]}]},{"tag":"LineTo","args":[{"point":[772,757]}]},{"tag":"LineTo","args":[{"point":[672,857]}]},{"tag":"LineTo","args":[{"point":[620,805]}]},{"tag":"LineTo","args":[{"point":[597,828]}]},{"tag":"LineTo","args":[{"point":[672,904]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-checked-outline","codePoint":59655},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[552,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,659]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,681],"end":[837.5,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,765],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[647,992],"end":[611.5,974.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,957],"end":[552,928]}]}]},{"tag":"LineTo","args":[{"point":[552,928]}]},{"tag":"LineTo","args":[{"point":[552,928]}]}]},{"start":[531,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,878],"end":[517,858]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,838],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,640],"end":[712.5,641.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,643],"end":[736,647]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[531,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[790,918],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,876],"end":[832,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,756],"end":[790,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,672],"end":[688,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,672],"end":[586,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,756],"end":[544,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,876],"end":[586,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,960],"end":[688,960]}]}]},{"tag":"LineTo","args":[{"point":[688,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,960],"end":[790,918]}]}]}]},{"start":[672,904],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[795,780]}]},{"tag":"LineTo","args":[{"point":[772,757]}]},{"tag":"LineTo","args":[{"point":[672,857]}]},{"tag":"LineTo","args":[{"point":[620,805]}]},{"tag":"LineTo","args":[{"point":[597,828]}]},{"tag":"LineTo","args":[{"point":[672,904]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-cancel","codePoint":59656},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[756,907],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[779,884]}]},{"tag":"LineTo","args":[{"point":[711,816]}]},{"tag":"LineTo","args":[{"point":[779,748]}]},{"tag":"LineTo","args":[{"point":[756,725]}]},{"tag":"LineTo","args":[{"point":[688,793]}]},{"tag":"LineTo","args":[{"point":[620,725]}]},{"tag":"LineTo","args":[{"point":[597,748]}]},{"tag":"LineTo","args":[{"point":[665,816]}]},{"tag":"LineTo","args":[{"point":[597,884]}]},{"tag":"LineTo","args":[{"point":[620,907]}]},{"tag":"LineTo","args":[{"point":[688,839]}]},{"tag":"LineTo","args":[{"point":[756,907]}]}]},{"start":[513,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,904],"end":[488.5,875.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,847],"end":[480,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,773],"end":[496,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[513,697],"end":[541,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[569,641],"end":[607,624]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,608],"end":[688,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,608],"end":[729.5,612]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[750,616],"end":[768,624]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[224,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[160,128]}]}]},{"tag":"LineTo","args":[{"point":[160,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[224,928]}]}]},{"tag":"LineTo","args":[{"point":[513,928]}]}]},{"start":[768,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]}]},{"start":[688,992],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,992],"end":[563.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,889],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,640],"end":[812.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,743],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-cancel-outline","codePoint":59657},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[756,907],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[779,884]}]},{"tag":"LineTo","args":[{"point":[711,816]}]},{"tag":"LineTo","args":[{"point":[779,748]}]},{"tag":"LineTo","args":[{"point":[756,725]}]},{"tag":"LineTo","args":[{"point":[688,793]}]},{"tag":"LineTo","args":[{"point":[620,725]}]},{"tag":"LineTo","args":[{"point":[597,748]}]},{"tag":"LineTo","args":[{"point":[665,816]}]},{"tag":"LineTo","args":[{"point":[597,884]}]},{"tag":"LineTo","args":[{"point":[620,907]}]},{"tag":"LineTo","args":[{"point":[688,839]}]},{"tag":"LineTo","args":[{"point":[756,907]}]}]},{"start":[552,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,659]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,681],"end":[837.5,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,765],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[647,992],"end":[611.5,974.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,957],"end":[552,928]}]}]},{"tag":"LineTo","args":[{"point":[552,928]}]},{"tag":"LineTo","args":[{"point":[552,928]}]}]},{"start":[531,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,878],"end":[517,858]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,838],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,640],"end":[712.5,641.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,643],"end":[736,647]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[531,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[790,918],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,876],"end":[832,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,756],"end":[790,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,672],"end":[688,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,672],"end":[586,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,756],"end":[544,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,876],"end":[586,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,960],"end":[688,960]}]}]},{"tag":"LineTo","args":[{"point":[688,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,960],"end":[790,918]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-error","codePoint":59658},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[448,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[656,576]}]},{"tag":"LineTo","args":[{"point":[768,766]}]},{"tag":"LineTo","args":[{"point":[768,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[224,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[160,128]}]}]},{"tag":"LineTo","args":[{"point":[160,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[224,928]}]}]},{"tag":"LineTo","args":[{"point":[448,928]}]}]},{"start":[768,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]}]},{"start":[448,992],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[864,992]}]},{"tag":"LineTo","args":[{"point":[656,640]}]},{"tag":"LineTo","args":[{"point":[448,992]}]}]},{"start":[672,768],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,864]}]},{"tag":"LineTo","args":[{"point":[640,864]}]},{"tag":"LineTo","args":[{"point":[640,768]}]},{"tag":"LineTo","args":[{"point":[672,768]}]}]},{"start":[672,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,928]}]},{"tag":"LineTo","args":[{"point":[640,928]}]},{"tag":"LineTo","args":[{"point":[640,896]}]},{"tag":"LineTo","args":[{"point":[672,896]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-error-outline","codePoint":59659},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,830],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[864,992]}]},{"tag":"LineTo","args":[{"point":[448,992]}]},{"tag":"LineTo","args":[{"point":[486,928]}]},{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,830]}]}]},{"start":[505,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[656,640]}]},{"tag":"LineTo","args":[{"point":[736,775]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[505,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[504,960],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[808,960]}]},{"tag":"LineTo","args":[{"point":[656,704]}]},{"tag":"LineTo","args":[{"point":[504,960]}]}]},{"start":[672,768],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,864]}]},{"tag":"LineTo","args":[{"point":[640,864]}]},{"tag":"LineTo","args":[{"point":[640,768]}]},{"tag":"LineTo","args":[{"point":[672,768]}]}]},{"start":[672,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,928]}]},{"tag":"LineTo","args":[{"point":[640,928]}]},{"tag":"LineTo","args":[{"point":[640,896]}]},{"tag":"LineTo","args":[{"point":[672,896]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-locked","codePoint":59660},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,584]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,580],"end":[745,578]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[733,576],"end":[720,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[660,576],"end":[618,618.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,661],"end":[576,720]}]}]},{"tag":"LineTo","args":[{"point":[576,739]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[562,744],"end":[553,756]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,768],"end":[544,784]}]}]},{"tag":"LineTo","args":[{"point":[544,928]}]},{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[608,320]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]}]},{"start":[576,64],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]}]},{"start":[608,720],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,674],"end":[641,641]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[674,608],"end":[720,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[766,608],"end":[799,641]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,674],"end":[832,720]}]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,768],"end":[854.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,787],"end":[864,800]}]}]},{"tag":"LineTo","args":[{"point":[864,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,973],"end":[854.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,992],"end":[832,992]}]}]},{"tag":"LineTo","args":[{"point":[608,992]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,992],"end":[585.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,973],"end":[576,960]}]}]},{"tag":"LineTo","args":[{"point":[576,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,787],"end":[585.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,768],"end":[608,768]}]}]},{"tag":"LineTo","args":[{"point":[608,720]}]}]},{"start":[640,768],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[800,768]}]},{"tag":"LineTo","args":[{"point":[800,720]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,687],"end":[776.5,663.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[753,640],"end":[720,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[687,640],"end":[663.5,663.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,687],"end":[640,720]}]}]},{"tag":"LineTo","args":[{"point":[640,768]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-locked-outline","codePoint":59661},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[576,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,619]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[796,632],"end":[814,659.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,687],"end":[832,720]}]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,768],"end":[854.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,787],"end":[864,800]}]}]},{"tag":"LineTo","args":[{"point":[864,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,973],"end":[854.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,992],"end":[832,992]}]}]},{"tag":"LineTo","args":[{"point":[608,992]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,992],"end":[585.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,973],"end":[576,960]}]}]},{"tag":"LineTo","args":[{"point":[576,928]}]}]},{"start":[576,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,787],"end":[585.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,768],"end":[608,768]}]}]},{"tag":"LineTo","args":[{"point":[608,768]}]},{"tag":"LineTo","args":[{"point":[608,720]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,674],"end":[641,641]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[674,608],"end":[720,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[724,608],"end":[728,608.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,609],"end":[736,609]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[576,896]}]}]},{"start":[800,768],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[800,720]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,687],"end":[776.5,663.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[753,640],"end":[720,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[687,640],"end":[663.5,663.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,687],"end":[640,720]}]}]},{"tag":"LineTo","args":[{"point":[640,768]}]},{"tag":"LineTo","args":[{"point":[800,768]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[624,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,800],"end":[612.5,804.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,809],"end":[608,816]}]}]},{"tag":"LineTo","args":[{"point":[608,944]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,951],"end":[613,955.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,960],"end":[624,960]}]}]},{"tag":"LineTo","args":[{"point":[816,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[823,960],"end":[827.5,955.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,951],"end":[832,944]}]}]},{"tag":"LineTo","args":[{"point":[832,816]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,809],"end":[827,804.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[822,800],"end":[816,800]}]}]},{"tag":"LineTo","args":[{"point":[624,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-unlocked","codePoint":59662},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,552]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,548],"end":[745,546]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[733,544],"end":[720,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[660,544],"end":[618,586]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,628],"end":[576,688]}]}]},{"tag":"LineTo","args":[{"point":[576,744]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[562,753],"end":[553,767.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,782],"end":[544,800]}]}]},{"tag":"LineTo","args":[{"point":[544,928]}]},{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[608,320]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]}]},{"start":[576,64],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]}]},{"start":[816,768],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,768]}]},{"tag":"LineTo","args":[{"point":[640,688]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,655],"end":[663.5,631.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[687,608],"end":[720,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[753,608],"end":[776.5,631.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,655],"end":[800,688]}]}]},{"tag":"LineTo","args":[{"point":[800,704]}]},{"tag":"LineTo","args":[{"point":[832,704]}]},{"tag":"LineTo","args":[{"point":[832,688]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,642],"end":[799,609]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[766,576],"end":[720,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[674,576],"end":[641,608.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,641],"end":[608,688]}]}]},{"tag":"LineTo","args":[{"point":[608,768]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,768],"end":[585.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,787],"end":[576,800]}]}]},{"tag":"LineTo","args":[{"point":[576,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,973],"end":[585.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,992],"end":[608,992]}]}]},{"tag":"LineTo","args":[{"point":[832,992]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,992],"end":[854.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,973],"end":[864,960]}]}]},{"tag":"LineTo","args":[{"point":[864,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,787],"end":[854.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,768],"end":[832,768]}]}]},{"tag":"LineTo","args":[{"point":[816,768]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-unlocked-outline","codePoint":59663},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,587],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,587]}]},{"tag":"LineTo","args":[{"point":[768,587]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[796,600],"end":[814,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,655],"end":[832,688]}]}]},{"tag":"LineTo","args":[{"point":[832,704]}]},{"tag":"LineTo","args":[{"point":[800,704]}]},{"tag":"LineTo","args":[{"point":[800,688]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,655],"end":[776.5,631.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[753,608],"end":[720,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[687,608],"end":[663.5,631.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,655],"end":[640,688]}]}]},{"tag":"LineTo","args":[{"point":[640,768]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,768],"end":[854.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,787],"end":[864,800]}]}]},{"tag":"LineTo","args":[{"point":[864,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,973],"end":[854.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[845,992],"end":[832,992]}]}]},{"tag":"LineTo","args":[{"point":[608,992]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,992],"end":[585.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,973],"end":[576,960]}]}]},{"tag":"LineTo","args":[{"point":[576,928]}]},{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,587]}]}]},{"start":[576,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,787],"end":[585.5,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,768],"end":[608,768]}]}]},{"tag":"LineTo","args":[{"point":[608,688]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,642],"end":[641,609]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[674,576],"end":[720,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[724,576],"end":[728,576.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,577],"end":[736,577]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[576,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[624,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,800],"end":[612.5,804.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,809],"end":[608,816]}]}]},{"tag":"LineTo","args":[{"point":[608,944]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,951],"end":[613,955.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,960],"end":[624,960]}]}]},{"tag":"LineTo","args":[{"point":[816,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[823,960],"end":[827.5,955.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,951],"end":[832,944]}]}]},{"tag":"LineTo","args":[{"point":[832,816]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,809],"end":[827,804.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[822,800],"end":[816,800]}]}]},{"tag":"LineTo","args":[{"point":[624,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-search","codePoint":59664},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[725,876],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[748,853]}]},{"tag":"LineTo","args":[{"point":[577,682]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[591,663],"end":[599.5,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,617],"end":[608,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,532],"end":[566,490]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[524,448],"end":[464,448]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,448],"end":[362,490]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,532],"end":[320,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,652],"end":[362,694]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,736],"end":[464,736]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,736],"end":[512.5,727.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[535,719],"end":[554,705]}]}]},{"tag":"LineTo","args":[{"point":[725,876]}]}]},{"start":[608,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[627,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,352],"end":[672,352]}]}]},{"tag":"LineTo","args":[{"point":[832,352]}]},{"tag":"LineTo","args":[{"point":[832,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[768,960]}]}]},{"tag":"LineTo","args":[{"point":[288,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[224,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[288,96]}]}]},{"tag":"LineTo","args":[{"point":[608,96]}]}]},{"start":[832,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[832,320]}]}]},{"start":[464,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[418,704],"end":[385,671]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,638],"end":[352,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,546],"end":[385,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[418,480],"end":[464,480]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,480],"end":[543,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,546],"end":[576,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,638],"end":[543,671]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,704],"end":[464,704]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-search-outline","codePoint":59665},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[725,876],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[748,853]}]},{"tag":"LineTo","args":[{"point":[577,682]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[591,663],"end":[599.5,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,617],"end":[608,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,532],"end":[566,490]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[524,448],"end":[464,448]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,448],"end":[362,490]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,532],"end":[320,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,652],"end":[362,694]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,736],"end":[464,736]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,736],"end":[512.5,727.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[535,719],"end":[554,705]}]}]},{"tag":"LineTo","args":[{"point":[725,876]}]}]},{"start":[624,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,320]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[624,96]}]}]},{"start":[608,128],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[626.5,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,352],"end":[672,352]}]}]},{"tag":"LineTo","args":[{"point":[800,352]}]},{"tag":"LineTo","args":[{"point":[800,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,909],"end":[790.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[781,928],"end":[768,928]}]}]},{"tag":"LineTo","args":[{"point":[288,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,928],"end":[265.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,909],"end":[256,896]}]}]},{"tag":"LineTo","args":[{"point":[256,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,147],"end":[265.5,137.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,128],"end":[288,128]}]}]},{"tag":"LineTo","args":[{"point":[608,128]}]}]},{"start":[640,144],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[790,320]}]},{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,144]}]}]},{"start":[464,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[418,704],"end":[385,671]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,638],"end":[352,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,546],"end":[385,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[418,480],"end":[464,480]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,480],"end":[543,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,546],"end":[576,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,638],"end":[543,671]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,704],"end":[464,704]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-code","codePoint":59666},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[608,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,352]}]},{"tag":"LineTo","args":[{"point":[672,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,352],"end":[627,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[608,96]}]}]},{"start":[832,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[832,320]}]}]},{"start":[288,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[416,576]}]},{"tag":"LineTo","args":[{"point":[438,598]}]},{"tag":"LineTo","args":[{"point":[333,704]}]},{"tag":"LineTo","args":[{"point":[438,810]}]},{"tag":"LineTo","args":[{"point":[416,832]}]},{"tag":"LineTo","args":[{"point":[288,704]}]}]},{"start":[618,810],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[723,704]}]},{"tag":"LineTo","args":[{"point":[618,598]}]},{"tag":"LineTo","args":[{"point":[640,576]}]},{"tag":"LineTo","args":[{"point":[768,704]}]},{"tag":"LineTo","args":[{"point":[640,832]}]},{"tag":"LineTo","args":[{"point":[618,810]}]}]},{"start":[576,576],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,832]}]},{"tag":"LineTo","args":[{"point":[480,832]}]},{"tag":"LineTo","args":[{"point":[544,576]}]},{"tag":"LineTo","args":[{"point":[576,576]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-code-outline","codePoint":59667},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[624,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,320]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[624,96]}]}]},{"start":[608,128],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[626.5,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,352],"end":[672,352]}]}]},{"tag":"LineTo","args":[{"point":[800,352]}]},{"tag":"LineTo","args":[{"point":[800,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,909],"end":[790.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[781,928],"end":[768,928]}]}]},{"tag":"LineTo","args":[{"point":[288,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,928],"end":[265.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,909],"end":[256,896]}]}]},{"tag":"LineTo","args":[{"point":[256,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,147],"end":[265.5,137.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,128],"end":[288,128]}]}]},{"tag":"LineTo","args":[{"point":[608,128]}]}]},{"start":[640,144],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[790,320]}]},{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,144]}]}]},{"start":[576,576],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,832]}]},{"tag":"LineTo","args":[{"point":[480,832]}]},{"tag":"LineTo","args":[{"point":[544,576]}]},{"tag":"LineTo","args":[{"point":[576,576]}]}]},{"start":[438,778],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[365,704]}]},{"tag":"LineTo","args":[{"point":[438,630]}]},{"tag":"LineTo","args":[{"point":[416,608]}]},{"tag":"LineTo","args":[{"point":[320,704]}]},{"tag":"LineTo","args":[{"point":[416,800]}]},{"tag":"LineTo","args":[{"point":[438,778]}]}]},{"start":[618,778],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[691,704]}]},{"tag":"LineTo","args":[{"point":[618,630]}]},{"tag":"LineTo","args":[{"point":[640,608]}]},{"tag":"LineTo","args":[{"point":[736,704]}]},{"tag":"LineTo","args":[{"point":[640,800]}]},{"tag":"LineTo","args":[{"point":[618,778]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-text","codePoint":59668},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[608,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,352]}]},{"tag":"LineTo","args":[{"point":[672,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,352],"end":[627,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[608,96]}]}]},{"start":[480,192],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[480,256]}]},{"tag":"LineTo","args":[{"point":[448,256]}]},{"tag":"LineTo","args":[{"point":[448,224]}]},{"tag":"LineTo","args":[{"point":[416,224]}]},{"tag":"LineTo","args":[{"point":[416,352]}]},{"tag":"LineTo","args":[{"point":[384,352]}]},{"tag":"LineTo","args":[{"point":[384,224]}]},{"tag":"LineTo","args":[{"point":[352,224]}]},{"tag":"LineTo","args":[{"point":[352,256]}]},{"tag":"LineTo","args":[{"point":[320,256]}]},{"tag":"LineTo","args":[{"point":[320,192]}]},{"tag":"LineTo","args":[{"point":[480,192]}]}]},{"start":[832,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[832,320]}]}]},{"start":[736,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,544]}]},{"tag":"LineTo","args":[{"point":[320,544]}]},{"tag":"LineTo","args":[{"point":[320,512]}]},{"tag":"LineTo","args":[{"point":[736,512]}]}]},{"start":[736,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,448]}]},{"tag":"LineTo","args":[{"point":[320,448]}]},{"tag":"LineTo","args":[{"point":[320,416]}]},{"tag":"LineTo","args":[{"point":[736,416]}]}]},{"start":[736,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,640]}]},{"tag":"LineTo","args":[{"point":[320,640]}]},{"tag":"LineTo","args":[{"point":[320,608]}]},{"tag":"LineTo","args":[{"point":[736,608]}]}]},{"start":[736,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,736]}]},{"tag":"LineTo","args":[{"point":[320,736]}]},{"tag":"LineTo","args":[{"point":[320,704]}]},{"tag":"LineTo","args":[{"point":[736,704]}]}]},{"start":[736,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,832]}]},{"tag":"LineTo","args":[{"point":[320,832]}]},{"tag":"LineTo","args":[{"point":[320,800]}]},{"tag":"LineTo","args":[{"point":[736,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-text-outline","codePoint":59669},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[624,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,320]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[624,96]}]}]},{"start":[608,128],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[626.5,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,352],"end":[672,352]}]}]},{"tag":"LineTo","args":[{"point":[800,352]}]},{"tag":"LineTo","args":[{"point":[800,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,909],"end":[790.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[781,928],"end":[768,928]}]}]},{"tag":"LineTo","args":[{"point":[288,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,928],"end":[265.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,909],"end":[256,896]}]}]},{"tag":"LineTo","args":[{"point":[256,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,147],"end":[265.5,137.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,128],"end":[288,128]}]}]},{"tag":"LineTo","args":[{"point":[608,128]}]}]},{"start":[640,144],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[790,320]}]},{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,144]}]}]},{"start":[480,192],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[480,256]}]},{"tag":"LineTo","args":[{"point":[448,256]}]},{"tag":"LineTo","args":[{"point":[448,224]}]},{"tag":"LineTo","args":[{"point":[416,224]}]},{"tag":"LineTo","args":[{"point":[416,352]}]},{"tag":"LineTo","args":[{"point":[384,352]}]},{"tag":"LineTo","args":[{"point":[384,224]}]},{"tag":"LineTo","args":[{"point":[352,224]}]},{"tag":"LineTo","args":[{"point":[352,256]}]},{"tag":"LineTo","args":[{"point":[320,256]}]},{"tag":"LineTo","args":[{"point":[320,192]}]},{"tag":"LineTo","args":[{"point":[480,192]}]}]},{"start":[736,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,544]}]},{"tag":"LineTo","args":[{"point":[320,544]}]},{"tag":"LineTo","args":[{"point":[320,512]}]},{"tag":"LineTo","args":[{"point":[736,512]}]}]},{"start":[736,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,448]}]},{"tag":"LineTo","args":[{"point":[320,448]}]},{"tag":"LineTo","args":[{"point":[320,416]}]},{"tag":"LineTo","args":[{"point":[736,416]}]}]},{"start":[736,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,640]}]},{"tag":"LineTo","args":[{"point":[320,640]}]},{"tag":"LineTo","args":[{"point":[320,608]}]},{"tag":"LineTo","args":[{"point":[736,608]}]}]},{"start":[736,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,736]}]},{"tag":"LineTo","args":[{"point":[320,736]}]},{"tag":"LineTo","args":[{"point":[320,704]}]},{"tag":"LineTo","args":[{"point":[736,704]}]}]},{"start":[736,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,832]}]},{"tag":"LineTo","args":[{"point":[320,832]}]},{"tag":"LineTo","args":[{"point":[320,800]}]},{"tag":"LineTo","args":[{"point":[736,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"text_format","codePoint":59670},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[592,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[432,491]}]},{"tag":"LineTo","args":[{"point":[512,277]}]},{"tag":"LineTo","args":[{"point":[592,491]}]}]},{"start":[618,567],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[658,661]}]},{"tag":"LineTo","args":[{"point":[746,661]}]},{"tag":"LineTo","args":[{"point":[544,191]}]},{"tag":"LineTo","args":[{"point":[480,191]}]},{"tag":"LineTo","args":[{"point":[278,661]}]},{"tag":"LineTo","args":[{"point":[366,661]}]},{"tag":"LineTo","args":[{"point":[406,567]}]},{"tag":"LineTo","args":[{"point":[618,567]}]}]},{"start":[214,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[810,747]}]},{"tag":"LineTo","args":[{"point":[214,747]}]},{"tag":"LineTo","args":[{"point":[214,831]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"chat_bubble","codePoint":59671},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,107],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,107],"end":[111,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,157],"end":[86,191]}]}]},{"tag":"LineTo","args":[{"point":[86,959]}]},{"tag":"LineTo","args":[{"point":[256,789]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,789],"end":[913,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,737],"end":[938,703]}]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,157],"end":[913,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,107],"end":[854,107]}]}]},{"tag":"LineTo","args":[{"point":[170,107]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"movie","codePoint":59672},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[854,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,363]}]},{"tag":"LineTo","args":[{"point":[640,191]}]},{"tag":"LineTo","args":[{"point":[554,191]}]},{"tag":"LineTo","args":[{"point":[640,363]}]},{"tag":"LineTo","args":[{"point":[512,363]}]},{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[342,191]}]},{"tag":"LineTo","args":[{"point":[426,363]}]},{"tag":"LineTo","args":[{"point":[298,363]}]},{"tag":"LineTo","args":[{"point":[214,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"LineTo","args":[{"point":[768,191]}]},{"tag":"LineTo","args":[{"point":[854,363]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"videocam","codePoint":59673},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[726,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,301],"end":[713,289]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,277],"end":[682,277]}]}]},{"tag":"LineTo","args":[{"point":[170,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[152,277],"end":[140,289]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,301],"end":[128,319]}]}]},{"tag":"LineTo","args":[{"point":[128,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,765],"end":[140,777]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[152,789],"end":[170,789]}]}]},{"tag":"LineTo","args":[{"point":[682,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,789],"end":[713,777]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,765],"end":[726,747]}]}]},{"tag":"LineTo","args":[{"point":[726,597]}]},{"tag":"LineTo","args":[{"point":[896,767]}]},{"tag":"LineTo","args":[{"point":[896,299]}]},{"tag":"LineTo","args":[{"point":[726,469]}]},{"tag":"LineTo","args":[{"point":[726,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-add","codePoint":59674},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[576,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,832]}]},{"tag":"LineTo","args":[{"point":[672,832]}]},{"tag":"LineTo","args":[{"point":[672,928]}]},{"tag":"LineTo","args":[{"point":[704,928]}]},{"tag":"LineTo","args":[{"point":[704,832]}]},{"tag":"LineTo","args":[{"point":[800,832]}]},{"tag":"LineTo","args":[{"point":[800,800]}]},{"tag":"LineTo","args":[{"point":[704,800]}]},{"tag":"LineTo","args":[{"point":[704,704]}]},{"tag":"LineTo","args":[{"point":[672,704]}]},{"tag":"LineTo","args":[{"point":[672,800]}]},{"tag":"LineTo","args":[{"point":[576,800]}]}]},{"start":[513,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"LineTo","args":[{"point":[513,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,904],"end":[488.5,875.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,847],"end":[480,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,773],"end":[496,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[513,697],"end":[541,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[569,641],"end":[607,624]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,608],"end":[688,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,608],"end":[729.5,612]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[750,616],"end":[768,624]}]}]},{"tag":"LineTo","args":[{"point":[768,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,320],"end":[563,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,64]}]},{"tag":"LineTo","args":[{"point":[224,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[160,128]}]}]},{"tag":"LineTo","args":[{"point":[160,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[224,928]}]}]},{"tag":"LineTo","args":[{"point":[513,928]}]}]},{"start":[768,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[576,256]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]}]},{"start":[688,992],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,992],"end":[563.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,889],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,640],"end":[812.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,743],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-add-outline","codePoint":59675},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[576,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,832]}]},{"tag":"LineTo","args":[{"point":[672,832]}]},{"tag":"LineTo","args":[{"point":[672,928]}]},{"tag":"LineTo","args":[{"point":[704,928]}]},{"tag":"LineTo","args":[{"point":[704,832]}]},{"tag":"LineTo","args":[{"point":[800,832]}]},{"tag":"LineTo","args":[{"point":[800,800]}]},{"tag":"LineTo","args":[{"point":[704,800]}]},{"tag":"LineTo","args":[{"point":[704,704]}]},{"tag":"LineTo","args":[{"point":[672,704]}]},{"tag":"LineTo","args":[{"point":[672,800]}]},{"tag":"LineTo","args":[{"point":[576,800]}]}]},{"start":[552,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[224,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,928],"end":[178.5,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,890],"end":[160,864]}]}]},{"tag":"LineTo","args":[{"point":[160,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,102],"end":[179,83]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,64],"end":[224,64]}]}]},{"tag":"LineTo","args":[{"point":[576,64]}]},{"tag":"LineTo","args":[{"point":[768,288]}]},{"tag":"LineTo","args":[{"point":[768,659]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,681],"end":[837.5,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,765],"end":[864,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,889],"end":[812.5,940.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[761,992],"end":[688,992]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[647,992],"end":[611.5,974.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,957],"end":[552,928]}]}]},{"tag":"LineTo","args":[{"point":[552,928]}]},{"tag":"LineTo","args":[{"point":[552,928]}]}]},{"start":[531,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"LineTo","args":[{"point":[531,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,878],"end":[517,858]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,838],"end":[512,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,743],"end":[563.5,691.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,640],"end":[688,640]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,640],"end":[712.5,641.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,643],"end":[736,647]}]}]},{"tag":"LineTo","args":[{"point":[736,320]}]},{"tag":"LineTo","args":[{"point":[608,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,320],"end":[562.5,301.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,283],"end":[544,256]}]}]},{"tag":"LineTo","args":[{"point":[544,96]}]},{"tag":"LineTo","args":[{"point":[224,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,96],"end":[201.5,105.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,115],"end":[192,128]}]}]},{"tag":"LineTo","args":[{"point":[192,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,877],"end":[201.5,886.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[211,896],"end":[224,896]}]}]},{"tag":"LineTo","args":[{"point":[531,896]}]}]},{"start":[576,112],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,269],"end":[585.5,278.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,288],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[726,288]}]},{"tag":"LineTo","args":[{"point":[576,112]}]}]},{"start":[790,918],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,876],"end":[832,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,756],"end":[790,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,672],"end":[688,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,672],"end":[586,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,756],"end":[544,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,876],"end":[586,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,960],"end":[688,960]}]}]},{"tag":"LineTo","args":[{"point":[688,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,960],"end":[790,918]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"documents","codePoint":59676},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[736,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,987],"end":[717.5,1005.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[699,1024],"end":[672,1024]}]}]},{"tag":"LineTo","args":[{"point":[192,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[165,1024],"end":[146.5,1005]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,986],"end":[128,960]}]}]},{"tag":"LineTo","args":[{"point":[128,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,198],"end":[147,179]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[166,160],"end":[192,160]}]}]},{"tag":"LineTo","args":[{"point":[512,160]}]},{"tag":"LineTo","args":[{"point":[512,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,379],"end":[531,397.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,416],"end":[576,416]}]}]},{"tag":"LineTo","args":[{"point":[736,416]}]},{"tag":"LineTo","args":[{"point":[736,864]}]},{"tag":"LineTo","args":[{"point":[768,864]}]},{"tag":"LineTo","args":[{"point":[768,371]}]},{"tag":"LineTo","args":[{"point":[560,128]}]},{"tag":"LineTo","args":[{"point":[320,128]}]},{"tag":"LineTo","args":[{"point":[320,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,38],"end":[339,19]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,0],"end":[384,0]}]}]},{"tag":"LineTo","args":[{"point":[704,0]}]},{"tag":"LineTo","args":[{"point":[704,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,219],"end":[723,237.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[742,256],"end":[768,256]}]}]},{"tag":"LineTo","args":[{"point":[928,256]}]},{"tag":"LineTo","args":[{"point":[928,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,827],"end":[909.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[891,864],"end":[864,864]}]}]},{"tag":"LineTo","args":[{"point":[736,864]}]}]},{"start":[928,224],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[755,224],"end":[745.5,214.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,205],"end":[736,192]}]}]},{"tag":"LineTo","args":[{"point":[736,0]}]},{"tag":"LineTo","args":[{"point":[928,224]}]}]},{"start":[736,384],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[576,384]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[563,384],"end":[553.5,374.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,365],"end":[544,352]}]}]},{"tag":"LineTo","args":[{"point":[544,160]}]},{"tag":"LineTo","args":[{"point":[736,384]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"documents-outline","codePoint":59677},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[736,384],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[544,160]}]},{"tag":"LineTo","args":[{"point":[352,160]}]},{"tag":"LineTo","args":[{"point":[352,64]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,51],"end":[361.5,41.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[371,32],"end":[384,32]}]}]},{"tag":"LineTo","args":[{"point":[704,32]}]},{"tag":"LineTo","args":[{"point":[704,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,219],"end":[722.5,237.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[741,256],"end":[768,256]}]}]},{"tag":"LineTo","args":[{"point":[896,256]}]},{"tag":"LineTo","args":[{"point":[896,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,813],"end":[886.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[877,832],"end":[864,832]}]}]},{"tag":"LineTo","args":[{"point":[736,832]}]},{"tag":"LineTo","args":[{"point":[736,400]}]},{"tag":"LineTo","args":[{"point":[736,384]}]}]},{"start":[320,160],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[192,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[166,160],"end":[147,179]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,198],"end":[128,224]}]}]},{"tag":"LineTo","args":[{"point":[128,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,986],"end":[146.5,1005]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[165,1024],"end":[192,1024]}]}]},{"tag":"LineTo","args":[{"point":[672,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[699,1024],"end":[717.5,1005.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,987],"end":[736,960]}]}]},{"tag":"LineTo","args":[{"point":[736,864]}]},{"tag":"LineTo","args":[{"point":[864,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[891,864],"end":[909.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,827],"end":[928,800]}]}]},{"tag":"LineTo","args":[{"point":[928,224]}]},{"tag":"LineTo","args":[{"point":[736,0]}]},{"tag":"LineTo","args":[{"point":[384,0]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,0],"end":[339,19]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,38],"end":[320,64]}]}]},{"tag":"LineTo","args":[{"point":[320,160]}]}]},{"start":[736,48],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[886,224]}]},{"tag":"LineTo","args":[{"point":[768,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[755,224],"end":[745.5,214.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,205],"end":[736,192]}]}]},{"tag":"LineTo","args":[{"point":[736,48]}]}]},{"start":[512,192],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,379],"end":[530.5,397.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,416],"end":[576,416]}]}]},{"tag":"LineTo","args":[{"point":[704,416]}]},{"tag":"LineTo","args":[{"point":[704,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,973],"end":[694.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[685,992],"end":[672,992]}]}]},{"tag":"LineTo","args":[{"point":[192,992]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[179,992],"end":[169.5,982.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,973],"end":[160,960]}]}]},{"tag":"LineTo","args":[{"point":[160,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,211],"end":[169.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[179,192],"end":[192,192]}]}]},{"tag":"LineTo","args":[{"point":[512,192]}]}]},{"start":[544,208],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[694,384]}]},{"tag":"LineTo","args":[{"point":[576,384]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[563,384],"end":[553.5,374.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,365],"end":[544,352]}]}]},{"tag":"LineTo","args":[{"point":[544,208]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-information","codePoint":59678},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[673,832],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[673,832]}]},{"tag":"LineTo","args":[{"point":[673,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[657,808],"end":[648.5,779.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,751],"end":[640,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,677],"end":[656,639]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[673,601],"end":[701,573]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[729,545],"end":[767,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[805,512],"end":[848,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[869,512],"end":[889.5,516]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[910,520],"end":[928,528]}]}]},{"tag":"LineTo","args":[{"point":[928,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,293],"end":[909,274.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,256],"end":[864,256]}]}]},{"tag":"LineTo","args":[{"point":[480,256]}]},{"tag":"LineTo","args":[{"point":[416,128]}]},{"tag":"LineTo","args":[{"point":[64,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,128],"end":[19,146.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,165],"end":[0,192]}]}]},{"tag":"LineTo","args":[{"point":[0,768]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,795],"end":[19,813.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,832],"end":[64,832]}]}]},{"tag":"LineTo","args":[{"point":[673,832]}]}]},{"start":[928,384],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[928,416]}]},{"tag":"LineTo","args":[{"point":[0,416]}]},{"tag":"LineTo","args":[{"point":[0,384]}]},{"tag":"LineTo","args":[{"point":[928,384]}]}]},{"start":[972.5,844.5],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,793],"end":[1024,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,647],"end":[972.5,595.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,544],"end":[848,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,544],"end":[723.5,595.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,647],"end":[672,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,793],"end":[723.5,844.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,896],"end":[848,896]}]}]},{"tag":"LineTo","args":[{"point":[848,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,896],"end":[972.5,844.5]}]}]}]},{"start":[864,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[864,768]}]},{"tag":"LineTo","args":[{"point":[896,768]}]},{"tag":"LineTo","args":[{"point":[896,800]}]},{"tag":"LineTo","args":[{"point":[800,800]}]},{"tag":"LineTo","args":[{"point":[800,768]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"LineTo","args":[{"point":[832,736]}]},{"tag":"LineTo","args":[{"point":[800,736]}]},{"tag":"LineTo","args":[{"point":[800,704]}]},{"tag":"LineTo","args":[{"point":[864,704]}]}]},{"start":[864,640],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[864,672]}]},{"tag":"LineTo","args":[{"point":[832,672]}]},{"tag":"LineTo","args":[{"point":[832,640]}]},{"tag":"LineTo","args":[{"point":[864,640]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-information-outline","codePoint":59679},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[64,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,800],"end":[41.5,790.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,781],"end":[32,768]}]}]},{"tag":"LineTo","args":[{"point":[32,416]}]},{"tag":"LineTo","args":[{"point":[896,416]}]},{"tag":"LineTo","args":[{"point":[896,551]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[885,547],"end":[872.5,545.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[860,544],"end":[848,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,544],"end":[723.5,595.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,647],"end":[672,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,742],"end":[677,762]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,782],"end":[691,800]}]}]},{"tag":"LineTo","args":[{"point":[691,800]}]},{"tag":"LineTo","args":[{"point":[64,800]}]}]},{"start":[712,832],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[712,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,861],"end":[771.5,878.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[807,896],"end":[848,896]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,896],"end":[972.5,844.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,793],"end":[1024,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,669],"end":[997.5,627]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[971,585],"end":[928,563]}]}]},{"tag":"LineTo","args":[{"point":[928,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,293],"end":[909,274.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,256],"end":[864,256]}]}]},{"tag":"LineTo","args":[{"point":[480,256]}]},{"tag":"LineTo","args":[{"point":[416,128]}]},{"tag":"LineTo","args":[{"point":[64,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,128],"end":[19,146.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,165],"end":[0,192]}]}]},{"tag":"LineTo","args":[{"point":[0,768]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,795],"end":[19,813.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,832],"end":[64,832]}]}]},{"tag":"LineTo","args":[{"point":[712,832]}]}]},{"start":[32,384],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[32,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,179],"end":[41.5,169.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,160],"end":[64,160]}]}]},{"tag":"LineTo","args":[{"point":[397,160]}]},{"tag":"LineTo","args":[{"point":[460,288]}]},{"tag":"LineTo","args":[{"point":[864,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[877,288],"end":[886.5,297.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,307],"end":[896,320]}]}]},{"tag":"LineTo","args":[{"point":[896,384]}]},{"tag":"LineTo","args":[{"point":[32,384]}]}]},{"start":[848,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,864],"end":[746,822]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,780],"end":[704,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,660],"end":[746,618]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,576],"end":[848,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,576],"end":[950,618]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[992,660],"end":[992,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[992,780],"end":[950,822]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,864],"end":[848,864]}]}]}]},{"start":[864,640],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[864,672]}]},{"tag":"LineTo","args":[{"point":[832,672]}]},{"tag":"LineTo","args":[{"point":[832,640]}]},{"tag":"LineTo","args":[{"point":[864,640]}]}]},{"start":[864,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[864,768]}]},{"tag":"LineTo","args":[{"point":[896,768]}]},{"tag":"LineTo","args":[{"point":[896,800]}]},{"tag":"LineTo","args":[{"point":[800,800]}]},{"tag":"LineTo","args":[{"point":[800,768]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"LineTo","args":[{"point":[832,736]}]},{"tag":"LineTo","args":[{"point":[800,736]}]},{"tag":"LineTo","args":[{"point":[800,704]}]},{"tag":"LineTo","args":[{"point":[864,704]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-remove","codePoint":59680},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[673,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[673,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[657,840],"end":[648.5,811.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,783],"end":[640,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,709],"end":[656,671]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[673,633],"end":[701,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[729,577],"end":[767,560]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[805,544],"end":[848,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[869,544],"end":[889.5,548]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[910,552],"end":[928,560]}]}]},{"tag":"LineTo","args":[{"point":[928,448]}]},{"tag":"LineTo","args":[{"point":[0,448]}]},{"tag":"LineTo","args":[{"point":[0,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,827],"end":[19,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,864],"end":[64,864]}]}]},{"tag":"LineTo","args":[{"point":[673,864]}]}]},{"start":[928,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[928,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,325],"end":[909,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,288],"end":[864,288]}]}]},{"tag":"LineTo","args":[{"point":[480,288]}]},{"tag":"LineTo","args":[{"point":[416,160]}]},{"tag":"LineTo","args":[{"point":[64,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,160],"end":[19,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,197],"end":[0,224]}]}]},{"tag":"LineTo","args":[{"point":[0,416]}]},{"tag":"LineTo","args":[{"point":[928,416]}]}]},{"start":[972.5,876.5],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,825],"end":[1024,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,679],"end":[972.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,576],"end":[848,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,576],"end":[723.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,679],"end":[672,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,825],"end":[723.5,876.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,928],"end":[848,928]}]}]},{"tag":"LineTo","args":[{"point":[848,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,928],"end":[972.5,876.5]}]}]}]},{"start":[960,736],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[960,768]}]},{"tag":"LineTo","args":[{"point":[736,768]}]},{"tag":"LineTo","args":[{"point":[736,736]}]},{"tag":"LineTo","args":[{"point":[960,736]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-remove-outline","codePoint":59681},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[64,832],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,832],"end":[41.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,813],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,448]}]},{"tag":"LineTo","args":[{"point":[896,448]}]},{"tag":"LineTo","args":[{"point":[896,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[885,579],"end":[872.5,577.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[860,576],"end":[848,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,576],"end":[723.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,679],"end":[672,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,774],"end":[677,794]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,814],"end":[691,832]}]}]},{"tag":"LineTo","args":[{"point":[691,832]}]},{"tag":"LineTo","args":[{"point":[64,832]}]}]},{"start":[712,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[712,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,893],"end":[771.5,910.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[807,928],"end":[848,928]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,928],"end":[972.5,876.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,825],"end":[1024,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,701],"end":[997.5,659]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[971,617],"end":[928,595]}]}]},{"tag":"LineTo","args":[{"point":[928,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,325],"end":[909,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,288],"end":[864,288]}]}]},{"tag":"LineTo","args":[{"point":[480,288]}]},{"tag":"LineTo","args":[{"point":[416,160]}]},{"tag":"LineTo","args":[{"point":[64,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,160],"end":[19,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,197],"end":[0,224]}]}]},{"tag":"LineTo","args":[{"point":[0,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,827],"end":[19,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,864],"end":[64,864]}]}]},{"tag":"LineTo","args":[{"point":[712,864]}]}]},{"start":[32,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[32,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,211],"end":[41.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,192],"end":[64,192]}]}]},{"tag":"LineTo","args":[{"point":[397,192]}]},{"tag":"LineTo","args":[{"point":[460,320]}]},{"tag":"LineTo","args":[{"point":[864,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[877,320],"end":[886.5,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,339],"end":[896,352]}]}]},{"tag":"LineTo","args":[{"point":[896,416]}]},{"tag":"LineTo","args":[{"point":[32,416]}]}]},{"start":[848,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,896],"end":[746,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,812],"end":[704,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,692],"end":[746,650]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,608],"end":[848,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,608],"end":[950,650]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[992,692],"end":[992,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[992,812],"end":[950,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,896],"end":[848,896]}]}]}]},{"start":[960,736],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[960,768]}]},{"tag":"LineTo","args":[{"point":[736,768]}]},{"tag":"LineTo","args":[{"point":[736,736]}]},{"tag":"LineTo","args":[{"point":[960,736]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-add","codePoint":59682},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[832,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[864,864]}]},{"tag":"LineTo","args":[{"point":[864,768]}]},{"tag":"LineTo","args":[{"point":[960,768]}]},{"tag":"LineTo","args":[{"point":[960,736]}]},{"tag":"LineTo","args":[{"point":[864,736]}]},{"tag":"LineTo","args":[{"point":[864,640]}]},{"tag":"LineTo","args":[{"point":[832,640]}]},{"tag":"LineTo","args":[{"point":[832,736]}]},{"tag":"LineTo","args":[{"point":[736,736]}]},{"tag":"LineTo","args":[{"point":[736,768]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"LineTo","args":[{"point":[832,864]}]}]},{"start":[673,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[673,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[657,840],"end":[648.5,811.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,783],"end":[640,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,709],"end":[656,671]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[673,633],"end":[701,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[729,577],"end":[767,560]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[805,544],"end":[848,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[869,544],"end":[889.5,548]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[910,552],"end":[928,560]}]}]},{"tag":"LineTo","args":[{"point":[928,448]}]},{"tag":"LineTo","args":[{"point":[0,448]}]},{"tag":"LineTo","args":[{"point":[0,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,827],"end":[19,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,864],"end":[64,864]}]}]},{"tag":"LineTo","args":[{"point":[673,864]}]}]},{"start":[928,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[928,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,325],"end":[909,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,288],"end":[864,288]}]}]},{"tag":"LineTo","args":[{"point":[480,288]}]},{"tag":"LineTo","args":[{"point":[416,160]}]},{"tag":"LineTo","args":[{"point":[64,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,160],"end":[19,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,197],"end":[0,224]}]}]},{"tag":"LineTo","args":[{"point":[0,416]}]},{"tag":"LineTo","args":[{"point":[928,416]}]}]},{"start":[848,928],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,928],"end":[723.5,876.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,825],"end":[672,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,679],"end":[723.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,576],"end":[848,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,576],"end":[972.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,679],"end":[1024,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,825],"end":[972.5,876.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,928],"end":[848,928]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-add-outline","codePoint":59683},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[736,736],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,768]}]},{"tag":"LineTo","args":[{"point":[832,768]}]},{"tag":"LineTo","args":[{"point":[832,864]}]},{"tag":"LineTo","args":[{"point":[864,864]}]},{"tag":"LineTo","args":[{"point":[864,768]}]},{"tag":"LineTo","args":[{"point":[960,768]}]},{"tag":"LineTo","args":[{"point":[960,736]}]},{"tag":"LineTo","args":[{"point":[864,736]}]},{"tag":"LineTo","args":[{"point":[864,640]}]},{"tag":"LineTo","args":[{"point":[832,640]}]},{"tag":"LineTo","args":[{"point":[832,736]}]},{"tag":"LineTo","args":[{"point":[736,736]}]}]},{"start":[64,832],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,832],"end":[41.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,813],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,448]}]},{"tag":"LineTo","args":[{"point":[896,448]}]},{"tag":"LineTo","args":[{"point":[896,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[885,579],"end":[872.5,577.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[860,576],"end":[848,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,576],"end":[723.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,679],"end":[672,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,774],"end":[677,794]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,814],"end":[691,832]}]}]},{"tag":"LineTo","args":[{"point":[691,832]}]},{"tag":"LineTo","args":[{"point":[64,832]}]}]},{"start":[712,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[712,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,893],"end":[771.5,910.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[807,928],"end":[848,928]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,928],"end":[972.5,876.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,825],"end":[1024,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,701],"end":[997.5,659]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[971,617],"end":[928,595]}]}]},{"tag":"LineTo","args":[{"point":[928,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,325],"end":[909,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,288],"end":[864,288]}]}]},{"tag":"LineTo","args":[{"point":[480,288]}]},{"tag":"LineTo","args":[{"point":[416,160]}]},{"tag":"LineTo","args":[{"point":[64,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,160],"end":[19,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,197],"end":[0,224]}]}]},{"tag":"LineTo","args":[{"point":[0,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,827],"end":[19,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[38,864],"end":[64,864]}]}]},{"tag":"LineTo","args":[{"point":[712,864]}]}]},{"start":[32,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[32,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,211],"end":[41.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,192],"end":[64,192]}]}]},{"tag":"LineTo","args":[{"point":[397,192]}]},{"tag":"LineTo","args":[{"point":[460,320]}]},{"tag":"LineTo","args":[{"point":[864,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[877,320],"end":[886.5,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,339],"end":[896,352]}]}]},{"tag":"LineTo","args":[{"point":[896,416]}]},{"tag":"LineTo","args":[{"point":[32,416]}]}]},{"start":[848,896],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,896],"end":[746,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,812],"end":[704,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,692],"end":[746,650]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,608],"end":[848,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,608],"end":[950,650]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[992,692],"end":[992,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[992,812],"end":[950,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,896],"end":[848,896]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-upload","codePoint":59684},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[480,864],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[480,576]}]},{"tag":"LineTo","args":[{"point":[376,680]}]},{"tag":"LineTo","args":[{"point":[352,656]}]},{"tag":"LineTo","args":[{"point":[496,512]}]},{"tag":"LineTo","args":[{"point":[640,656]}]},{"tag":"LineTo","args":[{"point":[616,680]}]},{"tag":"LineTo","args":[{"point":[512,576]}]},{"tag":"LineTo","args":[{"point":[512,864]}]},{"tag":"LineTo","args":[{"point":[896,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[960,800]}]}]},{"tag":"LineTo","args":[{"point":[960,448]}]},{"tag":"LineTo","args":[{"point":[32,448]}]},{"tag":"LineTo","args":[{"point":[32,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[96,864]}]}]},{"tag":"LineTo","args":[{"point":[480,864]}]}]},{"start":[960,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[960,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[896,288]}]}]},{"tag":"LineTo","args":[{"point":[512,288]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[96,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[32,224]}]}]},{"tag":"LineTo","args":[{"point":[32,416]}]},{"tag":"LineTo","args":[{"point":[960,416]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-upload-outline","codePoint":59685},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[480,544],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[376,648]}]},{"tag":"LineTo","args":[{"point":[352,624]}]},{"tag":"LineTo","args":[{"point":[496,480]}]},{"tag":"LineTo","args":[{"point":[640,624]}]},{"tag":"LineTo","args":[{"point":[616,648]}]},{"tag":"LineTo","args":[{"point":[512,544]}]},{"tag":"LineTo","args":[{"point":[512,832]}]},{"tag":"LineTo","args":[{"point":[896,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,832],"end":[918.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,813],"end":[928,800]}]}]},{"tag":"LineTo","args":[{"point":[928,448]}]},{"tag":"LineTo","args":[{"point":[64,448]}]},{"tag":"LineTo","args":[{"point":[64,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,813],"end":[73.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,832],"end":[96,832]}]}]},{"tag":"LineTo","args":[{"point":[480,832]}]},{"tag":"LineTo","args":[{"point":[480,544]}]}]},{"start":[928,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[928,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,339],"end":[918.5,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,320],"end":[896,320]}]}]},{"tag":"LineTo","args":[{"point":[492,320]}]},{"tag":"LineTo","args":[{"point":[429,192]}]},{"tag":"LineTo","args":[{"point":[96,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,192],"end":[73.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,211],"end":[64,224]}]}]},{"tag":"LineTo","args":[{"point":[64,416]}]},{"tag":"LineTo","args":[{"point":[928,416]}]}]},{"start":[512,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[960,352]}]}]},{"tag":"LineTo","args":[{"point":[960,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[896,864]}]}]},{"tag":"LineTo","args":[{"point":[96,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[96,160]}]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[512,288]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-download","codePoint":59686},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,448],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,738]}]},{"tag":"LineTo","args":[{"point":[616,634]}]},{"tag":"LineTo","args":[{"point":[640,658]}]},{"tag":"LineTo","args":[{"point":[496,802]}]},{"tag":"LineTo","args":[{"point":[352,658]}]},{"tag":"LineTo","args":[{"point":[376,634]}]},{"tag":"LineTo","args":[{"point":[480,738]}]},{"tag":"LineTo","args":[{"point":[480,448]}]},{"tag":"LineTo","args":[{"point":[32,448]}]},{"tag":"LineTo","args":[{"point":[32,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[96,864]}]}]},{"tag":"LineTo","args":[{"point":[896,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[960,800]}]}]},{"tag":"LineTo","args":[{"point":[960,448]}]},{"tag":"LineTo","args":[{"point":[512,448]}]}]},{"start":[960,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[960,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[896,288]}]}]},{"tag":"LineTo","args":[{"point":[512,288]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[96,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[32,224]}]}]},{"tag":"LineTo","args":[{"point":[32,416]}]},{"tag":"LineTo","args":[{"point":[960,416]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-download-outline","codePoint":59687},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,448],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,738]}]},{"tag":"LineTo","args":[{"point":[616,634]}]},{"tag":"LineTo","args":[{"point":[640,658]}]},{"tag":"LineTo","args":[{"point":[496,802]}]},{"tag":"LineTo","args":[{"point":[352,658]}]},{"tag":"LineTo","args":[{"point":[376,634]}]},{"tag":"LineTo","args":[{"point":[480,738]}]},{"tag":"LineTo","args":[{"point":[480,448]}]},{"tag":"LineTo","args":[{"point":[64,448]}]},{"tag":"LineTo","args":[{"point":[64,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,813],"end":[73.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,832],"end":[96,832]}]}]},{"tag":"LineTo","args":[{"point":[896,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,832],"end":[918.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,813],"end":[928,800]}]}]},{"tag":"LineTo","args":[{"point":[928,448]}]},{"tag":"LineTo","args":[{"point":[512,448]}]}]},{"start":[928,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[928,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,339],"end":[918.5,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,320],"end":[896,320]}]}]},{"tag":"LineTo","args":[{"point":[492,320]}]},{"tag":"LineTo","args":[{"point":[429,192]}]},{"tag":"LineTo","args":[{"point":[96,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,192],"end":[73.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,211],"end":[64,224]}]}]},{"tag":"LineTo","args":[{"point":[64,416]}]},{"tag":"LineTo","args":[{"point":[928,416]}]}]},{"start":[512,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[960,352]}]}]},{"tag":"LineTo","args":[{"point":[960,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[896,864]}]}]},{"tag":"LineTo","args":[{"point":[96,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[96,160]}]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[512,288]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-search","codePoint":59688},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[693,812],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[716,789]}]},{"tag":"LineTo","args":[{"point":[545,618]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[559,599],"end":[567.5,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,553],"end":[576,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,468],"end":[534,426]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[492,384],"end":[432,384]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,384],"end":[330,426]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,468],"end":[288,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,588],"end":[330,630]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,672],"end":[432,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[458,672],"end":[480.5,663.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[503,655],"end":[522,641]}]}]},{"tag":"LineTo","args":[{"point":[693,812]}]}]},{"start":[512,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[960,352]}]}]},{"tag":"LineTo","args":[{"point":[960,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[896,864]}]}]},{"tag":"LineTo","args":[{"point":[96,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[96,160]}]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[512,288]}]}]},{"start":[432,640],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[386,640],"end":[353,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,574],"end":[320,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,482],"end":[353,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[386,416],"end":[432,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,416],"end":[511,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,482],"end":[544,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,574],"end":[511,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,640],"end":[432,640]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-search-outline","codePoint":59689},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[693,812],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[716,789]}]},{"tag":"LineTo","args":[{"point":[545,618]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[559,599],"end":[567.5,576]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,553],"end":[576,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,468],"end":[534,426]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[492,384],"end":[432,384]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,384],"end":[330,426]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,468],"end":[288,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,588],"end":[330,630]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,672],"end":[432,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[458,672],"end":[480.5,663.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[503,655],"end":[522,641]}]}]},{"tag":"LineTo","args":[{"point":[693,812]}]}]},{"start":[448,160],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[96,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[32,224]}]}]},{"tag":"LineTo","args":[{"point":[32,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[96,864]}]}]},{"tag":"LineTo","args":[{"point":[896,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[960,800]}]}]},{"tag":"LineTo","args":[{"point":[960,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[896,288]}]}]},{"tag":"LineTo","args":[{"point":[512,288]}]},{"tag":"LineTo","args":[{"point":[448,160]}]}]},{"start":[492,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,320],"end":[918.5,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,339],"end":[928,352]}]}]},{"tag":"LineTo","args":[{"point":[928,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,813],"end":[918.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,832],"end":[896,832]}]}]},{"tag":"LineTo","args":[{"point":[96,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,832],"end":[73.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,813],"end":[64,800]}]}]},{"tag":"LineTo","args":[{"point":[64,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,211],"end":[73.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,192],"end":[96,192]}]}]},{"tag":"LineTo","args":[{"point":[429,192]}]},{"tag":"LineTo","args":[{"point":[492,320]}]}]},{"start":[432,640],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[386,640],"end":[353,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,574],"end":[320,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,482],"end":[353,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[386,416],"end":[432,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,416],"end":[511,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,482],"end":[544,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,574],"end":[511,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,640],"end":[432,640]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder","codePoint":59690},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[960,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[960,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[896,288]}]}]},{"tag":"LineTo","args":[{"point":[512,288]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[96,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[32,224]}]}]},{"tag":"LineTo","args":[{"point":[32,416]}]},{"tag":"LineTo","args":[{"point":[960,416]}]}]},{"start":[32,448],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[960,448]}]},{"tag":"LineTo","args":[{"point":[960,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[896,864]}]}]},{"tag":"LineTo","args":[{"point":[96,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,448]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder-outline","codePoint":59691},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[928,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[928,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,339],"end":[918.5,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,320],"end":[896,320]}]}]},{"tag":"LineTo","args":[{"point":[492,320]}]},{"tag":"LineTo","args":[{"point":[429,192]}]},{"tag":"LineTo","args":[{"point":[96,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,192],"end":[73.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,211],"end":[64,224]}]}]},{"tag":"LineTo","args":[{"point":[64,416]}]},{"tag":"LineTo","args":[{"point":[928,416]}]}]},{"start":[64,448],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[64,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,813],"end":[73.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,832],"end":[96,832]}]}]},{"tag":"LineTo","args":[{"point":[896,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,832],"end":[918.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,813],"end":[928,800]}]}]},{"tag":"LineTo","args":[{"point":[928,448]}]},{"tag":"LineTo","args":[{"point":[64,448]}]}]},{"start":[512,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[960,352]}]}]},{"tag":"LineTo","args":[{"point":[960,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[896,864]}]}]},{"tag":"LineTo","args":[{"point":[96,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[96,160]}]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[512,288]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder2","codePoint":59692},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,288],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[960,352]}]}]},{"tag":"LineTo","args":[{"point":[960,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[896,864]}]}]},{"tag":"LineTo","args":[{"point":[96,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[32,800]}]}]},{"tag":"LineTo","args":[{"point":[32,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[96,160]}]}]},{"tag":"LineTo","args":[{"point":[448,160]}]},{"tag":"LineTo","args":[{"point":[512,288]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder2-outline","codePoint":59693},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[448,160],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[96,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,160],"end":[51,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,197],"end":[32,224]}]}]},{"tag":"LineTo","args":[{"point":[32,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,827],"end":[51,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[70,864],"end":[96,864]}]}]},{"tag":"LineTo","args":[{"point":[896,864]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,864],"end":[941.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,827],"end":[960,800]}]}]},{"tag":"LineTo","args":[{"point":[960,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[960,325],"end":[941,306.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,288],"end":[896,288]}]}]},{"tag":"LineTo","args":[{"point":[512,288]}]},{"tag":"LineTo","args":[{"point":[448,160]}]}]},{"start":[492,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,320],"end":[918.5,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,339],"end":[928,352]}]}]},{"tag":"LineTo","args":[{"point":[928,800]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[928,813],"end":[918.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[909,832],"end":[896,832]}]}]},{"tag":"LineTo","args":[{"point":[96,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,832],"end":[73.5,822.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,813],"end":[64,800]}]}]},{"tag":"LineTo","args":[{"point":[64,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,211],"end":[73.5,201.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[83,192],"end":[96,192]}]}]},{"tag":"LineTo","args":[{"point":[429,192]}]},{"tag":"LineTo","args":[{"point":[492,320]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"android","codePoint":59694},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[945,393],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[945,393]}]},{"tag":"LineTo","args":[{"point":[945,658]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[945,684],"end":[926.5,702.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,721],"end":[882,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[856,721],"end":[837.5,702.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[819,684],"end":[819,658]}]}]},{"tag":"LineTo","args":[{"point":[819,394]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[819,368],"end":[837.5,349.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[856,331],"end":[882,331]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,331],"end":[926.5,349.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[945,368],"end":[945,394]}]}]},{"tag":"LineTo","args":[{"point":[945,393]}]}]},{"start":[230,343],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[793,342]}]},{"tag":"LineTo","args":[{"point":[793,753]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[793,781],"end":[773,800.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[753,820],"end":[726,820]}]}]},{"tag":"LineTo","args":[{"point":[680,820]}]},{"tag":"LineTo","args":[{"point":[680,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,987],"end":[661.5,1005.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[643,1024],"end":[616,1024]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[590,1024],"end":[572,1005.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,987],"end":[554,961]}]}]},{"tag":"LineTo","args":[{"point":[554,821]}]},{"tag":"LineTo","args":[{"point":[469,821]}]},{"tag":"LineTo","args":[{"point":[469,961]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[468,987],"end":[449.5,1005]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[431,1023],"end":[405,1023]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,1023],"end":[361.5,1004.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[343,986],"end":[343,960]}]}]},{"tag":"LineTo","args":[{"point":[343,820]}]},{"tag":"LineTo","args":[{"point":[297,820]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[270,820],"end":[250,800.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[230,781],"end":[230,752]}]}]},{"tag":"LineTo","args":[{"point":[230,343]}]}]},{"start":[659,211],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,204],"end":[666,194]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,184],"end":[659,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,170],"end":[642,170]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[632,170],"end":[625,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,184],"end":[618,194]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,204],"end":[625,211]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[632,218],"end":[642,218]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,218],"end":[659,211]}]}]}]},{"start":[383,218],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[393,218],"end":[400,211]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[407,204],"end":[407,194]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[407,184],"end":[400,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[393,170],"end":[383,170]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[373,170],"end":[366,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[359,184],"end":[359,194]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[359,204],"end":[366,211]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[373,218],"end":[383,218]}]}]}]},{"start":[651,93],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[716,126],"end":[756,186.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[796,247],"end":[796,320]}]}]},{"tag":"LineTo","args":[{"point":[228,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[228,247],"end":[268,186.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,126],"end":[373,93]}]}]},{"tag":"LineTo","args":[{"point":[329,13]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[327,10],"end":[328,6.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[329,3],"end":[332,1]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[335,-1],"end":[338.5,0]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,1],"end":[344,4]}]}]},{"tag":"LineTo","args":[{"point":[388,86]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[416,73],"end":[447,66.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,60],"end":[512,60]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[545,60],"end":[576,67]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[607,74],"end":[635,87]}]}]},{"tag":"LineTo","args":[{"point":[680,6]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,2],"end":[685,1]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,0],"end":[692,2]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[695,4],"end":[696,7.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[697,11],"end":[695,14]}]}]},{"tag":"LineTo","args":[{"point":[651,95]}]},{"tag":"LineTo","args":[{"point":[651,93]}]}]},{"start":[143,331],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[168,331],"end":[186.5,349.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[205,368],"end":[205,394]}]}]},{"tag":"LineTo","args":[{"point":[205,658]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[205,684],"end":[186.5,702.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[168,721],"end":[142,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[116,721],"end":[97.5,702.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[79,684],"end":[79,658]}]}]},{"tag":"LineTo","args":[{"point":[79,394]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[79,367],"end":[97.5,348.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[116,330],"end":[142,330]}]}]},{"tag":"LineTo","args":[{"point":[143,331]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"angular","codePoint":59695},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,330],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[600,540]}]},{"tag":"LineTo","args":[{"point":[424,540]}]},{"tag":"LineTo","args":[{"point":[512,330]}]}]},{"start":[512,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[994,170]}]},{"tag":"LineTo","args":[{"point":[921,800]}]},{"tag":"LineTo","args":[{"point":[512,1024]}]},{"tag":"LineTo","args":[{"point":[103,800]}]},{"tag":"LineTo","args":[{"point":[29,170]}]},{"tag":"LineTo","args":[{"point":[512,0]}]}]},{"start":[512,113],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,781]}]},{"tag":"LineTo","args":[{"point":[323,781]}]},{"tag":"LineTo","args":[{"point":[384,632]}]},{"tag":"LineTo","args":[{"point":[640,632]}]},{"tag":"LineTo","args":[{"point":[701,781]}]},{"tag":"LineTo","args":[{"point":[813,781]}]},{"tag":"LineTo","args":[{"point":[512,113]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"css3","codePoint":59696},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[64,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[146,920]}]},{"tag":"LineTo","args":[{"point":[511,1024]}]},{"tag":"LineTo","args":[{"point":[879,920]}]},{"tag":"LineTo","args":[{"point":[960,0]}]},{"tag":"LineTo","args":[{"point":[64,0]}]}]},{"start":[793,188],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[741,758]}]},{"tag":"LineTo","args":[{"point":[512,823]}]},{"tag":"LineTo","args":[{"point":[279,750]}]},{"tag":"LineTo","args":[{"point":[266,586]}]},{"tag":"LineTo","args":[{"point":[378,586]}]},{"tag":"LineTo","args":[{"point":[386,676]}]},{"tag":"LineTo","args":[{"point":[512,710]}]},{"tag":"LineTo","args":[{"point":[636,676]}]},{"tag":"LineTo","args":[{"point":[652,526]}]},{"tag":"LineTo","args":[{"point":[388,526]}]},{"tag":"LineTo","args":[{"point":[378,416]}]},{"tag":"LineTo","args":[{"point":[661,416]}]},{"tag":"LineTo","args":[{"point":[672,300]}]},{"tag":"LineTo","args":[{"point":[240,300]}]},{"tag":"LineTo","args":[{"point":[231,188]}]},{"tag":"LineTo","args":[{"point":[793,188]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"dev-dot-to","codePoint":59697},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[317,429],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[325,436],"end":[327,448.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[329,461],"end":[329,522]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[329,581],"end":[327.5,594.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,608],"end":[318,616]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[311,622],"end":[303.5,624.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,627],"end":[282,627]}]}]},{"tag":"LineTo","args":[{"point":[259,628]}]},{"tag":"LineTo","args":[{"point":[257,523]}]},{"tag":"LineTo","args":[{"point":[256,419]}]},{"tag":"LineTo","args":[{"point":[281,419]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[293,419],"end":[302,421.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[311,424],"end":[317,429]}]}]}]},{"start":[1024,211],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[1024,813]}]},{"tag":"LineTo","args":[{"point":[0,813]}]},{"tag":"LineTo","args":[{"point":[0,211]}]},{"tag":"LineTo","args":[{"point":[1024,211]}]}]},{"start":[365,653],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,635],"end":[383.5,609]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[387,583],"end":[385,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,449],"end":[382,432.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,416],"end":[372,402]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,379],"end":[336,371.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[314,364],"end":[261,364]}]}]},{"tag":"LineTo","args":[{"point":[201,364]}]},{"tag":"LineTo","args":[{"point":[201,686]}]},{"tag":"LineTo","args":[{"point":[257,686]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[304,686],"end":[327.5,678.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[351,671],"end":[365,653]}]}]}]},{"start":[582,420],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[582,364]}]},{"tag":"LineTo","args":[{"point":[512,364]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,364],"end":[450,366]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[440,368],"end":[432,378]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[425,387],"end":[423.5,408]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,429],"end":[422,526]}]}]},{"tag":"LineTo","args":[{"point":[422,661]}]},{"tag":"LineTo","args":[{"point":[434,673]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[443,682],"end":[454,684]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[465,686],"end":[514,686]}]}]},{"tag":"LineTo","args":[{"point":[582,686]}]},{"tag":"LineTo","args":[{"point":[582,631]}]},{"tag":"LineTo","args":[{"point":[531,629]}]},{"tag":"LineTo","args":[{"point":[479,628]}]},{"tag":"LineTo","args":[{"point":[479,553]}]},{"tag":"LineTo","args":[{"point":[511,551]}]},{"tag":"LineTo","args":[{"point":[542,550]}]},{"tag":"LineTo","args":[{"point":[542,495]}]},{"tag":"LineTo","args":[{"point":[477,495]}]},{"tag":"LineTo","args":[{"point":[477,419]}]},{"tag":"LineTo","args":[{"point":[582,419]}]},{"tag":"LineTo","args":[{"point":[582,420]}]}]},{"start":[782,652],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[784,647],"end":[796,603]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[808,559],"end":[822,507]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[836,455],"end":[847,412]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,369],"end":[858,367]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,366],"end":[849,365.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[840,365],"end":[827,365]}]}]},{"tag":"LineTo","args":[{"point":[796,367]}]},{"tag":"LineTo","args":[{"point":[768,474]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[754,526],"end":[746,551.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[738,577],"end":[736,573]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[735,568],"end":[726,536]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[718,503],"end":[708,466]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,429],"end":[690,398]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,367],"end":[683,367]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,366],"end":[673,365]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[663,364],"end":[651,364]}]}]},{"tag":"LineTo","args":[{"point":[618,364]}]},{"tag":"LineTo","args":[{"point":[637,436]}]},{"tag":"LineTo","args":[{"point":[676,580]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,627],"end":[694.5,643]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,659],"end":[711,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[718,677],"end":[726,681.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,686],"end":[739,686]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[751,686],"end":[764,676]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[777,666],"end":[782,652]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"facebook","codePoint":59698},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1024,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,608],"end":[991,693]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[958,779],"end":[900,846.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[842,914],"end":[763,958]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[684,1003],"end":[592,1018]}]}]},{"tag":"LineTo","args":[{"point":[592,660]}]},{"tag":"LineTo","args":[{"point":[711,660]}]},{"tag":"LineTo","args":[{"point":[734,512]}]},{"tag":"LineTo","args":[{"point":[592,512]}]},{"tag":"LineTo","args":[{"point":[592,416]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,386],"end":[610,361]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,336],"end":[675,336]}]}]},{"tag":"LineTo","args":[{"point":[740,336]}]},{"tag":"LineTo","args":[{"point":[740,210]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[740,210],"end":[703.5,205]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[667,200],"end":[625,200]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[538,200],"end":[485,251.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,303],"end":[432,399]}]}]},{"tag":"LineTo","args":[{"point":[432,512]}]},{"tag":"LineTo","args":[{"point":[302,512]}]},{"tag":"LineTo","args":[{"point":[302,660]}]},{"tag":"LineTo","args":[{"point":[432,660]}]},{"tag":"LineTo","args":[{"point":[432,1018]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[340,1003],"end":[261,958]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[182,914],"end":[124,846.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[66,779],"end":[33,693]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,608],"end":[0,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,406],"end":[40,313]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[80,219],"end":[149.5,149.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[219,80],"end":[313,40]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,0],"end":[512,0]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,0],"end":[711,40]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[804,80],"end":[873.5,149.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[943,219],"end":[984,313]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,406],"end":[1024,512]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"git","codePoint":59699},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1005,466],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,486],"end":[1024,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,540],"end":[1005,560]}]}]},{"tag":"LineTo","args":[{"point":[560,1005]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[540,1024],"end":[513,1024]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[486,1024],"end":[466,1005]}]}]},{"tag":"LineTo","args":[{"point":[19,558]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,538],"end":[0,511]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,484],"end":[19,464]}]}]},{"tag":"LineTo","args":[{"point":[326,158]}]},{"tag":"LineTo","args":[{"point":[442,274]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[433,295],"end":[437,318.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[441,342],"end":[458,360]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,366],"end":[470.5,370]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[477,374],"end":[484,377]}]}]},{"tag":"LineTo","args":[{"point":[484,658]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[477,661],"end":[470.5,665.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,670],"end":[458,675]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[435,698],"end":[435,730.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[435,763],"end":[458,786]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[481,809],"end":[514,809]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[547,809],"end":[570,786]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,763],"end":[592,730.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,698],"end":[570,675]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[565,671],"end":[559.5,667]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,663],"end":[549,660]}]}]},{"tag":"LineTo","args":[{"point":[549,382]}]},{"tag":"LineTo","args":[{"point":[655,488]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,509],"end":[650.5,532.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[655,556],"end":[672,573]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[695,596],"end":[727.5,596]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[760,596],"end":[783,573]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,550],"end":[806,517.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,485],"end":[783,462]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[767,445],"end":[745,440.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,436],"end":[702,443]}]}]},{"tag":"LineTo","args":[{"point":[589,330]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[596,309],"end":[591,287]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[586,265],"end":[570,249]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,232],"end":[532,227.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,223],"end":[489,230]}]}]},{"tag":"LineTo","args":[{"point":[372,112]}]},{"tag":"LineTo","args":[{"point":[464,19]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[484,0],"end":[511,0]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[538,0],"end":[558,19]}]}]},{"tag":"LineTo","args":[{"point":[1005,466]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"github","codePoint":59700},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,13],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,13],"end":[711,53]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[805,93],"end":[874.5,162.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[944,232],"end":[984,325]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,419],"end":[1024,525]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,609],"end":[998,686]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[972,763],"end":[925,826.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[878,890],"end":[814,937]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[749,985],"end":[673,1010]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[653,1014],"end":[645,1005]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[637,996],"end":[637,985]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[637,973],"end":[637.5,935]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[638,897],"end":[638,845]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[638,809],"end":[627.5,785.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,762],"end":[603,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,746],"end":[688,733]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,721],"end":[763,693.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[796,666],"end":[817,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[837,572],"end":[837,498]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[837,456],"end":[823,421.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[809,387],"end":[785,361]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,351],"end":[794,314.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,278],"end":[779,225]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[779,225],"end":[745.5,226.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[712,228],"end":[639,278]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[609,269],"end":[576.5,264.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,260],"end":[511,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[479,260],"end":[446.5,264.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[414,269],"end":[383,278]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[310,228],"end":[276.5,226.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[243,225],"end":[243,225]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,278],"end":[228,314.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[234,351],"end":[238,361]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[213,387],"end":[199,421.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[185,456],"end":[185,498]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[185,571],"end":[206,618]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[226,665],"end":[259,693]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[292,721],"end":[334,734]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[376,746],"end":[419,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,761],"end":[399,778]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[390,795],"end":[386,819]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[364,829],"end":[317.5,832.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[271,836],"end":[237,777]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[237,777],"end":[217,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,727],"end":[159,724]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[159,724],"end":[139.5,727.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[120,731],"end":[155,755]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[155,755],"end":[174.5,770.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[194,786],"end":[212,830]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[212,830],"end":[245,876]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[278,922],"end":[384,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[385,931],"end":[385,954]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[385,977],"end":[385,986]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[385,996],"end":[377,1005]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[369,1014],"end":[350,1010]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[274,985],"end":[210,938]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[145,891],"end":[98.5,827.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[52,764],"end":[26,687]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,610],"end":[0,525]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,419],"end":[40,325]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[80,232],"end":[149.5,162.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[219,93],"end":[313,53]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,13],"end":[512,13]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"gmail","codePoint":59701},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1024,192],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,178],"end":[1019,166.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1014,155],"end":[1006,146]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[997,138],"end":[985.5,133]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[974,128],"end":[960,128]}]}]},{"tag":"LineTo","args":[{"point":[939,128]}]},{"tag":"LineTo","args":[{"point":[512,437]}]},{"tag":"LineTo","args":[{"point":[85,128]}]},{"tag":"LineTo","args":[{"point":[64,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[50,128],"end":[38.5,133]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[27,138],"end":[18,146]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[10,155],"end":[5,166.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,178],"end":[0,192]}]}]},{"tag":"LineTo","args":[{"point":[0,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,859],"end":[18.5,877.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[37,896],"end":[64,896]}]}]},{"tag":"LineTo","args":[{"point":[128,896]}]},{"tag":"LineTo","args":[{"point":[128,315]}]},{"tag":"LineTo","args":[{"point":[512,591]}]},{"tag":"LineTo","args":[{"point":[896,315]}]},{"tag":"LineTo","args":[{"point":[896,896]}]},{"tag":"LineTo","args":[{"point":[960,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[987,896],"end":[1005.5,877.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,859],"end":[1024,832]}]}]},{"tag":"LineTo","args":[{"point":[1024,192]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"googlechrome","codePoint":59702},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[692,371],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[691,370]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[699,379],"end":[705,389]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[711,399],"end":[717,410]}]}]},{"tag":"LineTo","args":[{"point":[716,408]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[728,432],"end":[734.5,458.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[741,485],"end":[741,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[741,540],"end":[735,565]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[729,590],"end":[719,612]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[712,627],"end":[702.5,640.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[693,654],"end":[681,666]}]}]},{"tag":"LineTo","args":[{"point":[472,1023]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[482,1023],"end":[491.5,1023.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[501,1024],"end":[511,1024]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[616,1024],"end":[710,983]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[803,943],"end":[873,873]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[943,803],"end":[983,710]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,616],"end":[1024,511]}]}]},{"tag":"LineTo","args":[{"point":[1024,511]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,453],"end":[1012,399.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1000,346],"end":[978,299]}]}]},{"tag":"LineTo","args":[{"point":[692,371]}]}]},{"start":[554,737],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,743],"end":[489,740]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[456,737],"end":[424,725]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,701],"end":[330.5,650.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,600],"end":[293,583]}]}]},{"tag":"LineTo","args":[{"point":[87,225]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[44,289],"end":[22,362]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,435],"end":[0,511]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,610],"end":[36,698]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[71,787],"end":[133.5,855.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[196,924],"end":[281,968]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[365,1012],"end":[463,1022]}]}]},{"tag":"LineTo","args":[{"point":[554,737]}]}]},{"start":[511,325],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[511,325]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[511,325],"end":[511.5,325]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,325],"end":[512,325]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[534,325],"end":[554,329.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,334],"end":[591,343]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,368],"end":[676,424.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,481],"end":[698,540]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[691,588],"end":[659.5,628]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[628,668],"end":[583,687]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[532,707],"end":[476,696]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,685],"end":[381,647]}]}]},{"tag":"LineTo","args":[{"point":[381,647]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[355,621],"end":[340,586]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[325,551],"end":[325,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[325,502],"end":[326,492.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[327,483],"end":[328,475]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[341,412],"end":[394,369]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[447,326],"end":[511,325]}]}]}]},{"start":[301,425],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[301,424]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[310,402],"end":[323.5,382.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[337,363],"end":[354,347]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[385,317],"end":[431.5,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,281],"end":[536,285]}]}]},{"tag":"LineTo","args":[{"point":[972,285]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[939,219],"end":[891,167]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[842,114],"end":[782,77]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[722,40],"end":[654,20]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[585,0],"end":[512,0]}]}]},{"tag":"LineTo","args":[{"point":[512,0]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,0],"end":[388,15]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[329,30],"end":[276,57.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[223,85],"end":[177,124]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[132,163],"end":[96,212]}]}]},{"tag":"LineTo","args":[{"point":[301,425]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"googledrive","codePoint":59703},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[189,956],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[853,956]}]},{"tag":"LineTo","args":[{"point":[1024,660]}]},{"tag":"LineTo","args":[{"point":[360,660]}]},{"tag":"LineTo","args":[{"point":[189,956]}]}]},{"start":[341,660],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[503,380]}]},{"tag":"LineTo","args":[{"point":[332,84]}]},{"tag":"LineTo","args":[{"point":[0,660]}]},{"tag":"LineTo","args":[{"point":[171,956]}]},{"tag":"LineTo","args":[{"point":[341,660]}]}]},{"start":[1015,644],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[683,68]}]},{"tag":"LineTo","args":[{"point":[341,68]}]},{"tag":"LineTo","args":[{"point":[673,644]}]},{"tag":"LineTo","args":[{"point":[1015,644]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"googleplay","codePoint":59704},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[544,508],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[57,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[53,1020],"end":[51,1015.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[49,1011],"end":[49,1004]}]}]},{"tag":"LineTo","args":[{"point":[49,13]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[49,9],"end":[50,6]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,3],"end":[52,0]}]}]},{"tag":"LineTo","args":[{"point":[544,508]}]}]},{"start":[577,542],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[169,977]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[216,950],"end":[277,915]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[339,880],"end":[393.5,848.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,817],"end":[486,796]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[523,775],"end":[523,775]}]}]},{"tag":"LineTo","args":[{"point":[703,672]}]},{"tag":"LineTo","args":[{"point":[577,542]}]}]},{"start":[611,507],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[748,362]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,368],"end":[788,386]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[820,404],"end":[855,424.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,445],"end":[920,462]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[950,479],"end":[958,483]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[974,491],"end":[974.5,504]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[975,517],"end":[957,528]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[948,533],"end":[917,550]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[886,568],"end":[850.5,588]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[815,608],"end":[785,625]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[754,643],"end":[746,647]}]}]},{"tag":"LineTo","args":[{"point":[611,507]}]}]},{"start":[577,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[128,8]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[165,29],"end":[231,67]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,105],"end":[362.5,142]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[427,179],"end":[475,206]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[523,234],"end":[523,234]}]}]},{"tag":"LineTo","args":[{"point":[704,337]}]},{"tag":"LineTo","args":[{"point":[577,473]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"html5","codePoint":59705},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[64,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[146,920]}]},{"tag":"LineTo","args":[{"point":[511,1024]}]},{"tag":"LineTo","args":[{"point":[879,920]}]},{"tag":"LineTo","args":[{"point":[960,0]}]},{"tag":"LineTo","args":[{"point":[64,0]}]}]},{"start":[364,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[773,416]}]},{"tag":"LineTo","args":[{"point":[742,764]}]},{"tag":"LineTo","args":[{"point":[512,826]}]},{"tag":"LineTo","args":[{"point":[281,764]}]},{"tag":"LineTo","args":[{"point":[267,586]}]},{"tag":"LineTo","args":[{"point":[378,586]}]},{"tag":"LineTo","args":[{"point":[386,676]}]},{"tag":"LineTo","args":[{"point":[512,710]}]},{"tag":"LineTo","args":[{"point":[636,676]}]},{"tag":"LineTo","args":[{"point":[650,530]}]},{"tag":"LineTo","args":[{"point":[261,530]}]},{"tag":"LineTo","args":[{"point":[231,188]}]},{"tag":"LineTo","args":[{"point":[793,188]}]},{"tag":"LineTo","args":[{"point":[783,300]}]},{"tag":"LineTo","args":[{"point":[354,300]}]},{"tag":"LineTo","args":[{"point":[364,416]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"instagram","codePoint":59706},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[616,0],"end":[649,0.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,1],"end":[723,3]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[764,5],"end":[793.5,11]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[823,17],"end":[847,27]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[873,37],"end":[895,51]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[917,65],"end":[938,86]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[959,107],"end":[973,129]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[987,151],"end":[997,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1007,201],"end":[1013,230.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1019,260],"end":[1021,301]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1023,342],"end":[1023.5,375]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,408],"end":[1024,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,616],"end":[1023.5,649]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1023,682],"end":[1021,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1019,764],"end":[1013,793.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1007,823],"end":[997,847]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[987,873],"end":[973,895]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[959,917],"end":[938,938]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[917,959],"end":[895,973]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[873,987],"end":[847,997]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[823,1007],"end":[793.5,1013]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[764,1019],"end":[723,1021]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,1023],"end":[649,1023.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[616,1024],"end":[512,1024]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,1024],"end":[375,1023.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,1023],"end":[301,1021]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,1019],"end":[230.5,1013]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[201,1007],"end":[177,997]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[151,987],"end":[129,973]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[107,959],"end":[86,938]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[65,917],"end":[51,895]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[37,873],"end":[27,847]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[17,823],"end":[11,793.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[5,764],"end":[3,723]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1,682],"end":[0.5,649]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,616],"end":[0,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,408],"end":[0.5,375]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1,342],"end":[3,301]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[5,260],"end":[11,230.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[17,201],"end":[27,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[37,151],"end":[51,129]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[65,107],"end":[86,86]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[107,65],"end":[129,51]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[151,37],"end":[177,27]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[201,17],"end":[230.5,11]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,5],"end":[301,3]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,1],"end":[375,0.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,0],"end":[512,0]}]}]}]},{"start":[512,92],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[510,91]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[407,91],"end":[375.5,91.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[344,92],"end":[303,93]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[265,95],"end":[243,100.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[221,106],"end":[208,111]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[190,118],"end":[176.5,127]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[163,136],"end":[149,150]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,163],"end":[126.5,176.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[117,190],"end":[111,209]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[105,222],"end":[100,244.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[95,267],"end":[93,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[91,345],"end":[90.5,377]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[90,409],"end":[90,511]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[90,614],"end":[90.5,646]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[91,678],"end":[93,718]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[95,755],"end":[100,777.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[105,800],"end":[111,813]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[117,831],"end":[126.5,845]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,859],"end":[149,872]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[163,886],"end":[176.5,894.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[190,903],"end":[208,911]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[221,916],"end":[243.5,921.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[266,927],"end":[303,929]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[344,930],"end":[376,931]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,932],"end":[511,932]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[613,932],"end":[645,931.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,931],"end":[718,929]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[755,927],"end":[777.5,921.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,916],"end":[813,911]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[831,904],"end":[845,895]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[859,886],"end":[872,873]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[886,859],"end":[894.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[903,832],"end":[911,814]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[916,800],"end":[921.5,778]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[927,756],"end":[929,719]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,678],"end":[931,646.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[932,615],"end":[932,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[932,409],"end":[931.5,377.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[931,346],"end":[929,305]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[927,268],"end":[921.5,246]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[916,224],"end":[911,210]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[904,192],"end":[895,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[886,165],"end":[873,151]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[859,138],"end":[845.5,129]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,120],"end":[814,113]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,108],"end":[778,102.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[756,97],"end":[719,95]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[678,93],"end":[646,92.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,92],"end":[512,92]}]}]}]},{"start":[614,270],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[614,270]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[662,290],"end":[698,326]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,362],"end":[754,410]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,458],"end":[775,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[775,566],"end":[754,614]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,662],"end":[698,698]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[662,734],"end":[614,754]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[566,775],"end":[512,775]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[458,775],"end":[410,754]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,734],"end":[326,698]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,662],"end":[270,614]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[249,566],"end":[249,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[249,458],"end":[270,410]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,362],"end":[326,326]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,290],"end":[410,270]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[458,249],"end":[512,249]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[566,249],"end":[614,270]}]}]}]},{"start":[633,633],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,583],"end":[683,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,441],"end":[633,391]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[583,341],"end":[512,341]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[441,341],"end":[391,391]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[341,441],"end":[341,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[341,583],"end":[391,633]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[441,683],"end":[512,683]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[583,683],"end":[633,633]}]}]}]},{"start":[847,239],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[847,213],"end":[829,195]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,177],"end":[785,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[760,177],"end":[742,195]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[724,213],"end":[724,239]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[724,264],"end":[742,282]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[760,300],"end":[785,300]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[811,300],"end":[829,282]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[847,264],"end":[847,239]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"ionic","codePoint":59707},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[978,300],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1001,350],"end":[1012.5,403.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,457],"end":[1024,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,618],"end":[984,711]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[943,804],"end":[873.5,873.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[804,943],"end":[711,984]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,1024],"end":[512,1024]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,1024],"end":[313,984]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[220,943],"end":[150.5,873.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[81,804],"end":[40,711]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,618],"end":[0,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,406],"end":[40,313]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[81,220],"end":[150.5,150.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[220,81],"end":[313,40]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,0],"end":[512,0]}]}]},{"tag":"LineTo","args":[{"point":[512,0]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,0],"end":[512.5,0]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[513,0],"end":[513,0]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[577,0],"end":[636.5,15]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[696,30],"end":[747,57]}]}]},{"tag":"LineTo","args":[{"point":[756,62]}]},{"tag":"LineTo","args":[{"point":[748,69]}]},{"tag":"LineTo","args":[{"point":[748,69]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[733,81],"end":[721,96.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,112],"end":[701,131]}]}]},{"tag":"LineTo","args":[{"point":[698,137]}]},{"tag":"LineTo","args":[{"point":[691,134]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[649,114],"end":[604,104]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[559,94],"end":[512,94]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,94],"end":[349,127]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[273,159],"end":[216.5,216]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[160,273],"end":[127,349]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,426],"end":[94,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,598],"end":[127,675]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[159,751],"end":[216,807.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[273,864],"end":[349,897]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[425,930],"end":[512,930]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,930],"end":[675,897]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[751,864],"end":[807.5,807.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,751],"end":[897,674]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,598],"end":[930,512]}]}]},{"tag":"LineTo","args":[{"point":[930,512]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,512],"end":[930,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,512],"end":[930,511]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,468],"end":[921.5,427.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[913,387],"end":[898,351]}]}]},{"tag":"LineTo","args":[{"point":[895,345]}]},{"tag":"LineTo","args":[{"point":[902,342]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[921,335],"end":[937,324]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[953,313],"end":[966,298]}]}]},{"tag":"LineTo","args":[{"point":[974,290]}]},{"tag":"LineTo","args":[{"point":[978,300]}]}]},{"start":[512,279],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,279]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[560,279],"end":[603,297]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,315],"end":[677,347]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,379],"end":[727,421]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[745,464],"end":[745,512]}]}]},{"tag":"LineTo","args":[{"point":[745,512]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[745,560],"end":[727,603]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[709,645],"end":[677,677]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,709],"end":[603,727]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[560,745],"end":[512,745]}]}]},{"tag":"LineTo","args":[{"point":[512,745]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,745],"end":[421,727]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[379,709],"end":[347,677]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[315,645],"end":[297,603]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[279,560],"end":[279,512]}]}]},{"tag":"LineTo","args":[{"point":[279,512]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[279,464],"end":[297,421]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[315,379],"end":[347,347]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[379,315],"end":[421,297]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,279],"end":[512,279]}]}]}]},{"start":[953,193],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[953,149],"end":[922,118]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[891,87],"end":[847,87]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[803,87],"end":[772,118]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[741,149],"end":[741,193]}]}]},{"tag":"LineTo","args":[{"point":[741,193]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[741,237],"end":[772,268]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[803,299],"end":[847,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[891,299],"end":[922,268]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[953,237],"end":[953,193]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"javascript","codePoint":59708},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[0,1024],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[1024,1024]}]},{"tag":"LineTo","args":[{"point":[1024,0]}]},{"tag":"LineTo","args":[{"point":[0,0]}]},{"tag":"LineTo","args":[{"point":[0,1024]}]}]},{"start":[940,780],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[938,777]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[943,807],"end":[939.5,825.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[936,844],"end":[935,847]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[923,890],"end":[886,913]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[849,935],"end":[804,939.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[759,944],"end":[714,931]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[670,918],"end":[642,890]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[630,877],"end":[622,867.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,858],"end":[607,843]}]}]},{"tag":"LineTo","args":[{"point":[685,798]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,822],"end":[717.5,835.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,849],"end":[760,855]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,859],"end":[823.5,845.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[852,832],"end":[844,795]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[836,764],"end":[785,748.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,733],"end":[690,701]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,671],"end":[637.5,611]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,551],"end":[666,510]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[678,495],"end":[697.5,483.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,472],"end":[739,467]}]}]},{"tag":"LineTo","args":[{"point":[769,463]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[812,462],"end":[840.5,473.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[869,485],"end":[890,507]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[895,513],"end":[900.5,520]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[906,527],"end":[915,541]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[891,555],"end":[882,561]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[873,567],"end":[840,589]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,573],"end":[821,563.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,554],"end":[798,550]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[779,545],"end":[758.5,551.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[738,558],"end":[733,578]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[731,585],"end":[731.5,591.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,598],"end":[735,609]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[743,626],"end":[765.5,636]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,646],"end":[812,657]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[880,685],"end":[907.5,715]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[935,745],"end":[940,780]}]}]}]},{"start":[557,471],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[557,473]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,539],"end":[557,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,671],"end":[557,737]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,778],"end":[556.5,814]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,850],"end":[540,879]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[528,902],"end":[508,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[488,932],"end":[462,940]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[424,948],"end":[388.5,943.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[353,939],"end":[326,922]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[307,911],"end":[293,894.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[279,878],"end":[269,858]}]}]},{"tag":"LineTo","args":[{"point":[347,810]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[348,810],"end":[349,812.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[350,815],"end":[352,818]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[360,830],"end":[367.5,839]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[375,848],"end":[388,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[400,858],"end":[422,857]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[444,856],"end":[455,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[461,823],"end":[461,790.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[461,758],"end":[461,718]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[461,656],"end":[461,594.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[461,533],"end":[461,471]}]}]},{"tag":"LineTo","args":[{"point":[557,471]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"jekyll","codePoint":59709},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[344,1024],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[344,1024]}]},{"tag":"LineTo","args":[{"point":[345,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[379,1024],"end":[408,1005.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[437,987],"end":[453,957]}]}]},{"tag":"LineTo","args":[{"point":[454,957]}]},{"tag":"LineTo","args":[{"point":[755,171]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[758,165],"end":[769.5,152.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[781,140],"end":[791,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[793,130],"end":[794.5,128]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[796,126],"end":[797,124]}]}]},{"tag":"LineTo","args":[{"point":[798,123]}]},{"tag":"LineTo","args":[{"point":[799,122]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[804,110],"end":[796,97.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[788,85],"end":[769,70]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[753,58],"end":[730.5,46]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[708,34],"end":[682,24]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,13],"end":[623.5,6.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,0],"end":[574,0]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,0],"end":[545,4.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[534,9],"end":[531,19]}]}]},{"tag":"LineTo","args":[{"point":[530,20]}]},{"tag":"LineTo","args":[{"point":[530,20]}]},{"tag":"LineTo","args":[{"point":[530,20]}]},{"tag":"LineTo","args":[{"point":[530,21]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[529,24],"end":[529,26.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[529,29],"end":[529,32]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[531,43],"end":[531.5,60]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[532,77],"end":[529,86]}]}]},{"tag":"LineTo","args":[{"point":[230,861]}]},{"tag":"LineTo","args":[{"point":[227,871]}]},{"tag":"LineTo","args":[{"point":[227,871]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[215,915],"end":[236,957]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[257,999],"end":[301,1016]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[312,1020],"end":[322.5,1022]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[333,1024],"end":[344,1024]}]}]}]},{"start":[251,865],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[548,92]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,77],"end":[552,54]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,31],"end":[550,27]}]}]},{"tag":"LineTo","args":[{"point":[550,26]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,24],"end":[555,22]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[560,20],"end":[573,20]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,20],"end":[619,26.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,33],"end":[674,43]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,53],"end":[719,64]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[740,75],"end":[755,86]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[771,98],"end":[775,105]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[779,112],"end":[779,114]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[779,114],"end":[778.5,114.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[778,115],"end":[778,115]}]}]},{"tag":"LineTo","args":[{"point":[777,116]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[773,120],"end":[756.5,136]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[740,152],"end":[736,164]}]}]},{"tag":"LineTo","args":[{"point":[439,936]}]},{"tag":"LineTo","args":[{"point":[438,939]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,968],"end":[400.5,985.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[375,1003],"end":[344,1003]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[335,1003],"end":[326,1001.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[317,1000],"end":[308,996]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[270,982],"end":[253,944]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[236,906],"end":[250,867]}]}]},{"tag":"LineTo","args":[{"point":[251,865]}]}]},{"start":[633,374],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[418,933]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[407,963],"end":[376.5,976.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[346,990],"end":[316,978]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[285,966],"end":[270.5,935.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,905],"end":[268,875]}]}]},{"tag":"LineTo","args":[{"point":[405,518]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[405,518],"end":[417.5,502]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[430,486],"end":[455,471]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[481,456],"end":[503.5,454]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[526,452],"end":[554,439]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,426],"end":[607.5,400]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[633,374],"end":[633,374]}]}]}]},{"start":[418,805.5],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[417,809],"end":[418,812]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[419,815],"end":[422.5,816.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,818],"end":[429,816]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[433,815],"end":[434,811.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[435,808],"end":[434,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[433,802],"end":[429.5,800.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,799],"end":[423,800]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[419,802],"end":[418,805.5]}]}]}]},{"start":[379,723],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[385,721],"end":[386.5,716]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,711],"end":[386,707]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,702],"end":[379,700]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[374,698],"end":[369,700]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[364,702],"end":[362,707]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[360,712],"end":[362,717]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[364,722],"end":[369,724]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[374,726],"end":[379,724]}]}]},{"tag":"LineTo","args":[{"point":[379,723]}]}]},{"start":[392,677],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[391,677]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[395,686],"end":[404.5,689.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[414,693],"end":[424,689]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[433,685],"end":[436.5,675.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[440,666],"end":[436,657]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[431,647],"end":[422,643.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,640],"end":[404,644]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[395,649],"end":[391.5,658.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,668],"end":[392,677]}]}]}]},{"start":[495,580],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[495,580]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,582],"end":[488,587]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[486,592],"end":[488,597]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,602],"end":[495.5,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[501,606],"end":[506,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,602],"end":[512,597]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,592],"end":[512,587]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,582],"end":[505,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[500,578],"end":[495,580]}]}]}]},{"start":[440,522],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[437,523],"end":[435.5,526.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[434,530],"end":[435,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[437,536],"end":[440,537.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[443,539],"end":[447,537]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[450,536],"end":[451.5,532.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[453,529],"end":[451,526]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[450,523],"end":[446.5,521.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[443,520],"end":[440,522]}]}]}]},{"start":[584,49],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[584,49]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,57],"end":[600.5,71.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[621,86],"end":[653,99]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[685,111],"end":[710,114]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[735,117],"end":[738,108]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[742,100],"end":[721.5,85.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,71],"end":[669,58]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[637,46],"end":[612,43]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,40],"end":[584,49]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"linkedin","codePoint":59710},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[872,873],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[872,604]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[872,506],"end":[839,439]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,372],"end":[690,372]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[635,372],"end":[598.5,397]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[562,422],"end":[547,451]}]}]},{"tag":"LineTo","args":[{"point":[545,451]}]},{"tag":"LineTo","args":[{"point":[545,384]}]},{"tag":"LineTo","args":[{"point":[399,384]}]},{"tag":"LineTo","args":[{"point":[399,873]}]},{"tag":"LineTo","args":[{"point":[551,873]}]},{"tag":"LineTo","args":[{"point":[551,631]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,583],"end":[566.5,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,505],"end":[642,505]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,505],"end":[710.5,549]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[721,593],"end":[721,635]}]}]},{"tag":"LineTo","args":[{"point":[721,873]}]},{"tag":"LineTo","args":[{"point":[872,873]}]}]},{"start":[228,317],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,317],"end":[290,291]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[316,265],"end":[316,229]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[316,193],"end":[290,167]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,141],"end":[228,141]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[191,141],"end":[165.5,167]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[140,193],"end":[140,229]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[140,265],"end":[165.5,291]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[191,317],"end":[228,317]}]}]}]},{"start":[304,384],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[152,384]}]},{"tag":"LineTo","args":[{"point":[152,873]}]},{"tag":"LineTo","args":[{"point":[304,873]}]},{"tag":"LineTo","args":[{"point":[304,384]}]}]},{"start":[948,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[948,0]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[979,0],"end":[1001.5,21.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,43],"end":[1024,74]}]}]},{"tag":"LineTo","args":[{"point":[1024,950]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,981],"end":[1001.5,1002.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[979,1024],"end":[948,1024]}]}]},{"tag":"LineTo","args":[{"point":[76,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[44,1024],"end":[22,1002.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,981],"end":[0,950]}]}]},{"tag":"LineTo","args":[{"point":[0,74]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,43],"end":[22,21.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[44,0],"end":[76,0]}]}]},{"tag":"LineTo","args":[{"point":[948,0]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"markdown","codePoint":59711},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[950,827],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[950,827]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[981,827],"end":[1002.5,805.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,784],"end":[1024,753]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,753],"end":[1024,753]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,753],"end":[1024,753]}]}]},{"tag":"LineTo","args":[{"point":[1024,271]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,240],"end":[1002.5,218.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[981,197],"end":[950,197]}]}]},{"tag":"LineTo","args":[{"point":[74,197]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[43,197],"end":[21.5,218.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,240],"end":[0,271]}]}]},{"tag":"LineTo","args":[{"point":[0,753]}]},{"tag":"LineTo","args":[{"point":[0,753]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,784],"end":[21.5,805.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[43,827],"end":[74,827]}]}]},{"tag":"LineTo","args":[{"point":[950,827]}]}]},{"start":[148,679],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[148,345]}]},{"tag":"LineTo","args":[{"point":[246,345]}]},{"tag":"LineTo","args":[{"point":[345,468]}]},{"tag":"LineTo","args":[{"point":[443,345]}]},{"tag":"LineTo","args":[{"point":[542,345]}]},{"tag":"LineTo","args":[{"point":[542,679]}]},{"tag":"LineTo","args":[{"point":[443,679]}]},{"tag":"LineTo","args":[{"point":[443,487]}]},{"tag":"LineTo","args":[{"point":[345,610]}]},{"tag":"LineTo","args":[{"point":[246,487]}]},{"tag":"LineTo","args":[{"point":[246,679]}]},{"tag":"LineTo","args":[{"point":[148,679]}]}]},{"start":[758,684],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[610,512]}]},{"tag":"LineTo","args":[{"point":[709,512]}]},{"tag":"LineTo","args":[{"point":[709,345]}]},{"tag":"LineTo","args":[{"point":[807,345]}]},{"tag":"LineTo","args":[{"point":[807,512]}]},{"tag":"LineTo","args":[{"point":[906,512]}]},{"tag":"LineTo","args":[{"point":[758,684]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"npm","codePoint":59712},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1024,313],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[1024,654]}]},{"tag":"LineTo","args":[{"point":[512,654]}]},{"tag":"LineTo","args":[{"point":[512,711]}]},{"tag":"LineTo","args":[{"point":[284,711]}]},{"tag":"LineTo","args":[{"point":[284,654]}]},{"tag":"LineTo","args":[{"point":[0,654]}]},{"tag":"LineTo","args":[{"point":[0,313]}]},{"tag":"LineTo","args":[{"point":[1024,313]}]}]},{"start":[284,370],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[57,370]}]},{"tag":"LineTo","args":[{"point":[57,597]}]},{"tag":"LineTo","args":[{"point":[171,597]}]},{"tag":"LineTo","args":[{"point":[171,427]}]},{"tag":"LineTo","args":[{"point":[228,427]}]},{"tag":"LineTo","args":[{"point":[228,597]}]},{"tag":"LineTo","args":[{"point":[284,597]}]},{"tag":"LineTo","args":[{"point":[284,370]}]}]},{"start":[569,597],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[569,370]}]},{"tag":"LineTo","args":[{"point":[341,370]}]},{"tag":"LineTo","args":[{"point":[341,654]}]},{"tag":"LineTo","args":[{"point":[455,654]}]},{"tag":"LineTo","args":[{"point":[455,597]}]},{"tag":"LineTo","args":[{"point":[569,597]}]}]},{"start":[967,597],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[967,370]}]},{"tag":"LineTo","args":[{"point":[626,370]}]},{"tag":"LineTo","args":[{"point":[626,597]}]},{"tag":"LineTo","args":[{"point":[740,597]}]},{"tag":"LineTo","args":[{"point":[740,427]}]},{"tag":"LineTo","args":[{"point":[796,427]}]},{"tag":"LineTo","args":[{"point":[796,597]}]},{"tag":"LineTo","args":[{"point":[853,597]}]},{"tag":"LineTo","args":[{"point":[853,427]}]},{"tag":"LineTo","args":[{"point":[910,427]}]},{"tag":"LineTo","args":[{"point":[910,597]}]},{"tag":"LineTo","args":[{"point":[967,597]}]}]},{"start":[455,540],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,540]}]},{"tag":"LineTo","args":[{"point":[512,427]}]},{"tag":"LineTo","args":[{"point":[455,427]}]},{"tag":"LineTo","args":[{"point":[455,540]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"python","codePoint":59713},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[611,8],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[565,2]}]},{"tag":"LineTo","args":[{"point":[511,0]}]},{"tag":"LineTo","args":[{"point":[475,1]}]},{"tag":"LineTo","args":[{"point":[442,3]}]},{"tag":"LineTo","args":[{"point":[412,5]}]},{"tag":"LineTo","args":[{"point":[384,9]}]},{"tag":"LineTo","args":[{"point":[360,15]}]},{"tag":"LineTo","args":[{"point":[338,21]}]},{"tag":"LineTo","args":[{"point":[319,29]}]},{"tag":"LineTo","args":[{"point":[303,37]}]},{"tag":"LineTo","args":[{"point":[290,47]}]},{"tag":"LineTo","args":[{"point":[279,58]}]},{"tag":"LineTo","args":[{"point":[272,70]}]},{"tag":"LineTo","args":[{"point":[267,83]}]},{"tag":"LineTo","args":[{"point":[265,98]}]},{"tag":"LineTo","args":[{"point":[266,113]}]},{"tag":"LineTo","args":[{"point":[266,231]}]},{"tag":"LineTo","args":[{"point":[515,231]}]},{"tag":"LineTo","args":[{"point":[515,266]}]},{"tag":"LineTo","args":[{"point":[167,266]}]},{"tag":"LineTo","args":[{"point":[165,266]}]},{"tag":"LineTo","args":[{"point":[158,266]}]},{"tag":"LineTo","args":[{"point":[148,266]}]},{"tag":"LineTo","args":[{"point":[134,268]}]},{"tag":"LineTo","args":[{"point":[119,273]}]},{"tag":"LineTo","args":[{"point":[102,279]}]},{"tag":"LineTo","args":[{"point":[84,290]}]},{"tag":"LineTo","args":[{"point":[66,304]}]},{"tag":"LineTo","args":[{"point":[49,323]}]},{"tag":"LineTo","args":[{"point":[33,347]}]},{"tag":"LineTo","args":[{"point":[20,377]}]},{"tag":"LineTo","args":[{"point":[9,414]}]},{"tag":"LineTo","args":[{"point":[3,459]}]},{"tag":"LineTo","args":[{"point":[0,511]}]},{"tag":"LineTo","args":[{"point":[2,563]}]},{"tag":"LineTo","args":[{"point":[8,608]}]},{"tag":"LineTo","args":[{"point":[17,646]}]},{"tag":"LineTo","args":[{"point":[29,677]}]},{"tag":"LineTo","args":[{"point":[43,702]}]},{"tag":"LineTo","args":[{"point":[58,721]}]},{"tag":"LineTo","args":[{"point":[73,737]}]},{"tag":"LineTo","args":[{"point":[88,748]}]},{"tag":"LineTo","args":[{"point":[103,756]}]},{"tag":"LineTo","args":[{"point":[117,761]}]},{"tag":"LineTo","args":[{"point":[129,764]}]},{"tag":"LineTo","args":[{"point":[138,765]}]},{"tag":"LineTo","args":[{"point":[233,765]}]},{"tag":"LineTo","args":[{"point":[233,634]}]},{"tag":"LineTo","args":[{"point":[234,625]}]},{"tag":"LineTo","args":[{"point":[236,614]}]},{"tag":"LineTo","args":[{"point":[239,600]}]},{"tag":"LineTo","args":[{"point":[243,585]}]},{"tag":"LineTo","args":[{"point":[249,570]}]},{"tag":"LineTo","args":[{"point":[258,554]}]},{"tag":"LineTo","args":[{"point":[269,539]}]},{"tag":"LineTo","args":[{"point":[283,526]}]},{"tag":"LineTo","args":[{"point":[301,514]}]},{"tag":"LineTo","args":[{"point":[322,505]}]},{"tag":"LineTo","args":[{"point":[347,499]}]},{"tag":"LineTo","args":[{"point":[377,497]}]},{"tag":"LineTo","args":[{"point":[631,497]}]},{"tag":"LineTo","args":[{"point":[640,496]}]},{"tag":"LineTo","args":[{"point":[652,494]}]},{"tag":"LineTo","args":[{"point":[664,491]}]},{"tag":"LineTo","args":[{"point":[678,487]}]},{"tag":"LineTo","args":[{"point":[693,481]}]},{"tag":"LineTo","args":[{"point":[708,473]}]},{"tag":"LineTo","args":[{"point":[722,462]}]},{"tag":"LineTo","args":[{"point":[735,449]}]},{"tag":"LineTo","args":[{"point":[746,433]}]},{"tag":"LineTo","args":[{"point":[755,413]}]},{"tag":"LineTo","args":[{"point":[761,390]}]},{"tag":"LineTo","args":[{"point":[763,363]}]},{"tag":"LineTo","args":[{"point":[763,135]}]},{"tag":"LineTo","args":[{"point":[763,129]}]},{"tag":"LineTo","args":[{"point":[762,121]}]},{"tag":"LineTo","args":[{"point":[761,110]}]},{"tag":"LineTo","args":[{"point":[756,97]}]},{"tag":"LineTo","args":[{"point":[750,83]}]},{"tag":"LineTo","args":[{"point":[739,68]}]},{"tag":"LineTo","args":[{"point":[724,54]}]},{"tag":"LineTo","args":[{"point":[705,40]}]},{"tag":"LineTo","args":[{"point":[680,27]}]},{"tag":"LineTo","args":[{"point":[649,16]}]},{"tag":"LineTo","args":[{"point":[611,8]}]}]},{"start":[356,83],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[373,79]}]},{"tag":"LineTo","args":[{"point":[391,83]}]},{"tag":"LineTo","args":[{"point":[405,92]}]},{"tag":"LineTo","args":[{"point":[415,106]}]},{"tag":"LineTo","args":[{"point":[418,124]}]},{"tag":"LineTo","args":[{"point":[415,141]}]},{"tag":"LineTo","args":[{"point":[405,156]}]},{"tag":"LineTo","args":[{"point":[391,165]}]},{"tag":"LineTo","args":[{"point":[373,169]}]},{"tag":"LineTo","args":[{"point":[356,165]}]},{"tag":"LineTo","args":[{"point":[342,156]}]},{"tag":"LineTo","args":[{"point":[332,141]}]},{"tag":"LineTo","args":[{"point":[329,124]}]},{"tag":"LineTo","args":[{"point":[332,106]}]},{"tag":"LineTo","args":[{"point":[342,92]}]},{"tag":"LineTo","args":[{"point":[356,83]}]}]},{"start":[900,261],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[891,259]}]},{"tag":"LineTo","args":[{"point":[885,259]}]},{"tag":"LineTo","args":[{"point":[796,259]}]},{"tag":"LineTo","args":[{"point":[796,390]}]},{"tag":"LineTo","args":[{"point":[795,399]}]},{"tag":"LineTo","args":[{"point":[794,410]}]},{"tag":"LineTo","args":[{"point":[791,424]}]},{"tag":"LineTo","args":[{"point":[786,439]}]},{"tag":"LineTo","args":[{"point":[780,454]}]},{"tag":"LineTo","args":[{"point":[771,470]}]},{"tag":"LineTo","args":[{"point":[760,485]}]},{"tag":"LineTo","args":[{"point":[746,498]}]},{"tag":"LineTo","args":[{"point":[728,510]}]},{"tag":"LineTo","args":[{"point":[707,519]}]},{"tag":"LineTo","args":[{"point":[682,525]}]},{"tag":"LineTo","args":[{"point":[652,527]}]},{"tag":"LineTo","args":[{"point":[403,527]}]},{"tag":"LineTo","args":[{"point":[398,528]}]},{"tag":"LineTo","args":[{"point":[389,529]}]},{"tag":"LineTo","args":[{"point":[378,530]}]},{"tag":"LineTo","args":[{"point":[365,533]}]},{"tag":"LineTo","args":[{"point":[351,537]}]},{"tag":"LineTo","args":[{"point":[336,543]}]},{"tag":"LineTo","args":[{"point":[321,552]}]},{"tag":"LineTo","args":[{"point":[307,562]}]},{"tag":"LineTo","args":[{"point":[294,576]}]},{"tag":"LineTo","args":[{"point":[283,592]}]},{"tag":"LineTo","args":[{"point":[274,611]}]},{"tag":"LineTo","args":[{"point":[268,634]}]},{"tag":"LineTo","args":[{"point":[266,662]}]},{"tag":"LineTo","args":[{"point":[266,890]}]},{"tag":"LineTo","args":[{"point":[266,895]}]},{"tag":"LineTo","args":[{"point":[267,904]}]},{"tag":"LineTo","args":[{"point":[268,914]}]},{"tag":"LineTo","args":[{"point":[273,927]}]},{"tag":"LineTo","args":[{"point":[279,941]}]},{"tag":"LineTo","args":[{"point":[290,956]}]},{"tag":"LineTo","args":[{"point":[305,970]}]},{"tag":"LineTo","args":[{"point":[324,984]}]},{"tag":"LineTo","args":[{"point":[349,997]}]},{"tag":"LineTo","args":[{"point":[380,1008]}]},{"tag":"LineTo","args":[{"point":[419,1016]}]},{"tag":"LineTo","args":[{"point":[464,1022]}]},{"tag":"LineTo","args":[{"point":[518,1024]}]},{"tag":"LineTo","args":[{"point":[554,1024]}]},{"tag":"LineTo","args":[{"point":[587,1022]}]},{"tag":"LineTo","args":[{"point":[617,1019]}]},{"tag":"LineTo","args":[{"point":[645,1015]}]},{"tag":"LineTo","args":[{"point":[669,1009]}]},{"tag":"LineTo","args":[{"point":[691,1003]}]},{"tag":"LineTo","args":[{"point":[710,996]}]},{"tag":"LineTo","args":[{"point":[726,987]}]},{"tag":"LineTo","args":[{"point":[739,977]}]},{"tag":"LineTo","args":[{"point":[750,966]}]},{"tag":"LineTo","args":[{"point":[757,954]}]},{"tag":"LineTo","args":[{"point":[762,941]}]},{"tag":"LineTo","args":[{"point":[764,926]}]},{"tag":"LineTo","args":[{"point":[763,911]}]},{"tag":"LineTo","args":[{"point":[763,793]}]},{"tag":"LineTo","args":[{"point":[514,793]}]},{"tag":"LineTo","args":[{"point":[514,758]}]},{"tag":"LineTo","args":[{"point":[864,758]}]},{"tag":"LineTo","args":[{"point":[871,759]}]},{"tag":"LineTo","args":[{"point":[881,758]}]},{"tag":"LineTo","args":[{"point":[895,756]}]},{"tag":"LineTo","args":[{"point":[911,752]}]},{"tag":"LineTo","args":[{"point":[928,745]}]},{"tag":"LineTo","args":[{"point":[945,735]}]},{"tag":"LineTo","args":[{"point":[963,721]}]},{"tag":"LineTo","args":[{"point":[980,701]}]},{"tag":"LineTo","args":[{"point":[996,677]}]},{"tag":"LineTo","args":[{"point":[1009,647]}]},{"tag":"LineTo","args":[{"point":[1020,610]}]},{"tag":"LineTo","args":[{"point":[1027,566]}]},{"tag":"LineTo","args":[{"point":[1029,513]}]},{"tag":"LineTo","args":[{"point":[1027,461]}]},{"tag":"LineTo","args":[{"point":[1021,416]}]},{"tag":"LineTo","args":[{"point":[1012,379]}]},{"tag":"LineTo","args":[{"point":[1000,348]}]},{"tag":"LineTo","args":[{"point":[986,323]}]},{"tag":"LineTo","args":[{"point":[972,303]}]},{"tag":"LineTo","args":[{"point":[956,288]}]},{"tag":"LineTo","args":[{"point":[941,276]}]},{"tag":"LineTo","args":[{"point":[926,268]}]},{"tag":"LineTo","args":[{"point":[912,263]}]},{"tag":"LineTo","args":[{"point":[900,261]}]}]},{"start":[638,859],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[656,855]}]},{"tag":"LineTo","args":[{"point":[673,859]}]},{"tag":"LineTo","args":[{"point":[687,869]}]},{"tag":"LineTo","args":[{"point":[697,883]}]},{"tag":"LineTo","args":[{"point":[701,900]}]},{"tag":"LineTo","args":[{"point":[697,918]}]},{"tag":"LineTo","args":[{"point":[687,932]}]},{"tag":"LineTo","args":[{"point":[673,942]}]},{"tag":"LineTo","args":[{"point":[656,945]}]},{"tag":"LineTo","args":[{"point":[638,942]}]},{"tag":"LineTo","args":[{"point":[624,932]}]},{"tag":"LineTo","args":[{"point":[614,918]}]},{"tag":"LineTo","args":[{"point":[611,900]}]},{"tag":"LineTo","args":[{"point":[614,883]}]},{"tag":"LineTo","args":[{"point":[624,869]}]},{"tag":"LineTo","args":[{"point":[638,859]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"react","codePoint":59714},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,421],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,421],"end":[576.5,447.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[603,474],"end":[603,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[603,550],"end":[576.5,576.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,603],"end":[512,603]}]}]},{"tag":"LineTo","args":[{"point":[512,603]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[474,603],"end":[447.5,576.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[421,550],"end":[421,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[421,474],"end":[447.5,447.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[474,421],"end":[512,421]}]}]}]},{"start":[256,694],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[262,674]}]},{"tag":"LineTo","args":[{"point":[264,667]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,627],"end":[289.5,590]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[304,553],"end":[320,521]}]}]},{"tag":"LineTo","args":[{"point":[324,512]}]},{"tag":"LineTo","args":[{"point":[320,503]}]},{"tag":"LineTo","args":[{"point":[323,509]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[305,472],"end":[290,433.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,395],"end":[262,350]}]}]},{"tag":"LineTo","args":[{"point":[256,330]}]},{"tag":"LineTo","args":[{"point":[236,335]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[124,364],"end":[62,410]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,456],"end":[0,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,568],"end":[62,614]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[124,660],"end":[236,688]}]}]},{"tag":"LineTo","args":[{"point":[256,694]}]}]},{"start":[227,382],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[229,389]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[240,423],"end":[252.5,454]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[265,485],"end":[277,512]}]}]},{"tag":"LineTo","args":[{"point":[280,505]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[265,537],"end":[252,570]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[239,603],"end":[227,642]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[141,618],"end":[92,583]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[43,548],"end":[43,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[43,475],"end":[92,440.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[141,406],"end":[227,382]}]}]}]},{"start":[768,694],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[788,688]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[900,660],"end":[962,614]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,568],"end":[1024,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,456],"end":[962,410]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[900,364],"end":[788,335]}]}]},{"tag":"LineTo","args":[{"point":[768,330]}]},{"tag":"LineTo","args":[{"point":[762,350]}]},{"tag":"LineTo","args":[{"point":[760,357]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,396],"end":[734,433.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[720,471],"end":[704,503]}]}]},{"tag":"LineTo","args":[{"point":[699,512]}]},{"tag":"LineTo","args":[{"point":[704,521]}]},{"tag":"LineTo","args":[{"point":[701,515]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[719,551],"end":[734,590]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[749,629],"end":[762,674]}]}]},{"tag":"LineTo","args":[{"point":[768,694]}]}]},{"start":[747,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[744,518]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[759,487],"end":[772,454]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[785,421],"end":[797,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[883,406],"end":[932,440.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[981,475],"end":[981,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[981,548],"end":[932,583]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[883,618],"end":[797,642]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[787,610],"end":[774.5,577.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[762,545],"end":[747,512]}]}]}]},{"start":[227,382],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[247,377]}]},{"tag":"LineTo","args":[{"point":[240,378]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[280,368],"end":[321.5,361]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[363,354],"end":[408,351]}]}]},{"tag":"LineTo","args":[{"point":[418,350]}]},{"tag":"LineTo","args":[{"point":[424,341]}]},{"tag":"LineTo","args":[{"point":[426,338]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[449,305],"end":[474.5,274]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[500,243],"end":[527,215]}]}]},{"tag":"LineTo","args":[{"point":[542,200]}]},{"tag":"LineTo","args":[{"point":[527,185]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[447,103],"end":[375,72.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[303,42],"end":[256,69]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[208,97],"end":[198.5,173.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[189,250],"end":[221,362]}]}]},{"tag":"LineTo","args":[{"point":[227,382]}]}]},{"start":[307,99],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[341,99],"end":[386.5,125.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,152],"end":[482,200]}]}]},{"tag":"LineTo","args":[{"point":[483,200]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,225],"end":[438,252]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[416,279],"end":[395,309]}]}]},{"tag":"LineTo","args":[{"point":[390,309]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[354,313],"end":[320,318]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[286,323],"end":[257,330]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[235,244],"end":[240.5,184]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[246,124],"end":[277,106]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[284,102],"end":[291.5,100.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,99],"end":[307,99]}]}]}]},{"start":[717,968],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[717,968]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,968],"end":[717.5,968]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[718,968],"end":[718,968]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,968],"end":[744.5,964.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,961],"end":[768,954]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[816,927],"end":[825.5,850]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[835,773],"end":[803,662]}]}]},{"tag":"LineTo","args":[{"point":[797,642]}]},{"tag":"LineTo","args":[{"point":[777,647]}]},{"tag":"LineTo","args":[{"point":[784,646]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[744,656],"end":[702.5,662.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[661,669],"end":[616,673]}]}]},{"tag":"LineTo","args":[{"point":[606,674]}]},{"tag":"LineTo","args":[{"point":[600,682]}]},{"tag":"LineTo","args":[{"point":[598,685]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[575,718],"end":[549.5,749]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[524,780],"end":[497,809]}]}]},{"tag":"LineTo","args":[{"point":[482,824]}]},{"tag":"LineTo","args":[{"point":[497,839]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,901],"end":[614,934.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[671,968],"end":[717,968]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,968],"end":[717,968]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,968],"end":[717,968]}]}]}]},{"start":[542,823],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[541,824]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,799],"end":[586,772]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,745],"end":[629,715]}]}]},{"tag":"LineTo","args":[{"point":[633,714]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[669,711],"end":[703.5,705.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[738,700],"end":[767,693]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[789,779],"end":[783.5,839]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[778,899],"end":[747,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[740,921],"end":[732.5,923]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,925],"end":[717,925]}]}]},{"tag":"LineTo","args":[{"point":[717,925]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,925],"end":[637.5,898.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,872],"end":[542,823]}]}]}]},{"start":[803,362],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[835,250],"end":[825.5,173.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[816,97],"end":[768,69]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[720,42],"end":[648,72.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,103],"end":[497,185]}]}]},{"tag":"LineTo","args":[{"point":[482,200]}]},{"tag":"LineTo","args":[{"point":[497,215]}]},{"tag":"LineTo","args":[{"point":[496,215]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[524,243],"end":[549.5,274.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[575,306],"end":[600,341]}]}]},{"tag":"LineTo","args":[{"point":[606,350]}]},{"tag":"LineTo","args":[{"point":[616,351]}]},{"tag":"LineTo","args":[{"point":[619,351]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[662,354],"end":[702,361]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[742,368],"end":[777,377]}]}]},{"tag":"LineTo","args":[{"point":[797,382]}]},{"tag":"LineTo","args":[{"point":[803,362]}]}]},{"start":[629,309],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[627,306]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[607,278],"end":[585.5,251.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,225],"end":[542,200]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[604,139],"end":[659.5,113.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[715,88],"end":[747,106]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[778,124],"end":[783.5,184]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[789,244],"end":[767,330]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[735,323],"end":[700.5,317.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,312],"end":[629,309]}]}]}]},{"start":[307,968],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[353,968],"end":[409.5,934.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[466,901],"end":[527,839]}]}]},{"tag":"LineTo","args":[{"point":[542,824]}]},{"tag":"LineTo","args":[{"point":[527,809]}]},{"tag":"LineTo","args":[{"point":[528,809]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[500,780],"end":[474,749]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,718],"end":[424,682]}]}]},{"tag":"LineTo","args":[{"point":[418,674]}]},{"tag":"LineTo","args":[{"point":[408,673]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[365,669],"end":[324.5,662.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[284,656],"end":[247,647]}]}]},{"tag":"LineTo","args":[{"point":[227,642]}]},{"tag":"LineTo","args":[{"point":[221,662]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[189,773],"end":[198.5,850]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[208,927],"end":[256,954]}]}]},{"tag":"LineTo","args":[{"point":[256,954]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[267,961],"end":[279.5,964.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[292,968],"end":[306,968]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[306,968],"end":[306.5,968]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[307,968],"end":[307,968]}]}]}]},{"start":[257,693],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[250,692]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[284,700],"end":[320,705.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[356,711],"end":[395,715]}]}]},{"tag":"LineTo","args":[{"point":[397,718]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[417,746],"end":[438.5,772.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,799],"end":[482,823]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,884],"end":[364.5,910]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[309,936],"end":[277,918]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[246,899],"end":[240.5,839]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[235,779],"end":[257,693]}]}]}]},{"start":[512,720],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[538,720],"end":[565,719]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,718],"end":[619,716]}]}]},{"tag":"LineTo","args":[{"point":[629,715]}]},{"tag":"LineTo","args":[{"point":[635,707]}]},{"tag":"LineTo","args":[{"point":[632,711]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[663,668],"end":[690,621]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,574],"end":[742,521]}]}]},{"tag":"LineTo","args":[{"point":[747,512]}]},{"tag":"LineTo","args":[{"point":[742,503]}]},{"tag":"LineTo","args":[{"point":[739,495]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[716,447],"end":[689.5,402]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[663,357],"end":[635,317]}]}]},{"tag":"LineTo","args":[{"point":[629,309]}]},{"tag":"LineTo","args":[{"point":[619,308]}]},{"tag":"LineTo","args":[{"point":[624,308]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[596,306],"end":[568,305]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[540,304],"end":[512,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[484,304],"end":[456.5,305]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[429,306],"end":[405,308]}]}]},{"tag":"LineTo","args":[{"point":[395,309]}]},{"tag":"LineTo","args":[{"point":[389,317]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[373,339],"end":[359,362]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[345,385],"end":[331,408]}]}]},{"tag":"LineTo","args":[{"point":[335,402]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[321,424],"end":[308.5,448.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,473],"end":[282,503]}]}]},{"tag":"LineTo","args":[{"point":[277,512]}]},{"tag":"LineTo","args":[{"point":[282,521]}]},{"tag":"LineTo","args":[{"point":[285,529]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,553],"end":[308,575.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,598],"end":[331,616]}]}]},{"tag":"LineTo","args":[{"point":[335,622]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[348,645],"end":[362,666.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[376,688],"end":[389,707]}]}]},{"tag":"LineTo","args":[{"point":[395,715]}]},{"tag":"LineTo","args":[{"point":[405,716]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,718],"end":[459,719]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[486,720],"end":[512,720]}]}]}]},{"start":[418,674],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[421,678]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[395,640],"end":[371,599.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[347,559],"end":[325,512]}]}]},{"tag":"LineTo","args":[{"point":[328,505]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[348,463],"end":[371,424]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[394,385],"end":[418,350]}]}]},{"tag":"LineTo","args":[{"point":[414,350]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,348],"end":[462.5,347]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[487,346],"end":[512,346]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,346],"end":[561,347]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[585,348],"end":[606,350]}]}]},{"tag":"LineTo","args":[{"point":[603,346]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,383],"end":[653,424]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,465],"end":[699,512]}]}]},{"tag":"LineTo","args":[{"point":[696,519]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[676,561],"end":[653,600]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[630,639],"end":[606,674]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[558,677],"end":[512,677]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[466,677],"end":[418,674]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"stackexchange","codePoint":59715},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[927,665],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[97,665]}]},{"tag":"LineTo","args":[{"point":[97,709]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[97,765],"end":[136,804.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[175,844],"end":[229,844]}]}]},{"tag":"LineTo","args":[{"point":[582,844]}]},{"tag":"LineTo","args":[{"point":[582,1024]}]},{"tag":"LineTo","args":[{"point":[756,844]}]},{"tag":"LineTo","args":[{"point":[795,844]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[849,844],"end":[888,804.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[927,765],"end":[927,709]}]}]},{"tag":"LineTo","args":[{"point":[927,665]}]}]},{"start":[97,446],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[97,616]}]},{"tag":"LineTo","args":[{"point":[922,616]}]},{"tag":"LineTo","args":[{"point":[922,446]}]},{"tag":"LineTo","args":[{"point":[97,446]}]}]},{"start":[97,227],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[97,397]}]},{"tag":"LineTo","args":[{"point":[922,397]}]},{"tag":"LineTo","args":[{"point":[922,227]}]},{"tag":"LineTo","args":[{"point":[97,227]}]}]},{"start":[793,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[229,0]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[175,0],"end":[136,39.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[97,79],"end":[97,136]}]}]},{"tag":"LineTo","args":[{"point":[97,180]}]},{"tag":"LineTo","args":[{"point":[922,180]}]},{"tag":"LineTo","args":[{"point":[922,136]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[922,79],"end":[884,39.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[846,0],"end":[793,0]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"stackoverflow","codePoint":59716},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[810,933],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,933]}]},{"tag":"LineTo","args":[{"point":[170,660]}]},{"tag":"LineTo","args":[{"point":[79,660]}]},{"tag":"LineTo","args":[{"point":[79,1024]}]},{"tag":"LineTo","args":[{"point":[901,1024]}]},{"tag":"LineTo","args":[{"point":[901,660]}]},{"tag":"LineTo","args":[{"point":[810,660]}]},{"tag":"LineTo","args":[{"point":[810,933]}]}]},{"start":[261,751],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[719,751]}]},{"tag":"LineTo","args":[{"point":[719,842]}]},{"tag":"LineTo","args":[{"point":[261,842]}]},{"tag":"LineTo","args":[{"point":[261,751]}]}]},{"start":[272,635],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[291,547]}]},{"tag":"LineTo","args":[{"point":[738,640]}]},{"tag":"LineTo","args":[{"point":[719,728]}]},{"tag":"LineTo","args":[{"point":[272,635]}]}]},{"start":[330,419],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[368,336]}]},{"tag":"LineTo","args":[{"point":[782,529]}]},{"tag":"LineTo","args":[{"point":[744,612]}]},{"tag":"LineTo","args":[{"point":[330,419]}]}]},{"start":[446,215],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[446,215]}]},{"tag":"LineTo","args":[{"point":[504,146]}]},{"tag":"LineTo","args":[{"point":[854,438]}]},{"tag":"LineTo","args":[{"point":[796,507]}]},{"tag":"LineTo","args":[{"point":[446,215]}]}]},{"start":[945,367],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[871,422]}]},{"tag":"LineTo","args":[{"point":[598,55]}]},{"tag":"LineTo","args":[{"point":[672,0]}]},{"tag":"LineTo","args":[{"point":[945,367]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"telegram","codePoint":59717},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1020,161],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1032,114],"end":[1008.5,94]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[985,74],"end":[954,87]}]}]},{"tag":"LineTo","args":[{"point":[47,437]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1,456],"end":[0,478.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[-1,501],"end":[36,512]}]}]},{"tag":"LineTo","args":[{"point":[269,585]}]},{"tag":"LineTo","args":[{"point":[350,849]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,870],"end":[360.5,879.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[363,889],"end":[386,889]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[403,889],"end":[413,882]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[423,875],"end":[433,865]}]}]},{"tag":"LineTo","args":[{"point":[546,756]}]},{"tag":"LineTo","args":[{"point":[781,929]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[813,947],"end":[835.5,937.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,928],"end":[866,889]}]}]},{"tag":"LineTo","args":[{"point":[1020,162]}]},{"tag":"LineTo","args":[{"point":[1020,161]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"twitter","codePoint":59718},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1022,195],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[1024,196]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1003,227],"end":[976.5,254.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[950,282],"end":[919,305]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[920,311],"end":[920,318]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[920,325],"end":[920,331]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[920,435],"end":[881,541]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[842,648],"end":[766,734]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[690,820],"end":[579,874]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[467,928],"end":[322,928]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[278,928],"end":[235,922]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,915],"end":[151.5,903]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[111,891],"end":[73,874]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[35,856],"end":[0,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[13,835],"end":[25,836]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[37,837],"end":[50,837]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[123,837],"end":[190,813]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[257,789],"end":[310,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[241,746],"end":[187.5,705]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,664],"end":[114,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[123,603],"end":[133,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[143,605],"end":[153,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[167,605],"end":[181,603]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[195,601],"end":[208,598]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,583],"end":[88,525.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[40,468],"end":[40,392]}]}]},{"tag":"LineTo","args":[{"point":[40,389]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[61,401],"end":[85,407.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[109,414],"end":[135,415]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[93,387],"end":[67.5,341]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,295],"end":[42,240]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,211],"end":[49,184.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[56,158],"end":[70,135]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[109,182],"end":[157,221]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[205,260],"end":[260,288.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[315,317],"end":[376,334]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[437,350],"end":[503,354]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[500,342],"end":[498.5,330]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,318],"end":[497,306]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,262],"end":[514,224]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,186],"end":[558.5,157.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,129],"end":[625,112]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[664,96],"end":[707,96]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[753,96],"end":[792.5,114]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,132],"end":[860,162]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,155],"end":[929.5,142.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[963,130],"end":[994,112]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,149],"end":[958,178.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[934,208],"end":[902,228]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[933,224],"end":[963.5,215.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[994,207],"end":[1022,195]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"visualstudiocode","codePoint":59719},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,831]}]},{"tag":"LineTo","args":[{"point":[0,765]}]},{"tag":"LineTo","args":[{"point":[0,789]}]},{"tag":"LineTo","args":[{"point":[768,1024]}]},{"tag":"LineTo","args":[{"point":[1024,917]}]},{"tag":"LineTo","args":[{"point":[1024,107]}]},{"tag":"LineTo","args":[{"point":[768,0]}]}]},{"start":[103,595],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[250,484]}]},{"tag":"LineTo","args":[{"point":[512,725]}]},{"tag":"LineTo","args":[{"point":[640,663]}]},{"tag":"LineTo","args":[{"point":[640,190]}]},{"tag":"LineTo","args":[{"point":[512,128]}]},{"tag":"LineTo","args":[{"point":[250,369]}]},{"tag":"LineTo","args":[{"point":[103,259]}]},{"tag":"LineTo","args":[{"point":[43,294]}]},{"tag":"LineTo","args":[{"point":[187,427]}]},{"tag":"LineTo","args":[{"point":[43,559]}]},{"tag":"LineTo","args":[{"point":[103,595]}]}]},{"start":[512,287],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,567]}]},{"tag":"LineTo","args":[{"point":[326,427]}]},{"tag":"LineTo","args":[{"point":[512,287]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"webpack","codePoint":59720},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[897,773],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[757,693]}]},{"tag":"LineTo","args":[{"point":[527,819]}]},{"tag":"LineTo","args":[{"point":[527,981]}]},{"tag":"LineTo","args":[{"point":[897,773]}]}]},{"start":[922,750],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[787,673]}]},{"tag":"LineTo","args":[{"point":[787,393]}]},{"tag":"LineTo","args":[{"point":[922,315]}]},{"tag":"LineTo","args":[{"point":[922,750]}]}]},{"start":[125,773],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[264,693]}]},{"tag":"LineTo","args":[{"point":[495,819]}]},{"tag":"LineTo","args":[{"point":[495,981]}]},{"tag":"LineTo","args":[{"point":[125,773]}]}]},{"start":[100,750],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[235,673]}]},{"tag":"LineTo","args":[{"point":[235,393]}]},{"tag":"LineTo","args":[{"point":[100,315]}]},{"tag":"LineTo","args":[{"point":[100,750]}]}]},{"start":[115,287],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[250,364]}]},{"tag":"LineTo","args":[{"point":[495,230]}]},{"tag":"LineTo","args":[{"point":[495,73]}]},{"tag":"LineTo","args":[{"point":[115,287]}]}]},{"start":[906,287],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[772,364]}]},{"tag":"LineTo","args":[{"point":[527,230]}]},{"tag":"LineTo","args":[{"point":[527,73]}]},{"tag":"LineTo","args":[{"point":[906,287]}]}]},{"start":[495,782],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[495,542]}]},{"tag":"LineTo","args":[{"point":[267,411]}]},{"tag":"LineTo","args":[{"point":[267,658]}]},{"tag":"LineTo","args":[{"point":[495,782]}]}]},{"start":[527,782],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[527,542]}]},{"tag":"LineTo","args":[{"point":[754,411]}]},{"tag":"LineTo","args":[{"point":[754,658]}]},{"tag":"LineTo","args":[{"point":[527,782]}]}]},{"start":[283,383],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[511,514]}]},{"tag":"LineTo","args":[{"point":[739,383]}]},{"tag":"LineTo","args":[{"point":[511,258]}]},{"tag":"LineTo","args":[{"point":[283,383]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"yarn","codePoint":59721},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[711,40],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[711,40]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[805,80],"end":[874.5,149.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[944,219],"end":[984,313]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,406],"end":[1024,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,618],"end":[984,711]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[944,805],"end":[874.5,874.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[805,944],"end":[711,984]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,1024],"end":[512,1024]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,1024],"end":[313,984]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[219,944],"end":[149.5,874.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[80,805],"end":[40,711]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,618],"end":[0,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,406],"end":[40,313]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[80,219],"end":[149.5,149.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[219,80],"end":[313,40]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,0],"end":[512,0]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,0],"end":[711,40]}]}]}]},{"start":[545,175],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[545,175]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[541,175],"end":[537.5,175.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[534,176],"end":[531,178]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[518,182],"end":[507.5,194]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[497,206],"end":[487,227]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[485,231],"end":[484,234]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[483,237],"end":[482,240]}]}]},{"tag":"LineTo","args":[{"point":[481,240]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[452,242],"end":[427,254]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[402,266],"end":[384,287]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,290],"end":[374.5,292.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[369,295],"end":[363,298]}]}]},{"tag":"LineTo","args":[{"point":[363,298]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[350,302],"end":[342.5,313.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[335,325],"end":[328,344]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[318,371],"end":[325,396]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[332,421],"end":[342,438]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[328,451],"end":[312,470]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,489],"end":[286,512]}]}]},{"tag":"LineTo","args":[{"point":[286,511]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[278,532],"end":[273,554.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[268,577],"end":[268,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[268,603],"end":[268,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[268,607],"end":[268,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[258,619],"end":[245.5,638.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[233,658],"end":[231,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[228,714],"end":[236.5,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[245,756],"end":[250,764]}]}]},{"tag":"LineTo","args":[{"point":[250,765]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[252,767],"end":[253.5,769]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[255,771],"end":[257,773]}]}]},{"tag":"LineTo","args":[{"point":[257,772]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,774],"end":[256,776.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,779],"end":[256,781]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,794],"end":[263,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[270,816],"end":[281,822]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[301,832],"end":[326,835]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[351,838],"end":[372,828]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,836],"end":[394.5,842.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[409,849],"end":[434,849]}]}]},{"tag":"LineTo","args":[{"point":[436,849]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[442,849],"end":[501,845]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[560,841],"end":[584,835]}]}]},{"tag":"LineTo","args":[{"point":[584,835]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[593,833],"end":[600,829.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[607,826],"end":[614,821]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,816],"end":[666,801.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[703,787],"end":[742,761]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[770,743],"end":[784.5,736.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[799,730],"end":[820,725]}]}]},{"tag":"LineTo","args":[{"point":[820,725]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[838,721],"end":[849.5,707]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[861,693],"end":[861,674]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[861,672],"end":[861,670.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[861,669],"end":[860,667]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,646],"end":[841.5,633]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[825,620],"end":[803,620]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,621],"end":[740.5,634.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[713,648],"end":[694,660]}]}]},{"tag":"LineTo","args":[{"point":[695,659]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[689,663],"end":[682.5,666.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[676,670],"end":[668,674]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[669,657],"end":[667,635]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[665,613],"end":[656,588]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,558],"end":[632,539.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[619,521],"end":[609,510]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[621,492],"end":[635,466]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[649,440],"end":[657,400]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[664,366],"end":[661,321]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[658,276],"end":[643,246]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,240],"end":[634.5,236]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,232],"end":[623,230]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,230],"end":[614.5,229]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[609,228],"end":[599,231]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[584,200],"end":[577.5,192.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[571,185],"end":[567,182]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[562,179],"end":[556.5,177]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,175],"end":[545,175]}]}]}]},{"start":[545,205],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[545,205]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[545,205],"end":[545,205]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[545,205],"end":[545,205]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[547,205],"end":[548.5,205.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,206],"end":[552,207]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,211],"end":[571,240.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[585,270],"end":[585,270]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[585,270],"end":[599.5,262.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,255],"end":[617,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,285],"end":[631.5,325.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[634,366],"end":[628,394]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[619,442],"end":[600,469.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,497],"end":[571,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[569,517],"end":[590,533.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[611,550],"end":[629,598]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,643],"end":[638.5,675.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[632,708],"end":[634,712]}]}]},{"tag":"LineTo","args":[{"point":[635,714]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[635,714],"end":[653.5,711]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,708],"end":[709,685]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[729,673],"end":[752.5,661.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[776,650],"end":[803,650]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[829,650],"end":[831,670.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[833,691],"end":[813,696]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[790,702],"end":[773,709.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[756,717],"end":[727,736]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[681,766],"end":[640,780]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[599,794],"end":[599,794]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[599,794],"end":[594,798.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[589,803],"end":[577,806]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,811],"end":[499.5,815]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[443,819],"end":[436,819]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[417,819],"end":[405,814.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[393,810],"end":[390,802]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,778],"end":[394.5,767]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[409,756],"end":[409,756]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[409,756],"end":[405,753.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[401,751],"end":[398,748]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[395,745],"end":[392.5,741]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[390,737],"end":[389,739]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[385,749],"end":[381.5,766.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[378,784],"end":[368,794]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[355,808],"end":[333,806]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[311,804],"end":[295,796]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[279,787],"end":[288,771.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[297,756],"end":[297,756]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[297,756],"end":[289.5,758]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[282,760],"end":[275,749]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[269,739],"end":[264,722]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[259,705],"end":[261,684]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,660],"end":[280.5,640.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,621],"end":[299,621]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,621],"end":[298.5,591]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,561],"end":[313,524]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[327,491],"end":[355,467.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[383,444],"end":[383,444]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[383,444],"end":[363.5,415]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[344,386],"end":[356,354]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[364,334],"end":[367.5,330.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[371,327],"end":[374,326]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[383,322],"end":[391,318]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[399,314],"end":[406,307]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,273],"end":[470,271.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[502,270],"end":[502,270]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[502,270],"end":[515.5,238]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[529,206],"end":[545,205]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"youtube","codePoint":59722},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1002,265],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[1001,257]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1012,316],"end":[1018,377.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,439],"end":[1024,502]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,505],"end":[1024,507.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,510],"end":[1024,512]}]}]},{"tag":"LineTo","args":[{"point":[1024,512]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,514],"end":[1024,516.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,519],"end":[1024,521]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,584],"end":[1018.5,645]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1013,706],"end":[1002,759]}]}]},{"tag":"LineTo","args":[{"point":[1002,760]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[993,792],"end":[969.5,815.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[946,839],"end":[913,848]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[883,856],"end":[817,861]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[750,865],"end":[682.5,867]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,869],"end":[564,869]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,870],"end":[512,870]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,870],"end":[461,869]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[410,869],"end":[342,867]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[274,865],"end":[208,861]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[141,856],"end":[112,848]}]}]},{"tag":"LineTo","args":[{"point":[111,848]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[79,839],"end":[55.5,815.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,792],"end":[22,759]}]}]},{"tag":"LineTo","args":[{"point":[24,767]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[12,707],"end":[6,644]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,581],"end":[0,516]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,515],"end":[0,514]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,513],"end":[0,512]}]}]},{"tag":"LineTo","args":[{"point":[0,513]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,511],"end":[0,510]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,509],"end":[0,507]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,443],"end":[6,381]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[12,319],"end":[22,265]}]}]},{"tag":"LineTo","args":[{"point":[23,264]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[32,232],"end":[55.5,208.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[79,185],"end":[112,176]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[142,167],"end":[208,163]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,159],"end":[342.5,157]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[410,155],"end":[461,155]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,154],"end":[512,154]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[512,154],"end":[564,155]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[615,155],"end":[683,157]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[751,159],"end":[817,163]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[883,168],"end":[913,176]}]}]},{"tag":"LineTo","args":[{"point":[914,176]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[946,185],"end":[969.5,208.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[993,232],"end":[1002,265]}]}]}]},{"start":[677,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[410,359]}]},{"tag":"LineTo","args":[{"point":[410,666]}]},{"tag":"LineTo","args":[{"point":[677,512]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"error","codePoint":59723},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[470,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[554,575]}]},{"tag":"LineTo","args":[{"point":[470,575]}]}]},{"start":[470,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,661]}]},{"tag":"LineTo","args":[{"point":[554,661]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[470,747]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"error_outline","codePoint":59724},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[470,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,575]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[470,575]}]}]},{"start":[470,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[554,661]}]},{"tag":"LineTo","args":[{"point":[470,661]}]},{"tag":"LineTo","args":[{"point":[470,747]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"warningreport_problem","codePoint":59725},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[470,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,447]}]},{"tag":"LineTo","args":[{"point":[554,447]}]},{"tag":"LineTo","args":[{"point":[554,619]}]},{"tag":"LineTo","args":[{"point":[470,619]}]}]},{"start":[470,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,703]}]},{"tag":"LineTo","args":[{"point":[554,703]}]},{"tag":"LineTo","args":[{"point":[554,789]}]},{"tag":"LineTo","args":[{"point":[470,789]}]}]},{"start":[982,917],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,107]}]},{"tag":"LineTo","args":[{"point":[42,917]}]},{"tag":"LineTo","args":[{"point":[982,917]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"library_addqueueadd_to_photos","codePoint":59726},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,661]}]},{"tag":"LineTo","args":[{"point":[554,661]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[384,491]}]},{"tag":"LineTo","args":[{"point":[384,405]}]},{"tag":"LineTo","args":[{"point":[554,405]}]},{"tag":"LineTo","args":[{"point":[554,235]}]},{"tag":"LineTo","args":[{"point":[640,235]}]},{"tag":"LineTo","args":[{"point":[640,405]}]},{"tag":"LineTo","args":[{"point":[810,405]}]},{"tag":"LineTo","args":[{"point":[810,491]}]},{"tag":"LineTo","args":[{"point":[640,491]}]}]},{"start":[342,107],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,107],"end":[282,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,157],"end":[256,191]}]}]},{"tag":"LineTo","args":[{"point":[256,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,737],"end":[282,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,789],"end":[342,789]}]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,789],"end":[913,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,737],"end":[938,703]}]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,157],"end":[913,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,107],"end":[854,107]}]}]},{"tag":"LineTo","args":[{"point":[342,107]}]}]},{"start":[86,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[86,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,909],"end":[111,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,959],"end":[170,959]}]}]},{"tag":"LineTo","args":[{"point":[768,959]}]},{"tag":"LineTo","args":[{"point":[768,875]}]},{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"LineTo","args":[{"point":[170,277]}]},{"tag":"LineTo","args":[{"point":[86,277]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"library_music","codePoint":59727},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[86,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[86,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,909],"end":[111,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,959],"end":[170,959]}]}]},{"tag":"LineTo","args":[{"point":[768,959]}]},{"tag":"LineTo","args":[{"point":[768,875]}]},{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"LineTo","args":[{"point":[170,277]}]},{"tag":"LineTo","args":[{"point":[86,277]}]}]},{"start":[640,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,555]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,599],"end":[609,630]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,661],"end":[534,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,661],"end":[458,630]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,599],"end":[426,555]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,511],"end":[458,479]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,447],"end":[534,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[570,447],"end":[598,469]}]}]},{"tag":"LineTo","args":[{"point":[598,235]}]},{"tag":"LineTo","args":[{"point":[768,235]}]},{"tag":"LineTo","args":[{"point":[768,319]}]},{"tag":"LineTo","args":[{"point":[640,319]}]}]},{"start":[342,107],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,107],"end":[282,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,157],"end":[256,191]}]}]},{"tag":"LineTo","args":[{"point":[256,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,737],"end":[282,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,789],"end":[342,789]}]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,789],"end":[913,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,737],"end":[938,703]}]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,157],"end":[913,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,107],"end":[854,107]}]}]},{"tag":"LineTo","args":[{"point":[342,107]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"new_releases","codePoint":59728},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[470,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[554,575]}]},{"tag":"LineTo","args":[{"point":[470,575]}]}]},{"start":[470,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,661]}]},{"tag":"LineTo","args":[{"point":[554,661]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[470,747]}]}]},{"start":[878,415],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[892,257]}]},{"tag":"LineTo","args":[{"point":[738,223]}]},{"tag":"LineTo","args":[{"point":[658,87]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[366,87]}]},{"tag":"LineTo","args":[{"point":[286,223]}]},{"tag":"LineTo","args":[{"point":[132,257]}]},{"tag":"LineTo","args":[{"point":[146,413]}]},{"tag":"LineTo","args":[{"point":[42,533]}]},{"tag":"LineTo","args":[{"point":[146,651]}]},{"tag":"LineTo","args":[{"point":[132,809]}]},{"tag":"LineTo","args":[{"point":[286,845]}]},{"tag":"LineTo","args":[{"point":[366,979]}]},{"tag":"LineTo","args":[{"point":[512,917]}]},{"tag":"LineTo","args":[{"point":[658,979]}]},{"tag":"LineTo","args":[{"point":[738,843]}]},{"tag":"LineTo","args":[{"point":[892,809]}]},{"tag":"LineTo","args":[{"point":[878,651]}]},{"tag":"LineTo","args":[{"point":[982,533]}]},{"tag":"LineTo","args":[{"point":[878,415]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"not_interesteddo_not_disturb","codePoint":59729},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[302,263],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[340,233],"end":[402,212]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,653],"end":[782,743]}]}]},{"tag":"LineTo","args":[{"point":[302,263]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,413],"end":[242,323]}]}]},{"tag":"LineTo","args":[{"point":[722,803]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[684,833],"end":[622,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[560,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"pause","codePoint":59730},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,831]}]},{"tag":"LineTo","args":[{"point":[768,235]}]},{"tag":"LineTo","args":[{"point":[598,235]}]},{"tag":"LineTo","args":[{"point":[598,831]}]}]},{"start":[426,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,235]}]},{"tag":"LineTo","args":[{"point":[256,235]}]},{"tag":"LineTo","args":[{"point":[256,831]}]},{"tag":"LineTo","args":[{"point":[426,831]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"pause_circle_filled","codePoint":59731},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,363]}]},{"tag":"LineTo","args":[{"point":[640,363]}]},{"tag":"LineTo","args":[{"point":[640,703]}]},{"tag":"LineTo","args":[{"point":[554,703]}]}]},{"start":[384,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[384,363]}]},{"tag":"LineTo","args":[{"point":[470,363]}]},{"tag":"LineTo","args":[{"point":[470,703]}]},{"tag":"LineTo","args":[{"point":[384,703]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"pause_circle_outline","codePoint":59732},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,363]}]},{"tag":"LineTo","args":[{"point":[554,363]}]},{"tag":"LineTo","args":[{"point":[554,703]}]},{"tag":"LineTo","args":[{"point":[640,703]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[470,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,363]}]},{"tag":"LineTo","args":[{"point":[384,363]}]},{"tag":"LineTo","args":[{"point":[384,703]}]},{"tag":"LineTo","args":[{"point":[470,703]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"play_arrow","codePoint":59733},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[342,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[810,533]}]},{"tag":"LineTo","args":[{"point":[342,235]}]},{"tag":"LineTo","args":[{"point":[342,831]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"play_circle_filled","codePoint":59734},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[426,341],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,533]}]},{"tag":"LineTo","args":[{"point":[426,725]}]},{"tag":"LineTo","args":[{"point":[426,341]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"play_circle_outline","codePoint":59735},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[682,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,341]}]},{"tag":"LineTo","args":[{"point":[426,725]}]},{"tag":"LineTo","args":[{"point":[682,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"repeat","codePoint":59736},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[298,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,619]}]},{"tag":"LineTo","args":[{"point":[128,789]}]},{"tag":"LineTo","args":[{"point":[298,959]}]},{"tag":"LineTo","args":[{"point":[298,831]}]},{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[810,575]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[726,747]}]},{"tag":"LineTo","args":[{"point":[298,747]}]}]},{"start":[726,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,447]}]},{"tag":"LineTo","args":[{"point":[896,277]}]},{"tag":"LineTo","args":[{"point":[726,107]}]},{"tag":"LineTo","args":[{"point":[726,235]}]},{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"LineTo","args":[{"point":[214,491]}]},{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[298,319]}]},{"tag":"LineTo","args":[{"point":[726,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"repeat_one","codePoint":59737},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,405]}]},{"tag":"LineTo","args":[{"point":[426,447]}]},{"tag":"LineTo","args":[{"point":[426,491]}]},{"tag":"LineTo","args":[{"point":[490,491]}]},{"tag":"LineTo","args":[{"point":[490,661]}]},{"tag":"LineTo","args":[{"point":[554,661]}]},{"tag":"LineTo","args":[{"point":[554,405]}]}]},{"start":[298,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,619]}]},{"tag":"LineTo","args":[{"point":[128,789]}]},{"tag":"LineTo","args":[{"point":[298,959]}]},{"tag":"LineTo","args":[{"point":[298,831]}]},{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[810,575]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[726,747]}]},{"tag":"LineTo","args":[{"point":[298,747]}]}]},{"start":[726,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,447]}]},{"tag":"LineTo","args":[{"point":[896,277]}]},{"tag":"LineTo","args":[{"point":[726,107]}]},{"tag":"LineTo","args":[{"point":[726,235]}]},{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"LineTo","args":[{"point":[214,491]}]},{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[298,319]}]},{"tag":"LineTo","args":[{"point":[726,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"replay","codePoint":59738},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,63],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,277]}]},{"tag":"LineTo","args":[{"point":[512,491]}]},{"tag":"LineTo","args":[{"point":[512,319]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,319],"end":[693,394]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,469],"end":[768,575]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,681],"end":[693,756]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,831],"end":[512,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,831],"end":[331,756]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,681],"end":[256,575]}]}]},{"tag":"LineTo","args":[{"point":[170,575]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,717],"end":[271,817]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,917],"end":[512,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,917],"end":[753,817]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,717],"end":[854,575]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,435],"end":[754,335]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,235],"end":[512,235]}]}]},{"tag":"LineTo","args":[{"point":[512,63]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"shuffle","codePoint":59739},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[572,653],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[706,787]}]},{"tag":"LineTo","args":[{"point":[618,875]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"LineTo","args":[{"point":[854,639]}]},{"tag":"LineTo","args":[{"point":[766,727]}]},{"tag":"LineTo","args":[{"point":[632,593]}]},{"tag":"LineTo","args":[{"point":[572,653]}]}]},{"start":[706,279],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,815]}]},{"tag":"LineTo","args":[{"point":[230,875]}]},{"tag":"LineTo","args":[{"point":[766,339]}]},{"tag":"LineTo","args":[{"point":[854,427]}]},{"tag":"LineTo","args":[{"point":[854,191]}]},{"tag":"LineTo","args":[{"point":[618,191]}]},{"tag":"LineTo","args":[{"point":[706,279]}]}]},{"start":[230,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,251]}]},{"tag":"LineTo","args":[{"point":[392,473]}]},{"tag":"LineTo","args":[{"point":[452,413]}]},{"tag":"LineTo","args":[{"point":[230,191]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"skip_next","codePoint":59740},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,789]}]},{"tag":"LineTo","args":[{"point":[768,277]}]},{"tag":"LineTo","args":[{"point":[682,277]}]},{"tag":"LineTo","args":[{"point":[682,789]}]}]},{"start":[618,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[256,277]}]},{"tag":"LineTo","args":[{"point":[256,789]}]},{"tag":"LineTo","args":[{"point":[618,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"skip_previous","codePoint":59741},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,277]}]},{"tag":"LineTo","args":[{"point":[406,533]}]},{"tag":"LineTo","args":[{"point":[768,789]}]}]},{"start":[256,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,789]}]},{"tag":"LineTo","args":[{"point":[342,277]}]},{"tag":"LineTo","args":[{"point":[256,277]}]},{"tag":"LineTo","args":[{"point":[256,789]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"emailmailmarkunreadlocal_post_office","codePoint":59742},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,363]}]},{"tag":"LineTo","args":[{"point":[170,277]}]},{"tag":"LineTo","args":[{"point":[512,491]}]},{"tag":"LineTo","args":[{"point":[854,277]}]},{"tag":"LineTo","args":[{"point":[854,363]}]},{"tag":"LineTo","args":[{"point":[512,575]}]}]},{"start":[170,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,243],"end":[913,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,191],"end":[854,191]}]}]},{"tag":"LineTo","args":[{"point":[170,191]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"vpn_key","codePoint":59743},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[239,593],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[239,593]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,567],"end":[214,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,499],"end":[239,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,447],"end":[298,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[332,447],"end":[358,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,499],"end":[384,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,567],"end":[358,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[332,619],"end":[298,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,619],"end":[239,593]}]}]}]},{"start":[443,327],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[443,327]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,277],"end":[298,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,277],"end":[117,352]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,427],"end":[42,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,639],"end":[117,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,789],"end":[298,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,789],"end":[443,739]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,689],"end":[540,619]}]}]},{"tag":"LineTo","args":[{"point":[726,619]}]},{"tag":"LineTo","args":[{"point":[726,789]}]},{"tag":"LineTo","args":[{"point":[896,789]}]},{"tag":"LineTo","args":[{"point":[896,619]}]},{"tag":"LineTo","args":[{"point":[982,619]}]},{"tag":"LineTo","args":[{"point":[982,447]}]},{"tag":"LineTo","args":[{"point":[540,447]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,377],"end":[443,327]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"add","codePoint":59744},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[810,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[554,235]}]},{"tag":"LineTo","args":[{"point":[470,235]}]},{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[214,491]}]},{"tag":"LineTo","args":[{"point":[214,575]}]},{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[470,831]}]},{"tag":"LineTo","args":[{"point":[554,831]}]},{"tag":"LineTo","args":[{"point":[554,575]}]},{"tag":"LineTo","args":[{"point":[810,575]}]},{"tag":"LineTo","args":[{"point":[810,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"add_box","codePoint":59745},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[298,575]}]},{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[726,491]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[554,575]}]}]},{"start":[214,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,149],"end":[153,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,199],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,867],"end":[153,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,201],"end":[870,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,149],"end":[810,149]}]}]},{"tag":"LineTo","args":[{"point":[214,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"add_circle","codePoint":59746},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[298,575]}]},{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[726,491]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[554,575]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"add_circle_outlinecontrol_point","codePoint":59747},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[470,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[298,575]}]},{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[554,575]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[726,491]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[470,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"block","codePoint":59748},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[302,803],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[302,803]}]},{"tag":"LineTo","args":[{"point":[782,323]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[812,361],"end":[833,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,485],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[392,875],"end":[302,803]}]}]}]},{"start":[271,292],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,292]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[632,191],"end":[722,263]}]}]},{"tag":"LineTo","args":[{"point":[242,743]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[212,705],"end":[191,643]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,581],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"clearclose","codePoint":59749},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[750,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,473]}]},{"tag":"LineTo","args":[{"point":[274,235]}]},{"tag":"LineTo","args":[{"point":[214,295]}]},{"tag":"LineTo","args":[{"point":[452,533]}]},{"tag":"LineTo","args":[{"point":[214,771]}]},{"tag":"LineTo","args":[{"point":[274,831]}]},{"tag":"LineTo","args":[{"point":[512,593]}]},{"tag":"LineTo","args":[{"point":[750,831]}]},{"tag":"LineTo","args":[{"point":[810,771]}]},{"tag":"LineTo","args":[{"point":[572,533]}]},{"tag":"LineTo","args":[{"point":[810,295]}]},{"tag":"LineTo","args":[{"point":[750,235]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"copy","codePoint":59750},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[342,917],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,319]}]},{"tag":"LineTo","args":[{"point":[810,319]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"LineTo","args":[{"point":[342,917]}]}]},{"start":[342,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,235],"end":[282,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,285],"end":[256,319]}]}]},{"tag":"LineTo","args":[{"point":[256,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,951],"end":[282,977]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,1003],"end":[342,1003]}]}]},{"tag":"LineTo","args":[{"point":[810,1003]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,1003],"end":[870,977]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,951],"end":[896,917]}]}]},{"tag":"LineTo","args":[{"point":[896,319]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,285],"end":[870,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,235],"end":[810,235]}]}]},{"tag":"LineTo","args":[{"point":[342,235]}]}]},{"start":[170,63],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,63],"end":[111,89]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,115],"end":[86,149]}]}]},{"tag":"LineTo","args":[{"point":[86,747]}]},{"tag":"LineTo","args":[{"point":[170,747]}]},{"tag":"LineTo","args":[{"point":[170,149]}]},{"tag":"LineTo","args":[{"point":[682,149]}]},{"tag":"LineTo","args":[{"point":[682,63]}]},{"tag":"LineTo","args":[{"point":[170,63]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cut","codePoint":59751},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,491]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"LineTo","args":[{"point":[938,149]}]},{"tag":"LineTo","args":[{"point":[810,149]}]},{"tag":"LineTo","args":[{"point":[554,405]}]}]},{"start":[490,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[490,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,511],"end":[512,511]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[534,511],"end":[534,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[534,555],"end":[512,555]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,555],"end":[490,533]}]}]}]},{"start":[196,850],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[196,850]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,825],"end":[170,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,753],"end":[196,728]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,703],"end":[256,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,703],"end":[316,728]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,753],"end":[342,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,825],"end":[316,850]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,875],"end":[256,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,875],"end":[196,850]}]}]}]},{"start":[196,338],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[196,338]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,313],"end":[170,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,241],"end":[196,216]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,191],"end":[256,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,191],"end":[316,216]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,241],"end":[342,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,313],"end":[316,338]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,363],"end":[256,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,363],"end":[196,338]}]}]}]},{"start":[426,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,207],"end":[376,157]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,107],"end":[256,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[186,107],"end":[136,157]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,207],"end":[86,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,347],"end":[136,397]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[186,447],"end":[256,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,447],"end":[326,433]}]}]},{"tag":"LineTo","args":[{"point":[426,533]}]},{"tag":"LineTo","args":[{"point":[326,633]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,619],"end":[256,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[186,619],"end":[136,669]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,719],"end":[86,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,859],"end":[136,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[186,959],"end":[256,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,959],"end":[376,909]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,859],"end":[426,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,749],"end":[412,719]}]}]},{"tag":"LineTo","args":[{"point":[512,619]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"LineTo","args":[{"point":[938,917]}]},{"tag":"LineTo","args":[{"point":[938,875]}]},{"tag":"LineTo","args":[{"point":[412,347]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,317],"end":[426,277]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"paste","codePoint":59752},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,191]}]},{"tag":"LineTo","args":[{"point":[298,191]}]},{"tag":"LineTo","args":[{"point":[298,319]}]},{"tag":"LineTo","args":[{"point":[726,319]}]},{"tag":"LineTo","args":[{"point":[726,191]}]},{"tag":"LineTo","args":[{"point":[810,191]}]},{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"LineTo","args":[{"point":[214,875]}]}]},{"start":[542,119],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[542,119]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,131],"end":[554,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,167],"end":[542,179]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[494,191],"end":[482,179]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,167],"end":[470,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,131],"end":[482,119]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[494,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,107],"end":[542,119]}]}]}]},{"start":[632,107],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,69],"end":[586,45]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,21],"end":[512,21]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,21],"end":[438,45]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,69],"end":[392,107]}]}]},{"tag":"LineTo","args":[{"point":[214,107]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,107],"end":[154,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,157],"end":[128,191]}]}]},{"tag":"LineTo","args":[{"point":[128,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,909],"end":[154,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,959],"end":[214,959]}]}]},{"tag":"LineTo","args":[{"point":[810,959]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,959],"end":[870,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,909],"end":[896,875]}]}]},{"tag":"LineTo","args":[{"point":[896,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,157],"end":[870,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,107],"end":[810,107]}]}]},{"tag":"LineTo","args":[{"point":[632,107]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"edit","codePoint":59753},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[896,291],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,291]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,273],"end":[884,261]}]}]},{"tag":"LineTo","args":[{"point":[784,161]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[772,149],"end":[754,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,149],"end":[724,161]}]}]},{"tag":"LineTo","args":[{"point":[646,239]}]},{"tag":"LineTo","args":[{"point":[806,399]}]},{"tag":"LineTo","args":[{"point":[884,321]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,309],"end":[896,291]}]}]}]},{"start":[128,917],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,917]}]},{"tag":"LineTo","args":[{"point":[760,445]}]},{"tag":"LineTo","args":[{"point":[600,285]}]},{"tag":"LineTo","args":[{"point":[128,757]}]},{"tag":"LineTo","args":[{"point":[128,917]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"drafts","codePoint":59754},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[160,355],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[864,355]}]},{"tag":"LineTo","args":[{"point":[512,575]}]},{"tag":"LineTo","args":[{"point":[160,355]}]}]},{"start":[898,289],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[898,289]}]},{"tag":"LineTo","args":[{"point":[512,63]}]},{"tag":"LineTo","args":[{"point":[126,289]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,313],"end":[86,363]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,313],"end":[898,289]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"forward","codePoint":59755},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,703]}]},{"tag":"LineTo","args":[{"point":[512,703]}]},{"tag":"LineTo","args":[{"point":[512,875]}]},{"tag":"LineTo","args":[{"point":[854,533]}]},{"tag":"LineTo","args":[{"point":[512,191]}]},{"tag":"LineTo","args":[{"point":[512,363]}]},{"tag":"LineTo","args":[{"point":[170,363]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"remove","codePoint":59756},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[810,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,491]}]},{"tag":"LineTo","args":[{"point":[214,575]}]},{"tag":"LineTo","args":[{"point":[810,575]}]},{"tag":"LineTo","args":[{"point":[810,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"remove_circledo_not_disturb_on","codePoint":59757},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[298,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[726,491]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[298,575]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"remove_circle_outline","codePoint":59758},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[298,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[726,491]}]},{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[298,575]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"send","codePoint":59759},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[982,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[86,149]}]},{"tag":"LineTo","args":[{"point":[86,447]}]},{"tag":"LineTo","args":[{"point":[726,533]}]},{"tag":"LineTo","args":[{"point":[86,619]}]},{"tag":"LineTo","args":[{"point":[86,917]}]},{"tag":"LineTo","args":[{"point":[982,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"undo","codePoint":59760},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[238,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[238,473]}]},{"tag":"LineTo","args":[{"point":[86,319]}]},{"tag":"LineTo","args":[{"point":[86,703]}]},{"tag":"LineTo","args":[{"point":[470,703]}]},{"tag":"LineTo","args":[{"point":[314,549]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[410,469],"end":[534,469]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,469],"end":[735,534]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[824,599],"end":[858,703]}]}]},{"tag":"LineTo","args":[{"point":[958,671]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[912,535],"end":[796,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,363],"end":[534,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[364,363],"end":[238,473]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"save_alt","codePoint":59761},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,149]}]},{"tag":"LineTo","args":[{"point":[470,561]}]},{"tag":"LineTo","args":[{"point":[358,451]}]},{"tag":"LineTo","args":[{"point":[298,511]}]},{"tag":"LineTo","args":[{"point":[512,725]}]},{"tag":"LineTo","args":[{"point":[726,511]}]},{"tag":"LineTo","args":[{"point":[666,451]}]},{"tag":"LineTo","args":[{"point":[554,561]}]},{"tag":"LineTo","args":[{"point":[554,149]}]}]},{"start":[810,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,831]}]},{"tag":"LineTo","args":[{"point":[214,533]}]},{"tag":"LineTo","args":[{"point":[128,533]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,865],"end":[154,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,533]}]},{"tag":"LineTo","args":[{"point":[810,533]}]},{"tag":"LineTo","args":[{"point":[810,831]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"file_copy","codePoint":59762},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,299],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[832,533]}]},{"tag":"LineTo","args":[{"point":[598,533]}]},{"tag":"LineTo","args":[{"point":[598,299]}]}]},{"start":[342,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,235],"end":[282,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,285],"end":[256,319]}]}]},{"tag":"LineTo","args":[{"point":[256,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,951],"end":[281,977]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[306,1003],"end":[340,1003]}]}]},{"tag":"LineTo","args":[{"point":[810,1003]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,1003],"end":[870,977]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,951],"end":[896,917]}]}]},{"tag":"LineTo","args":[{"point":[896,491]}]},{"tag":"LineTo","args":[{"point":[640,235]}]},{"tag":"LineTo","args":[{"point":[342,235]}]}]},{"start":[170,63],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,63],"end":[111,89]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,115],"end":[86,149]}]}]},{"tag":"LineTo","args":[{"point":[86,747]}]},{"tag":"LineTo","args":[{"point":[170,747]}]},{"tag":"LineTo","args":[{"point":[170,149]}]},{"tag":"LineTo","args":[{"point":[682,149]}]},{"tag":"LineTo","args":[{"point":[682,63]}]},{"tag":"LineTo","args":[{"point":[170,63]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"sd_storagesd_card","codePoint":59763},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,191]}]},{"tag":"LineTo","args":[{"point":[768,191]}]},{"tag":"LineTo","args":[{"point":[768,363]}]},{"tag":"LineTo","args":[{"point":[682,363]}]}]},{"start":[554,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,191]}]},{"tag":"LineTo","args":[{"point":[640,191]}]},{"tag":"LineTo","args":[{"point":[640,363]}]},{"tag":"LineTo","args":[{"point":[554,363]}]}]},{"start":[426,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[512,191]}]},{"tag":"LineTo","args":[{"point":[512,363]}]},{"tag":"LineTo","args":[{"point":[426,363]}]}]},{"start":[426,107],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[172,363]}]},{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,909],"end":[196,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,959],"end":[256,959]}]}]},{"tag":"LineTo","args":[{"point":[768,959]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[802,959],"end":[828,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,909],"end":[854,875]}]}]},{"tag":"LineTo","args":[{"point":[854,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,157],"end":[828,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[802,107],"end":[768,107]}]}]},{"tag":"LineTo","args":[{"point":[426,107]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"attach_file","codePoint":59764},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[704,767],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,837],"end":[654,888]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[604,939],"end":[534,939]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,939],"end":[413,888]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,837],"end":[362,767]}]}]},{"tag":"LineTo","args":[{"point":[362,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,191],"end":[394,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,127],"end":[470,127]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,127],"end":[545,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,191],"end":[576,235]}]}]},{"tag":"LineTo","args":[{"point":[576,683]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,701],"end":[564,713]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[552,725],"end":[534,725]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[516,725],"end":[503,713]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,701],"end":[490,683]}]}]},{"tag":"LineTo","args":[{"point":[490,277]}]},{"tag":"LineTo","args":[{"point":[426,277]}]},{"tag":"LineTo","args":[{"point":[426,683]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,727],"end":[458,758]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[490,789],"end":[534,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,789],"end":[609,758]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,727],"end":[640,683]}]}]},{"tag":"LineTo","args":[{"point":[640,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,165],"end":[590,114]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[540,63],"end":[470,63]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[400,63],"end":[349,114]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,165],"end":[298,235]}]}]},{"tag":"LineTo","args":[{"point":[298,767]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,865],"end":[367,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,1003],"end":[534,1003]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[632,1003],"end":[700,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,865],"end":[768,767]}]}]},{"tag":"LineTo","args":[{"point":[768,277]}]},{"tag":"LineTo","args":[{"point":[704,277]}]},{"tag":"LineTo","args":[{"point":[704,767]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"attach_money","codePoint":59765},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[376,395],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[376,395]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[376,359],"end":[407,337]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,315],"end":[490,315]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[594,315],"end":[598,405]}]}]},{"tag":"LineTo","args":[{"point":[692,405]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[690,347],"end":[655,303]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,259],"end":[554,243]}]}]},{"tag":"LineTo","args":[{"point":[554,149]}]},{"tag":"LineTo","args":[{"point":[426,149]}]},{"tag":"LineTo","args":[{"point":[426,241]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,255],"end":[320,296]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[278,337],"end":[278,395]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[278,525],"end":[478,571]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[606,603],"end":[606,675]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[606,705],"end":[579,728]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[552,751],"end":[490,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,751],"end":[364,661]}]}]},{"tag":"LineTo","args":[{"point":[270,661]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[274,727],"end":[317,769]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[360,811],"end":[426,825]}]}]},{"tag":"LineTo","args":[{"point":[426,917]}]},{"tag":"LineTo","args":[{"point":[554,917]}]},{"tag":"LineTo","args":[{"point":[554,825]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[622,813],"end":[663,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,735],"end":[704,673]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,629],"end":[687,597]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[670,565],"end":[638,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[606,523],"end":[577,511]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,499],"end":[504,487]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[376,453],"end":[376,395]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"format_bold","codePoint":59766},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[426,683],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,555]}]},{"tag":"LineTo","args":[{"point":[576,555]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[604,555],"end":[622,574]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,593],"end":[640,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,645],"end":[622,664]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[604,683],"end":[576,683]}]}]},{"tag":"LineTo","args":[{"point":[426,683]}]}]},{"start":[554,299],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,299],"end":[599,318]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,337],"end":[618,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,389],"end":[599,408]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,427],"end":[554,427]}]}]},{"tag":"LineTo","args":[{"point":[426,427]}]},{"tag":"LineTo","args":[{"point":[426,299]}]},{"tag":"LineTo","args":[{"point":[554,299]}]}]},{"start":[736,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,291],"end":[687,241]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[638,191],"end":[566,191]}]}]},{"tag":"LineTo","args":[{"point":[298,191]}]},{"tag":"LineTo","args":[{"point":[298,789]}]},{"tag":"LineTo","args":[{"point":[600,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[668,789],"end":[713,742]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[758,695],"end":[758,627]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[758,523],"end":[666,481]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,435],"end":[736,363]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"format_color_fill","codePoint":59767},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[0,1045],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[1024,1045]}]},{"tag":"LineTo","args":[{"point":[1024,875]}]},{"tag":"LineTo","args":[{"point":[0,875]}]},{"tag":"LineTo","args":[{"point":[0,1045]}]}]},{"start":[788,537],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,561],"end":[747,599]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,637],"end":[726,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,695],"end":[751,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[776,747],"end":[810,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,747],"end":[870,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,695],"end":[896,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,605],"end":[810,511]}]}]},{"tag":"LineTo","args":[{"point":[788,537]}]}]},{"start":[426,243],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[632,447]}]},{"tag":"LineTo","args":[{"point":[222,447]}]},{"tag":"LineTo","args":[{"point":[426,243]}]}]},{"start":[326,21],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[264,81]}]},{"tag":"LineTo","args":[{"point":[366,183]}]},{"tag":"LineTo","args":[{"point":[146,403]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,421],"end":[128,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,475],"end":[146,493]}]}]},{"tag":"LineTo","args":[{"point":[382,727]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[402,747],"end":[426,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[452,747],"end":[472,727]}]}]},{"tag":"LineTo","args":[{"point":[706,493]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,475],"end":[726,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,423],"end":[706,403]}]}]},{"tag":"LineTo","args":[{"point":[326,21]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"music_video","codePoint":59768},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[380,751],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[380,751]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[418,789],"end":[470,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,789],"end":[560,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,715],"end":[598,663]}]}]},{"tag":"LineTo","args":[{"point":[598,363]}]},{"tag":"LineTo","args":[{"point":[726,363]}]},{"tag":"LineTo","args":[{"point":[726,277]}]},{"tag":"LineTo","args":[{"point":[512,277]}]},{"tag":"LineTo","args":[{"point":[512,541]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[484,533],"end":[470,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[418,533],"end":[380,571]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,609],"end":[342,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,713],"end":[380,751]}]}]}]},{"start":[128,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[128,235]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"LineTo","args":[{"point":[896,831]}]},{"tag":"LineTo","args":[{"point":[128,831]}]}]},{"start":[128,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,149],"end":[68,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,201],"end":[42,235]}]}]},{"tag":"LineTo","args":[{"point":[42,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,865],"end":[68,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,917],"end":[128,917]}]}]},{"tag":"LineTo","args":[{"point":[896,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,917],"end":[956,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,865],"end":[982,831]}]}]},{"tag":"LineTo","args":[{"point":[982,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,201],"end":[956,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,149],"end":[896,149]}]}]},{"tag":"LineTo","args":[{"point":[128,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"format_size","codePoint":59769},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[256,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[256,831]}]},{"tag":"LineTo","args":[{"point":[384,831]}]},{"tag":"LineTo","args":[{"point":[384,533]}]},{"tag":"LineTo","args":[{"point":[512,533]}]},{"tag":"LineTo","args":[{"point":[512,405]}]},{"tag":"LineTo","args":[{"point":[128,405]}]},{"tag":"LineTo","args":[{"point":[128,533]}]},{"tag":"LineTo","args":[{"point":[256,533]}]}]},{"start":[384,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,319]}]},{"tag":"LineTo","args":[{"point":[598,831]}]},{"tag":"LineTo","args":[{"point":[726,831]}]},{"tag":"LineTo","args":[{"point":[726,319]}]},{"tag":"LineTo","args":[{"point":[938,319]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"LineTo","args":[{"point":[384,191]}]},{"tag":"LineTo","args":[{"point":[384,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"format_underlined","codePoint":59770},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,917],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[214,831]}]},{"tag":"LineTo","args":[{"point":[214,917]}]}]},{"start":[693,672],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[693,672]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,597],"end":[768,491]}]}]},{"tag":"LineTo","args":[{"point":[768,149]}]},{"tag":"LineTo","args":[{"point":[662,149]}]},{"tag":"LineTo","args":[{"point":[662,491]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[662,553],"end":[618,596]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,639],"end":[512,639]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[450,639],"end":[406,596]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,553],"end":[362,491]}]}]},{"tag":"LineTo","args":[{"point":[362,149]}]},{"tag":"LineTo","args":[{"point":[256,149]}]},{"tag":"LineTo","args":[{"point":[256,491]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,597],"end":[331,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,747],"end":[512,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,747],"end":[693,672]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"insert_chartpollassessment","codePoint":59771},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,575]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[726,747]}]},{"tag":"LineTo","args":[{"point":[640,747]}]}]},{"start":[470,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[470,747]}]}]},{"start":[298,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,447]}]},{"tag":"LineTo","args":[{"point":[384,447]}]},{"tag":"LineTo","args":[{"point":[384,747]}]},{"tag":"LineTo","args":[{"point":[298,747]}]}]},{"start":[214,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,149],"end":[154,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,201],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,865],"end":[154,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,201],"end":[870,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,149],"end":[810,149]}]}]},{"tag":"LineTo","args":[{"point":[214,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"insert_emoticontag_facesmood","codePoint":59772},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[645,726],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[645,726]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,685],"end":[730,619]}]}]},{"tag":"LineTo","args":[{"point":[294,619]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,685],"end":[379,726]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,767],"end":[512,767]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[586,767],"end":[645,726]}]}]}]},{"start":[407,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[407,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,453],"end":[426,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,401],"end":[407,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,363],"end":[362,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,363],"end":[317,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,401],"end":[298,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,453],"end":[317,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,491],"end":[362,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,491],"end":[407,472]}]}]}]},{"start":[707,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[707,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,453],"end":[726,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,401],"end":[707,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,363],"end":[662,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,363],"end":[617,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,401],"end":[598,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,453],"end":[617,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,491],"end":[662,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,491],"end":[707,472]}]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"insert_invitationevent","codePoint":59773},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,363]}]},{"tag":"LineTo","args":[{"point":[810,363]}]},{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[214,831]}]}]},{"start":[682,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,149]}]},{"tag":"LineTo","args":[{"point":[342,63]}]},{"tag":"LineTo","args":[{"point":[256,63]}]},{"tag":"LineTo","args":[{"point":[256,149]}]},{"tag":"LineTo","args":[{"point":[214,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,149],"end":[153,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,201],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,867],"end":[153,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,201],"end":[870,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,149],"end":[810,149]}]}]},{"tag":"LineTo","args":[{"point":[768,149]}]},{"tag":"LineTo","args":[{"point":[768,63]}]},{"tag":"LineTo","args":[{"point":[682,63]}]},{"tag":"LineTo","args":[{"point":[682,149]}]}]},{"start":[512,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,747]}]},{"tag":"LineTo","args":[{"point":[726,747]}]},{"tag":"LineTo","args":[{"point":[726,533]}]},{"tag":"LineTo","args":[{"point":[512,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"image","codePoint":59774},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[470,725],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[618,533]}]},{"tag":"LineTo","args":[{"point":[810,789]}]},{"tag":"LineTo","args":[{"point":[214,789]}]},{"tag":"LineTo","args":[{"point":[362,597]}]},{"tag":"LineTo","args":[{"point":[470,725]}]}]},{"start":[896,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,201],"end":[870,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,149],"end":[810,149]}]}]},{"tag":"LineTo","args":[{"point":[214,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,149],"end":[154,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,201],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,865],"end":[154,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,235]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"publish","codePoint":59775},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[384,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[384,875]}]},{"tag":"LineTo","args":[{"point":[640,875]}]},{"tag":"LineTo","args":[{"point":[640,619]}]},{"tag":"LineTo","args":[{"point":[810,619]}]},{"tag":"LineTo","args":[{"point":[512,319]}]},{"tag":"LineTo","args":[{"point":[214,619]}]},{"tag":"LineTo","args":[{"point":[384,619]}]}]},{"start":[214,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[810,277]}]},{"tag":"LineTo","args":[{"point":[810,191]}]},{"tag":"LineTo","args":[{"point":[214,191]}]},{"tag":"LineTo","args":[{"point":[214,277]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"vertical_align_bottom","codePoint":59776},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,917],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,917]}]},{"tag":"LineTo","args":[{"point":[854,831]}]},{"tag":"LineTo","args":[{"point":[170,831]}]},{"tag":"LineTo","args":[{"point":[170,917]}]}]},{"start":[554,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,149]}]},{"tag":"LineTo","args":[{"point":[470,149]}]},{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[342,575]}]},{"tag":"LineTo","args":[{"point":[512,747]}]},{"tag":"LineTo","args":[{"point":[682,575]}]},{"tag":"LineTo","args":[{"point":[554,575]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"vertical_align_top","codePoint":59777},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,235]}]},{"tag":"LineTo","args":[{"point":[854,149]}]},{"tag":"LineTo","args":[{"point":[170,149]}]},{"tag":"LineTo","args":[{"point":[170,235]}]}]},{"start":[470,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,917]}]},{"tag":"LineTo","args":[{"point":[554,917]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[682,491]}]},{"tag":"LineTo","args":[{"point":[512,319]}]},{"tag":"LineTo","args":[{"point":[342,491]}]},{"tag":"LineTo","args":[{"point":[470,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"monetization_on","codePoint":59778},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[572,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[458,875]}]},{"tag":"LineTo","args":[{"point":[458,791]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[324,761],"end":[318,647]}]}]},{"tag":"LineTo","args":[{"point":[402,647]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[410,727],"end":[516,727]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,727],"end":[595,706]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,685],"end":[618,659]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,593],"end":[504,567]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,523],"end":[326,411]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,357],"end":[363,322]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[400,287],"end":[458,275]}]}]},{"tag":"LineTo","args":[{"point":[458,191]}]},{"tag":"LineTo","args":[{"point":[572,191]}]},{"tag":"LineTo","args":[{"point":[572,275]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[690,305],"end":[694,419]}]}]},{"tag":"LineTo","args":[{"point":[610,419]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[606,339],"end":[516,339]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[468,339],"end":[441,359]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[414,379],"end":[414,409]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[414,463],"end":[526,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,537],"end":[706,659]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,767],"end":[572,793]}]}]},{"tag":"LineTo","args":[{"point":[572,875]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cloud","codePoint":59779},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[713,267],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[713,267]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,191],"end":[346,239]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[270,287],"end":[228,365]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,375],"end":[67,450]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,525],"end":[0,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,725],"end":[75,800]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[150,875],"end":[256,875]}]}]},{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[898,875],"end":[961,812]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,749],"end":[1024,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,577],"end":[966,516]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,455],"end":[826,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,343],"end":[713,267]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cloud_done","codePoint":59780},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[278,597],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[338,537]}]},{"tag":"LineTo","args":[{"point":[426,625]}]},{"tag":"LineTo","args":[{"point":[648,405]}]},{"tag":"LineTo","args":[{"point":[708,465]}]},{"tag":"LineTo","args":[{"point":[426,747]}]},{"tag":"LineTo","args":[{"point":[278,597]}]}]},{"start":[713,267],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[713,267]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,191],"end":[346,239]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[270,287],"end":[228,365]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,375],"end":[67,450]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,525],"end":[0,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,725],"end":[75,800]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[150,875],"end":[256,875]}]}]},{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[898,875],"end":[961,812]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,749],"end":[1024,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,577],"end":[966,516]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,455],"end":[826,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,343],"end":[713,267]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cloud_download","codePoint":59781},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,575]}]},{"tag":"LineTo","args":[{"point":[426,575]}]},{"tag":"LineTo","args":[{"point":[426,405]}]},{"tag":"LineTo","args":[{"point":[598,405]}]},{"tag":"LineTo","args":[{"point":[598,575]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[512,789]}]}]},{"start":[713,267],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[713,267]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,191],"end":[346,239]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[270,287],"end":[228,365]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,375],"end":[67,450]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,525],"end":[0,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,725],"end":[75,800]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[150,875],"end":[256,875]}]}]},{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[898,875],"end":[961,812]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,749],"end":[1024,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,577],"end":[966,516]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,455],"end":[826,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,343],"end":[713,267]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cloud_uploadbackup","codePoint":59782},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,747]}]},{"tag":"LineTo","args":[{"point":[426,575]}]},{"tag":"LineTo","args":[{"point":[298,575]}]},{"tag":"LineTo","args":[{"point":[512,363]}]},{"tag":"LineTo","args":[{"point":[726,575]}]},{"tag":"LineTo","args":[{"point":[598,575]}]},{"tag":"LineTo","args":[{"point":[598,747]}]}]},{"start":[713,267],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[713,267]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,191],"end":[346,239]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[270,287],"end":[228,365]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,375],"end":[67,450]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,525],"end":[0,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,725],"end":[75,800]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[150,875],"end":[256,875]}]}]},{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[898,875],"end":[961,812]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,749],"end":[1024,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,577],"end":[966,516]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[908,455],"end":[826,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,343],"end":[713,267]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"file_downloadget_app","codePoint":59783},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"LineTo","args":[{"point":[810,789]}]},{"tag":"LineTo","args":[{"point":[214,789]}]},{"tag":"LineTo","args":[{"point":[214,875]}]}]},{"start":[640,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,149]}]},{"tag":"LineTo","args":[{"point":[384,149]}]},{"tag":"LineTo","args":[{"point":[384,405]}]},{"tag":"LineTo","args":[{"point":[214,405]}]},{"tag":"LineTo","args":[{"point":[512,703]}]},{"tag":"LineTo","args":[{"point":[810,405]}]},{"tag":"LineTo","args":[{"point":[640,405]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"file_upload","codePoint":59784},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"LineTo","args":[{"point":[810,789]}]},{"tag":"LineTo","args":[{"point":[214,789]}]},{"tag":"LineTo","args":[{"point":[214,875]}]}]},{"start":[640,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,447]}]},{"tag":"LineTo","args":[{"point":[810,447]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[214,447]}]},{"tag":"LineTo","args":[{"point":[384,447]}]},{"tag":"LineTo","args":[{"point":[384,703]}]},{"tag":"LineTo","args":[{"point":[640,703]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"audiotrack","codePoint":59785},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,545],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[476,533],"end":[448,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[368,533],"end":[312,589]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,645],"end":[256,725]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,805],"end":[312,861]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[368,917],"end":[448,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,917],"end":[576,868]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[630,819],"end":[638,747]}]}]},{"tag":"LineTo","args":[{"point":[640,747]}]},{"tag":"LineTo","args":[{"point":[640,277]}]},{"tag":"LineTo","args":[{"point":[810,277]}]},{"tag":"LineTo","args":[{"point":[810,149]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[512,545]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"music_note","codePoint":59786},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,599],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[468,575],"end":[426,575]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[356,575],"end":[306,626]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,677],"end":[256,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,817],"end":[306,867]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[356,917],"end":[426,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[496,917],"end":[547,867]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,817],"end":[598,747]}]}]},{"tag":"LineTo","args":[{"point":[598,319]}]},{"tag":"LineTo","args":[{"point":[768,319]}]},{"tag":"LineTo","args":[{"point":[768,149]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[512,599]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"movie_filter","codePoint":59787},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[642,531]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[642,451]}]},{"tag":"LineTo","args":[{"point":[682,363]}]},{"tag":"LineTo","args":[{"point":[722,451]}]},{"tag":"LineTo","args":[{"point":[810,491]}]},{"tag":"LineTo","args":[{"point":[722,531]}]},{"tag":"LineTo","args":[{"point":[682,619]}]}]},{"start":[426,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[374,671]}]},{"tag":"LineTo","args":[{"point":[256,619]}]},{"tag":"LineTo","args":[{"point":[374,565]}]},{"tag":"LineTo","args":[{"point":[426,447]}]},{"tag":"LineTo","args":[{"point":[480,565]}]},{"tag":"LineTo","args":[{"point":[598,619]}]},{"tag":"LineTo","args":[{"point":[480,671]}]},{"tag":"LineTo","args":[{"point":[426,789]}]}]},{"start":[854,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,319]}]},{"tag":"LineTo","args":[{"point":[640,191]}]},{"tag":"LineTo","args":[{"point":[554,191]}]},{"tag":"LineTo","args":[{"point":[640,319]}]},{"tag":"LineTo","args":[{"point":[512,319]}]},{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[342,191]}]},{"tag":"LineTo","args":[{"point":[426,319]}]},{"tag":"LineTo","args":[{"point":[298,319]}]},{"tag":"LineTo","args":[{"point":[214,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"LineTo","args":[{"point":[768,191]}]},{"tag":"LineTo","args":[{"point":[854,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"local_moviestheaters","codePoint":59788},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,319]}]},{"tag":"LineTo","args":[{"point":[768,319]}]},{"tag":"LineTo","args":[{"point":[768,405]}]},{"tag":"LineTo","args":[{"point":[682,405]}]}]},{"start":[682,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,491]}]},{"tag":"LineTo","args":[{"point":[768,491]}]},{"tag":"LineTo","args":[{"point":[768,575]}]},{"tag":"LineTo","args":[{"point":[682,575]}]}]},{"start":[682,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,661]}]},{"tag":"LineTo","args":[{"point":[768,661]}]},{"tag":"LineTo","args":[{"point":[768,747]}]},{"tag":"LineTo","args":[{"point":[682,747]}]}]},{"start":[256,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[256,319]}]},{"tag":"LineTo","args":[{"point":[342,319]}]},{"tag":"LineTo","args":[{"point":[342,405]}]},{"tag":"LineTo","args":[{"point":[256,405]}]}]},{"start":[256,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[256,491]}]},{"tag":"LineTo","args":[{"point":[342,491]}]},{"tag":"LineTo","args":[{"point":[342,575]}]},{"tag":"LineTo","args":[{"point":[256,575]}]}]},{"start":[256,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[256,661]}]},{"tag":"LineTo","args":[{"point":[342,661]}]},{"tag":"LineTo","args":[{"point":[342,747]}]},{"tag":"LineTo","args":[{"point":[256,747]}]}]},{"start":[768,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,235]}]},{"tag":"LineTo","args":[{"point":[682,149]}]},{"tag":"LineTo","args":[{"point":[342,149]}]},{"tag":"LineTo","args":[{"point":[342,235]}]},{"tag":"LineTo","args":[{"point":[256,235]}]},{"tag":"LineTo","args":[{"point":[256,149]}]},{"tag":"LineTo","args":[{"point":[170,149]}]},{"tag":"LineTo","args":[{"point":[170,917]}]},{"tag":"LineTo","args":[{"point":[256,917]}]},{"tag":"LineTo","args":[{"point":[256,831]}]},{"tag":"LineTo","args":[{"point":[342,831]}]},{"tag":"LineTo","args":[{"point":[342,917]}]},{"tag":"LineTo","args":[{"point":[682,917]}]},{"tag":"LineTo","args":[{"point":[682,831]}]},{"tag":"LineTo","args":[{"point":[768,831]}]},{"tag":"LineTo","args":[{"point":[768,917]}]},{"tag":"LineTo","args":[{"point":[854,917]}]},{"tag":"LineTo","args":[{"point":[854,149]}]},{"tag":"LineTo","args":[{"point":[768,149]}]},{"tag":"LineTo","args":[{"point":[768,235]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_arrow_down","codePoint":59789},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[256,447],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,703]}]},{"tag":"LineTo","args":[{"point":[768,447]}]},{"tag":"LineTo","args":[{"point":[708,387]}]},{"tag":"LineTo","args":[{"point":[512,583]}]},{"tag":"LineTo","args":[{"point":[316,387]}]},{"tag":"LineTo","args":[{"point":[256,447]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_arrow_left","codePoint":59790},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[462,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[658,337]}]},{"tag":"LineTo","args":[{"point":[598,277]}]},{"tag":"LineTo","args":[{"point":[342,533]}]},{"tag":"LineTo","args":[{"point":[598,789]}]},{"tag":"LineTo","args":[{"point":[658,729]}]},{"tag":"LineTo","args":[{"point":[462,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_arrow_right","codePoint":59791},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[426,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,533]}]},{"tag":"LineTo","args":[{"point":[426,277]}]},{"tag":"LineTo","args":[{"point":[366,337]}]},{"tag":"LineTo","args":[{"point":[562,533]}]},{"tag":"LineTo","args":[{"point":[366,729]}]},{"tag":"LineTo","args":[{"point":[426,789]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_arrow_up","codePoint":59792},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,483],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[708,679]}]},{"tag":"LineTo","args":[{"point":[768,619]}]},{"tag":"LineTo","args":[{"point":[512,363]}]},{"tag":"LineTo","args":[{"point":[256,619]}]},{"tag":"LineTo","args":[{"point":[316,679]}]},{"tag":"LineTo","args":[{"point":[512,483]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_backspace","codePoint":59793},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[292,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[444,337]}]},{"tag":"LineTo","args":[{"point":[384,277]}]},{"tag":"LineTo","args":[{"point":[128,533]}]},{"tag":"LineTo","args":[{"point":[384,789]}]},{"tag":"LineTo","args":[{"point":[444,729]}]},{"tag":"LineTo","args":[{"point":[292,575]}]},{"tag":"LineTo","args":[{"point":[896,575]}]},{"tag":"LineTo","args":[{"point":[896,491]}]},{"tag":"LineTo","args":[{"point":[292,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_capslock","codePoint":59794},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,703]}]},{"tag":"LineTo","args":[{"point":[256,703]}]},{"tag":"LineTo","args":[{"point":[256,789]}]},{"tag":"LineTo","args":[{"point":[768,789]}]}]},{"start":[708,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,515]}]},{"tag":"LineTo","args":[{"point":[512,259]}]},{"tag":"LineTo","args":[{"point":[256,515]}]},{"tag":"LineTo","args":[{"point":[316,575]}]},{"tag":"LineTo","args":[{"point":[512,379]}]},{"tag":"LineTo","args":[{"point":[708,575]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_hide","codePoint":59795},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,831]}]},{"tag":"LineTo","args":[{"point":[512,1003]}]},{"tag":"LineTo","args":[{"point":[682,831]}]}]},{"start":[726,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,277]}]},{"tag":"LineTo","args":[{"point":[810,277]}]},{"tag":"LineTo","args":[{"point":[810,363]}]},{"tag":"LineTo","args":[{"point":[726,363]}]}]},{"start":[726,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,405]}]},{"tag":"LineTo","args":[{"point":[810,405]}]},{"tag":"LineTo","args":[{"point":[810,491]}]},{"tag":"LineTo","args":[{"point":[726,491]}]}]},{"start":[598,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,277]}]},{"tag":"LineTo","args":[{"point":[682,277]}]},{"tag":"LineTo","args":[{"point":[682,363]}]},{"tag":"LineTo","args":[{"point":[598,363]}]}]},{"start":[598,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,405]}]},{"tag":"LineTo","args":[{"point":[682,405]}]},{"tag":"LineTo","args":[{"point":[682,491]}]},{"tag":"LineTo","args":[{"point":[598,491]}]}]},{"start":[342,661],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,575]}]},{"tag":"LineTo","args":[{"point":[682,575]}]},{"tag":"LineTo","args":[{"point":[682,661]}]},{"tag":"LineTo","args":[{"point":[342,661]}]}]},{"start":[214,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,277]}]},{"tag":"LineTo","args":[{"point":[298,277]}]},{"tag":"LineTo","args":[{"point":[298,363]}]},{"tag":"LineTo","args":[{"point":[214,363]}]}]},{"start":[214,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,405]}]},{"tag":"LineTo","args":[{"point":[298,405]}]},{"tag":"LineTo","args":[{"point":[298,491]}]},{"tag":"LineTo","args":[{"point":[214,491]}]}]},{"start":[426,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,491]}]},{"tag":"LineTo","args":[{"point":[342,491]}]},{"tag":"LineTo","args":[{"point":[342,405]}]},{"tag":"LineTo","args":[{"point":[426,405]}]}]},{"start":[426,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,363]}]},{"tag":"LineTo","args":[{"point":[342,363]}]},{"tag":"LineTo","args":[{"point":[342,277]}]},{"tag":"LineTo","args":[{"point":[426,277]}]}]},{"start":[554,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[470,405]}]},{"tag":"LineTo","args":[{"point":[554,405]}]}]},{"start":[554,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,363]}]},{"tag":"LineTo","args":[{"point":[470,363]}]},{"tag":"LineTo","args":[{"point":[470,277]}]},{"tag":"LineTo","args":[{"point":[554,277]}]}]},{"start":[170,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,149],"end":[111,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,201],"end":[86,235]}]}]},{"tag":"LineTo","args":[{"point":[86,661]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,695],"end":[111,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,747],"end":[170,747]}]}]},{"tag":"LineTo","args":[{"point":[854,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,747],"end":[913,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,695],"end":[938,661]}]}]},{"tag":"LineTo","args":[{"point":[938,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,201],"end":[913,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,149],"end":[854,149]}]}]},{"tag":"LineTo","args":[{"point":[170,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_tab","codePoint":59796},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[854,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[938,789]}]},{"tag":"LineTo","args":[{"point":[938,277]}]},{"tag":"LineTo","args":[{"point":[854,277]}]},{"tag":"LineTo","args":[{"point":[854,789]}]}]},{"start":[648,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[42,491]}]},{"tag":"LineTo","args":[{"point":[42,575]}]},{"tag":"LineTo","args":[{"point":[648,575]}]},{"tag":"LineTo","args":[{"point":[494,729]}]},{"tag":"LineTo","args":[{"point":[554,789]}]},{"tag":"LineTo","args":[{"point":[810,533]}]},{"tag":"LineTo","args":[{"point":[554,277]}]},{"tag":"LineTo","args":[{"point":[494,337]}]},{"tag":"LineTo","args":[{"point":[648,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_voice","codePoint":59797},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[671,689],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[671,689]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[604,751],"end":[512,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,751],"end":[353,689]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[286,627],"end":[286,533]}]}]},{"tag":"LineTo","args":[{"point":[214,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,641],"end":[289,722]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[364,803],"end":[470,819]}]}]},{"tag":"LineTo","args":[{"point":[470,959]}]},{"tag":"LineTo","args":[{"point":[554,959]}]},{"tag":"LineTo","args":[{"point":[554,819]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[660,803],"end":[735,722]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,641],"end":[810,533]}]}]},{"tag":"LineTo","args":[{"point":[738,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[738,627],"end":[671,689]}]}]}]},{"start":[602,623],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[602,623]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,585],"end":[640,533]}]}]},{"tag":"LineTo","args":[{"point":[640,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,225],"end":[602,187]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,149],"end":[512,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,149],"end":[422,187]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,225],"end":[384,277]}]}]},{"tag":"LineTo","args":[{"point":[384,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,585],"end":[422,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,661],"end":[512,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,661],"end":[602,623]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"laptop_chromebook","codePoint":59798},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,661],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,235]}]},{"tag":"LineTo","args":[{"point":[854,235]}]},{"tag":"LineTo","args":[{"point":[854,661]}]},{"tag":"LineTo","args":[{"point":[170,661]}]}]},{"start":[426,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,747]}]},{"tag":"LineTo","args":[{"point":[598,747]}]},{"tag":"LineTo","args":[{"point":[598,789]}]},{"tag":"LineTo","args":[{"point":[426,789]}]}]},{"start":[938,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[86,149]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"LineTo","args":[{"point":[0,789]}]},{"tag":"LineTo","args":[{"point":[0,875]}]},{"tag":"LineTo","args":[{"point":[1024,875]}]},{"tag":"LineTo","args":[{"point":[1024,789]}]},{"tag":"LineTo","args":[{"point":[938,789]}]},{"tag":"LineTo","args":[{"point":[938,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"laptop_mac","codePoint":59799},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[482,819],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[482,819]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,807],"end":[470,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,771],"end":[482,759]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[494,747],"end":[512,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,747],"end":[542,759]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,771],"end":[554,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,807],"end":[542,819]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,831],"end":[512,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[494,831],"end":[482,819]}]}]}]},{"start":[854,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,703]}]},{"tag":"LineTo","args":[{"point":[170,703]}]},{"tag":"LineTo","args":[{"point":[170,235]}]},{"tag":"LineTo","args":[{"point":[854,235]}]}]},{"start":[913,763],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[913,763]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,737],"end":[938,703]}]}]},{"tag":"LineTo","args":[{"point":[938,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,201],"end":[913,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,149],"end":[854,149]}]}]},{"tag":"LineTo","args":[{"point":[170,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,149],"end":[111,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,201],"end":[86,235]}]}]},{"tag":"LineTo","args":[{"point":[86,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,737],"end":[111,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,789],"end":[170,789]}]}]},{"tag":"LineTo","args":[{"point":[0,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,823],"end":[26,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[52,875],"end":[86,875]}]}]},{"tag":"LineTo","args":[{"point":[938,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[972,875],"end":[998,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,823],"end":[1024,789]}]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,789],"end":[913,763]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"laptop_windows","codePoint":59800},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[854,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,661]}]},{"tag":"LineTo","args":[{"point":[170,661]}]},{"tag":"LineTo","args":[{"point":[170,235]}]},{"tag":"LineTo","args":[{"point":[854,235]}]}]},{"start":[854,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,747],"end":[913,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,695],"end":[938,661]}]}]},{"tag":"LineTo","args":[{"point":[938,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,201],"end":[913,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,149],"end":[854,149]}]}]},{"tag":"LineTo","args":[{"point":[170,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,149],"end":[111,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,201],"end":[86,235]}]}]},{"tag":"LineTo","args":[{"point":[86,661]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,695],"end":[111,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,747],"end":[170,747]}]}]},{"tag":"LineTo","args":[{"point":[170,789]}]},{"tag":"LineTo","args":[{"point":[0,789]}]},{"tag":"LineTo","args":[{"point":[0,875]}]},{"tag":"LineTo","args":[{"point":[1024,875]}]},{"tag":"LineTo","args":[{"point":[1024,789]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"LineTo","args":[{"point":[854,747]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"phone_android","codePoint":59801},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[288,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,191]}]},{"tag":"LineTo","args":[{"point":[736,191]}]},{"tag":"LineTo","args":[{"point":[736,789]}]},{"tag":"LineTo","args":[{"point":[288,789]}]}]},{"start":[426,917],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,875]}]},{"tag":"LineTo","args":[{"point":[598,875]}]},{"tag":"LineTo","args":[{"point":[598,917]}]},{"tag":"LineTo","args":[{"point":[426,917]}]}]},{"start":[342,63],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,63],"end":[252,101]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,139],"end":[214,191]}]}]},{"tag":"LineTo","args":[{"point":[214,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,927],"end":[252,965]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,1003],"end":[342,1003]}]}]},{"tag":"LineTo","args":[{"point":[682,1003]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,1003],"end":[772,965]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,927],"end":[810,875]}]}]},{"tag":"LineTo","args":[{"point":[810,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,139],"end":[772,101]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,63],"end":[682,63]}]}]},{"tag":"LineTo","args":[{"point":[342,63]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"phone_iphone","codePoint":59802},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[298,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,191]}]},{"tag":"LineTo","args":[{"point":[682,191]}]},{"tag":"LineTo","args":[{"point":[682,789]}]},{"tag":"LineTo","args":[{"point":[298,789]}]}]},{"start":[445,940],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[445,940]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,921],"end":[426,895]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,869],"end":[445,850]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,831],"end":[490,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[516,831],"end":[535,850]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,869],"end":[554,895]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,921],"end":[535,940]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[516,959],"end":[490,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[464,959],"end":[445,940]}]}]}]},{"start":[320,63],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,63],"end":[245,95]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,127],"end":[214,171]}]}]},{"tag":"LineTo","args":[{"point":[214,895]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,939],"end":[245,971]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,1003],"end":[320,1003]}]}]},{"tag":"LineTo","args":[{"point":[662,1003]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,1003],"end":[737,971]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,939],"end":[768,895]}]}]},{"tag":"LineTo","args":[{"point":[768,171]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,127],"end":[737,95]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,63],"end":[662,63]}]}]},{"tag":"LineTo","args":[{"point":[320,63]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"color_lenspalette","codePoint":59803},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[701,515],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[701,515]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,497],"end":[682,469]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,441],"end":[701,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[720,405],"end":[746,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[772,405],"end":[791,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,441],"end":[810,469]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,497],"end":[791,515]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[772,533],"end":[746,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[720,533],"end":[701,515]}]}]}]},{"start":[573,344],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[573,344]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,325],"end":[554,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,273],"end":[573,254]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,235],"end":[618,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,235],"end":[663,254]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,273],"end":[682,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,325],"end":[663,344]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,363],"end":[618,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,363],"end":[573,344]}]}]}]},{"start":[361,344],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[361,344]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,325],"end":[342,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,273],"end":[361,254]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,235],"end":[406,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,235],"end":[451,254]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,273],"end":[470,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,325],"end":[451,344]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,363],"end":[406,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,363],"end":[361,344]}]}]}]},{"start":[233,515],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[233,515]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,497],"end":[214,469]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,441],"end":[233,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[252,405],"end":[278,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[304,405],"end":[323,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,441],"end":[342,469]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,497],"end":[323,515]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[304,533],"end":[278,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[252,533],"end":[233,515]}]}]}]},{"start":[240,261],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[240,261]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,373],"end":[128,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,693],"end":[240,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,917],"end":[512,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[540,917],"end":[558,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,881],"end":[576,853]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,829],"end":[560,809]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,789],"end":[544,767]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,741],"end":[562,722]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,703],"end":[608,703]}]}]},{"tag":"LineTo","args":[{"point":[682,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[770,703],"end":[833,641]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,579],"end":[896,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,349],"end":[783,249]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[670,149],"end":[512,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,149],"end":[240,261]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"colorize","codePoint":59804},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,749],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[558,405]}]},{"tag":"LineTo","args":[{"point":[640,487]}]},{"tag":"LineTo","args":[{"point":[296,831]}]},{"tag":"LineTo","args":[{"point":[214,749]}]}]},{"start":[784,161],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[772,149],"end":[754,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,149],"end":[724,161]}]}]},{"tag":"LineTo","args":[{"point":[590,295]}]},{"tag":"LineTo","args":[{"point":[508,213]}]},{"tag":"LineTo","args":[{"point":[448,273]}]},{"tag":"LineTo","args":[{"point":[508,333]}]},{"tag":"LineTo","args":[{"point":[128,715]}]},{"tag":"LineTo","args":[{"point":[128,917]}]},{"tag":"LineTo","args":[{"point":[330,917]}]},{"tag":"LineTo","args":[{"point":[712,537]}]},{"tag":"LineTo","args":[{"point":[772,597]}]},{"tag":"LineTo","args":[{"point":[832,537]}]},{"tag":"LineTo","args":[{"point":[750,455]}]},{"tag":"LineTo","args":[{"point":[884,321]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[914,291],"end":[884,261]}]}]},{"tag":"LineTo","args":[{"point":[784,161]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"navigate_beforechevron_left","codePoint":59805},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,533]}]},{"tag":"LineTo","args":[{"point":[598,789]}]},{"tag":"LineTo","args":[{"point":[658,729]}]},{"tag":"LineTo","args":[{"point":[462,533]}]},{"tag":"LineTo","args":[{"point":[658,337]}]},{"tag":"LineTo","args":[{"point":[598,277]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"navigate_nextchevron_right","codePoint":59806},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[366,337],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[562,533]}]},{"tag":"LineTo","args":[{"point":[366,729]}]},{"tag":"LineTo","args":[{"point":[426,789]}]},{"tag":"LineTo","args":[{"point":[682,533]}]},{"tag":"LineTo","args":[{"point":[426,277]}]},{"tag":"LineTo","args":[{"point":[366,337]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"remove_red_eyevisibility","codePoint":59807},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[422,443],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[422,443]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,481],"end":[384,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,585],"end":[422,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,661],"end":[512,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,661],"end":[602,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,585],"end":[640,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,481],"end":[602,443]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,405],"end":[512,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,405],"end":[422,443]}]}]}]},{"start":[361,684],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[361,684]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,621],"end":[298,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,445],"end":[361,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[424,319],"end":[512,319]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[600,319],"end":[663,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,445],"end":[726,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,621],"end":[663,684]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[600,747],"end":[512,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[424,747],"end":[361,684]}]}]}]},{"start":[226,301],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[226,301]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[98,389],"end":[42,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[98,677],"end":[226,765]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[354,853],"end":[512,853]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[670,853],"end":[798,765]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[926,677],"end":[982,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[926,389],"end":[798,301]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[670,213],"end":[512,213]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[354,213],"end":[226,301]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"tune","codePoint":59808},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[726,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,319]}]},{"tag":"LineTo","args":[{"point":[896,319]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"LineTo","args":[{"point":[726,235]}]},{"tag":"LineTo","args":[{"point":[726,149]}]},{"tag":"LineTo","args":[{"point":[640,149]}]},{"tag":"LineTo","args":[{"point":[640,405]}]},{"tag":"LineTo","args":[{"point":[726,405]}]}]},{"start":[896,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[896,575]}]},{"tag":"LineTo","args":[{"point":[896,491]}]}]},{"start":[298,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[128,491]}]},{"tag":"LineTo","args":[{"point":[128,575]}]},{"tag":"LineTo","args":[{"point":[298,575]}]},{"tag":"LineTo","args":[{"point":[298,661]}]},{"tag":"LineTo","args":[{"point":[384,661]}]},{"tag":"LineTo","args":[{"point":[384,405]}]},{"tag":"LineTo","args":[{"point":[298,405]}]},{"tag":"LineTo","args":[{"point":[298,491]}]}]},{"start":[554,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,831]}]},{"tag":"LineTo","args":[{"point":[896,747]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[554,661]}]},{"tag":"LineTo","args":[{"point":[470,661]}]},{"tag":"LineTo","args":[{"point":[470,917]}]},{"tag":"LineTo","args":[{"point":[554,917]}]},{"tag":"LineTo","args":[{"point":[554,831]}]}]},{"start":[128,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[554,235]}]},{"tag":"LineTo","args":[{"point":[128,235]}]},{"tag":"LineTo","args":[{"point":[128,319]}]}]},{"start":[128,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[384,831]}]},{"tag":"LineTo","args":[{"point":[384,747]}]},{"tag":"LineTo","args":[{"point":[128,747]}]},{"tag":"LineTo","args":[{"point":[128,831]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"add_photo_alternate","codePoint":59809},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[342,661],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,789]}]},{"tag":"LineTo","args":[{"point":[554,619]}]},{"tag":"LineTo","args":[{"point":[726,831]}]},{"tag":"LineTo","args":[{"point":[214,831]}]},{"tag":"LineTo","args":[{"point":[342,661]}]}]},{"start":[682,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,363]}]},{"tag":"LineTo","args":[{"point":[554,235]}]},{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,235],"end":[154,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,285],"end":[128,319]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,865],"end":[154,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[726,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[760,917],"end":[785,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,865],"end":[810,831]}]}]},{"tag":"LineTo","args":[{"point":[810,491]}]},{"tag":"LineTo","args":[{"point":[682,491]}]},{"tag":"LineTo","args":[{"point":[682,363]}]}]},{"start":[938,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[938,235]}]},{"tag":"LineTo","args":[{"point":[810,235]}]},{"tag":"LineTo","args":[{"point":[810,107]}]},{"tag":"LineTo","args":[{"point":[726,107]}]},{"tag":"LineTo","args":[{"point":[726,235]}]},{"tag":"LineTo","args":[{"point":[598,235]}]},{"tag":"LineTo","args":[{"point":[598,319]}]},{"tag":"LineTo","args":[{"point":[726,319]}]},{"tag":"LineTo","args":[{"point":[726,447]}]},{"tag":"LineTo","args":[{"point":[810,447]}]},{"tag":"LineTo","args":[{"point":[810,319]}]},{"tag":"LineTo","args":[{"point":[938,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"image_search","codePoint":59810},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[586,374],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[586,374]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,343],"end":[554,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,255],"end":[586,223]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,191],"end":[662,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,191],"end":[737,223]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,255],"end":[768,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,343],"end":[737,374]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,405],"end":[662,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,405],"end":[586,374]}]}]}]},{"start":[854,299],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,299]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,219],"end":[798,163]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[742,107],"end":[662,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,107],"end":[526,163]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,219],"end":[470,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,379],"end":[525,435]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,491],"end":[660,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[712,491],"end":[762,461]}]}]},{"tag":"LineTo","args":[{"point":[896,593]}]},{"tag":"LineTo","args":[{"point":[956,533]}]},{"tag":"LineTo","args":[{"point":[824,401]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,351],"end":[854,299]}]}]}]},{"start":[552,589],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[436,739]}]},{"tag":"LineTo","args":[{"point":[352,639]}]},{"tag":"LineTo","args":[{"point":[234,789]}]},{"tag":"LineTo","args":[{"point":[704,789]}]},{"tag":"LineTo","args":[{"point":[552,589]}]}]},{"start":[768,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"LineTo","args":[{"point":[170,277]}]},{"tag":"LineTo","args":[{"point":[384,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[386,233],"end":[406,191]}]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,909],"end":[111,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,959],"end":[170,959]}]}]},{"tag":"LineTo","args":[{"point":[768,959]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[802,959],"end":[828,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,909],"end":[854,875]}]}]},{"tag":"LineTo","args":[{"point":[854,661]}]},{"tag":"LineTo","args":[{"point":[768,575]}]},{"tag":"LineTo","args":[{"point":[768,875]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"beenhere","codePoint":59811},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[274,431]}]},{"tag":"LineTo","args":[{"point":[426,583]}]},{"tag":"LineTo","args":[{"point":[750,259]}]},{"tag":"LineTo","args":[{"point":[810,319]}]},{"tag":"LineTo","args":[{"point":[426,703]}]},{"tag":"LineTo","args":[{"point":[214,491]}]}]},{"start":[214,63],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,63],"end":[154,89]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,115],"end":[128,149]}]}]},{"tag":"LineTo","args":[{"point":[128,701]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,743],"end":[166,771]}]}]},{"tag":"LineTo","args":[{"point":[512,1003]}]},{"tag":"LineTo","args":[{"point":[858,771]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,743],"end":[896,701]}]}]},{"tag":"LineTo","args":[{"point":[896,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,115],"end":[870,89]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,63],"end":[810,63]}]}]},{"tag":"LineTo","args":[{"point":[214,63]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"apps","codePoint":59812},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[854,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,703]}]},{"tag":"LineTo","args":[{"point":[682,703]}]},{"tag":"LineTo","args":[{"point":[682,875]}]},{"tag":"LineTo","args":[{"point":[854,875]}]}]},{"start":[854,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,447]}]},{"tag":"LineTo","args":[{"point":[682,447]}]},{"tag":"LineTo","args":[{"point":[682,619]}]},{"tag":"LineTo","args":[{"point":[854,619]}]}]},{"start":[598,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,191]}]},{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[426,363]}]},{"tag":"LineTo","args":[{"point":[598,363]}]}]},{"start":[682,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[854,363]}]},{"tag":"LineTo","args":[{"point":[854,191]}]},{"tag":"LineTo","args":[{"point":[682,191]}]},{"tag":"LineTo","args":[{"point":[682,363]}]}]},{"start":[598,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,447]}]},{"tag":"LineTo","args":[{"point":[426,447]}]},{"tag":"LineTo","args":[{"point":[426,619]}]},{"tag":"LineTo","args":[{"point":[598,619]}]}]},{"start":[342,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,447]}]},{"tag":"LineTo","args":[{"point":[170,447]}]},{"tag":"LineTo","args":[{"point":[170,619]}]},{"tag":"LineTo","args":[{"point":[342,619]}]}]},{"start":[342,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,703]}]},{"tag":"LineTo","args":[{"point":[170,703]}]},{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"LineTo","args":[{"point":[342,875]}]}]},{"start":[598,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,703]}]},{"tag":"LineTo","args":[{"point":[426,703]}]},{"tag":"LineTo","args":[{"point":[426,875]}]},{"tag":"LineTo","args":[{"point":[598,875]}]}]},{"start":[342,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"LineTo","args":[{"point":[170,363]}]},{"tag":"LineTo","args":[{"point":[342,363]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_back","codePoint":59813},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[334,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[572,251]}]},{"tag":"LineTo","args":[{"point":[512,191]}]},{"tag":"LineTo","args":[{"point":[170,533]}]},{"tag":"LineTo","args":[{"point":[512,875]}]},{"tag":"LineTo","args":[{"point":[572,815]}]},{"tag":"LineTo","args":[{"point":[334,575]}]},{"tag":"LineTo","args":[{"point":[854,575]}]},{"tag":"LineTo","args":[{"point":[854,491]}]},{"tag":"LineTo","args":[{"point":[334,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_drop_down","codePoint":59814},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,661],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,447]}]},{"tag":"LineTo","args":[{"point":[298,447]}]},{"tag":"LineTo","args":[{"point":[512,661]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_drop_down_circle","codePoint":59815},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[342,447],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,447]}]},{"tag":"LineTo","args":[{"point":[512,619]}]},{"tag":"LineTo","args":[{"point":[342,447]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_drop_up","codePoint":59816},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[726,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,405]}]},{"tag":"LineTo","args":[{"point":[298,619]}]},{"tag":"LineTo","args":[{"point":[726,619]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_forward","codePoint":59817},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[452,251],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[690,491]}]},{"tag":"LineTo","args":[{"point":[170,491]}]},{"tag":"LineTo","args":[{"point":[170,575]}]},{"tag":"LineTo","args":[{"point":[690,575]}]},{"tag":"LineTo","args":[{"point":[452,815]}]},{"tag":"LineTo","args":[{"point":[512,875]}]},{"tag":"LineTo","args":[{"point":[854,533]}]},{"tag":"LineTo","args":[{"point":[512,191]}]},{"tag":"LineTo","args":[{"point":[452,251]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cancel","codePoint":59818},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[666,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,593]}]},{"tag":"LineTo","args":[{"point":[358,747]}]},{"tag":"LineTo","args":[{"point":[298,687]}]},{"tag":"LineTo","args":[{"point":[452,533]}]},{"tag":"LineTo","args":[{"point":[298,379]}]},{"tag":"LineTo","args":[{"point":[358,319]}]},{"tag":"LineTo","args":[{"point":[512,473]}]},{"tag":"LineTo","args":[{"point":[666,319]}]},{"tag":"LineTo","args":[{"point":[726,379]}]},{"tag":"LineTo","args":[{"point":[572,533]}]},{"tag":"LineTo","args":[{"point":[726,687]}]},{"tag":"LineTo","args":[{"point":[666,747]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"check","codePoint":59819},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[206,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[146,593]}]},{"tag":"LineTo","args":[{"point":[384,831]}]},{"tag":"LineTo","args":[{"point":[896,319]}]},{"tag":"LineTo","args":[{"point":[836,259]}]},{"tag":"LineTo","args":[{"point":[384,711]}]},{"tag":"LineTo","args":[{"point":[206,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"expand_less","codePoint":59820},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[256,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[316,679]}]},{"tag":"LineTo","args":[{"point":[512,483]}]},{"tag":"LineTo","args":[{"point":[708,679]}]},{"tag":"LineTo","args":[{"point":[768,619]}]},{"tag":"LineTo","args":[{"point":[512,363]}]},{"tag":"LineTo","args":[{"point":[256,619]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"expand_more","codePoint":59821},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,583],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[316,387]}]},{"tag":"LineTo","args":[{"point":[256,447]}]},{"tag":"LineTo","args":[{"point":[512,703]}]},{"tag":"LineTo","args":[{"point":[768,447]}]},{"tag":"LineTo","args":[{"point":[708,387]}]},{"tag":"LineTo","args":[{"point":[512,583]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"fullscreen","codePoint":59822},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,319]}]},{"tag":"LineTo","args":[{"point":[726,447]}]},{"tag":"LineTo","args":[{"point":[810,447]}]},{"tag":"LineTo","args":[{"point":[810,235]}]},{"tag":"LineTo","args":[{"point":[598,235]}]},{"tag":"LineTo","args":[{"point":[598,319]}]}]},{"start":[598,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,831]}]},{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[810,619]}]},{"tag":"LineTo","args":[{"point":[726,619]}]},{"tag":"LineTo","args":[{"point":[726,747]}]},{"tag":"LineTo","args":[{"point":[598,747]}]}]},{"start":[298,447],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[298,319]}]},{"tag":"LineTo","args":[{"point":[426,319]}]},{"tag":"LineTo","args":[{"point":[426,235]}]},{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"LineTo","args":[{"point":[214,447]}]},{"tag":"LineTo","args":[{"point":[298,447]}]}]},{"start":[214,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,831]}]},{"tag":"LineTo","args":[{"point":[426,831]}]},{"tag":"LineTo","args":[{"point":[426,747]}]},{"tag":"LineTo","args":[{"point":[298,747]}]},{"tag":"LineTo","args":[{"point":[298,619]}]},{"tag":"LineTo","args":[{"point":[214,619]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"fullscreen_exit","codePoint":59823},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,235]}]},{"tag":"LineTo","args":[{"point":[598,447]}]},{"tag":"LineTo","args":[{"point":[810,447]}]},{"tag":"LineTo","args":[{"point":[810,363]}]},{"tag":"LineTo","args":[{"point":[682,363]}]},{"tag":"LineTo","args":[{"point":[682,235]}]}]},{"start":[682,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,703]}]},{"tag":"LineTo","args":[{"point":[810,703]}]},{"tag":"LineTo","args":[{"point":[810,619]}]},{"tag":"LineTo","args":[{"point":[598,619]}]},{"tag":"LineTo","args":[{"point":[598,831]}]},{"tag":"LineTo","args":[{"point":[682,831]}]}]},{"start":[214,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,447]}]},{"tag":"LineTo","args":[{"point":[426,447]}]},{"tag":"LineTo","args":[{"point":[426,235]}]},{"tag":"LineTo","args":[{"point":[342,235]}]},{"tag":"LineTo","args":[{"point":[342,363]}]},{"tag":"LineTo","args":[{"point":[214,363]}]}]},{"start":[342,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,831]}]},{"tag":"LineTo","args":[{"point":[426,831]}]},{"tag":"LineTo","args":[{"point":[426,619]}]},{"tag":"LineTo","args":[{"point":[214,619]}]},{"tag":"LineTo","args":[{"point":[214,703]}]},{"tag":"LineTo","args":[{"point":[342,703]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"menu","codePoint":59824},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[128,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,363]}]},{"tag":"LineTo","args":[{"point":[896,277]}]},{"tag":"LineTo","args":[{"point":[128,277]}]},{"tag":"LineTo","args":[{"point":[128,363]}]}]},{"start":[896,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,491]}]},{"tag":"LineTo","args":[{"point":[128,491]}]},{"tag":"LineTo","args":[{"point":[128,575]}]},{"tag":"LineTo","args":[{"point":[896,575]}]}]},{"start":[896,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,703]}]},{"tag":"LineTo","args":[{"point":[128,703]}]},{"tag":"LineTo","args":[{"point":[128,789]}]},{"tag":"LineTo","args":[{"point":[896,789]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"keyboard_control","codePoint":59825},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[452,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[452,473]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,499],"end":[426,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,567],"end":[452,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,619],"end":[512,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,619],"end":[572,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,567],"end":[598,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,499],"end":[572,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,447],"end":[512,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,447],"end":[452,473]}]}]}]},{"start":[708,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[708,473]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,499],"end":[682,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,567],"end":[708,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,619],"end":[768,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[802,619],"end":[828,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,567],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,499],"end":[828,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[802,447],"end":[768,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,447],"end":[708,473]}]}]}]},{"start":[196,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[196,473]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,499],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,567],"end":[196,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,619],"end":[256,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,619],"end":[316,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,567],"end":[342,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,499],"end":[316,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,447],"end":[256,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,447],"end":[196,473]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"more_vert","codePoint":59826},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[452,729],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[452,729]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,755],"end":[426,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,823],"end":[452,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,875],"end":[572,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,823],"end":[598,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,755],"end":[572,729]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,703],"end":[512,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,703],"end":[452,729]}]}]}]},{"start":[452,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[452,473]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,499],"end":[426,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,567],"end":[452,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,619],"end":[512,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,619],"end":[572,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,567],"end":[598,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,499],"end":[572,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,447],"end":[512,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,447],"end":[452,473]}]}]}]},{"start":[572,337],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[572,337]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,311],"end":[598,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,243],"end":[572,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,191],"end":[452,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,243],"end":[426,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,311],"end":[452,337]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,363],"end":[512,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,363],"end":[572,337]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"refresh","codePoint":59827},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[643,220],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[643,220]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[272,291]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[172,391],"end":[172,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[172,675],"end":[272,775]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[630,875],"end":[722,803]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[814,731],"end":[842,619]}]}]},{"tag":"LineTo","args":[{"point":[754,619]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[728,689],"end":[657,739]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[586,789],"end":[512,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,789],"end":[331,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,639],"end":[256,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,427],"end":[331,352]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,277],"end":[512,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[616,277],"end":[692,353]}]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[854,491]}]},{"tag":"LineTo","args":[{"point":[854,191]}]},{"tag":"LineTo","args":[{"point":[754,291]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[714,249],"end":[643,220]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"unfold_less","codePoint":59828},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[648,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,327]}]},{"tag":"LineTo","args":[{"point":[376,191]}]},{"tag":"LineTo","args":[{"point":[316,251]}]},{"tag":"LineTo","args":[{"point":[512,447]}]},{"tag":"LineTo","args":[{"point":[708,251]}]},{"tag":"LineTo","args":[{"point":[648,191]}]}]},{"start":[376,875],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,739]}]},{"tag":"LineTo","args":[{"point":[648,875]}]},{"tag":"LineTo","args":[{"point":[708,815]}]},{"tag":"LineTo","args":[{"point":[512,619]}]},{"tag":"LineTo","args":[{"point":[316,815]}]},{"tag":"LineTo","args":[{"point":[376,875]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"unfold_more","codePoint":59829},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[376,661],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[316,721]}]},{"tag":"LineTo","args":[{"point":[512,917]}]},{"tag":"LineTo","args":[{"point":[708,721]}]},{"tag":"LineTo","args":[{"point":[648,661]}]},{"tag":"LineTo","args":[{"point":[512,797]}]},{"tag":"LineTo","args":[{"point":[376,661]}]}]},{"start":[648,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[708,345]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[316,345]}]},{"tag":"LineTo","args":[{"point":[376,405]}]},{"tag":"LineTo","args":[{"point":[512,269]}]},{"tag":"LineTo","args":[{"point":[648,405]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_upward","codePoint":59830},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[230,593],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,355]}]},{"tag":"LineTo","args":[{"point":[470,875]}]},{"tag":"LineTo","args":[{"point":[554,875]}]},{"tag":"LineTo","args":[{"point":[554,355]}]},{"tag":"LineTo","args":[{"point":[792,593]}]},{"tag":"LineTo","args":[{"point":[854,533]}]},{"tag":"LineTo","args":[{"point":[512,191]}]},{"tag":"LineTo","args":[{"point":[170,533]}]},{"tag":"LineTo","args":[{"point":[230,593]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"subdirectory_arrow_left","codePoint":59831},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,661],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,917]}]},{"tag":"LineTo","args":[{"point":[530,857]}]},{"tag":"LineTo","args":[{"point":[376,703]}]},{"tag":"LineTo","args":[{"point":[854,703]}]},{"tag":"LineTo","args":[{"point":[854,191]}]},{"tag":"LineTo","args":[{"point":[768,191]}]},{"tag":"LineTo","args":[{"point":[768,619]}]},{"tag":"LineTo","args":[{"point":[376,619]}]},{"tag":"LineTo","args":[{"point":[530,465]}]},{"tag":"LineTo","args":[{"point":[470,405]}]},{"tag":"LineTo","args":[{"point":[214,661]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"subdirectory_arrow_right","codePoint":59832},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[494,465]}]},{"tag":"LineTo","args":[{"point":[648,619]}]},{"tag":"LineTo","args":[{"point":[256,619]}]},{"tag":"LineTo","args":[{"point":[256,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"LineTo","args":[{"point":[170,703]}]},{"tag":"LineTo","args":[{"point":[648,703]}]},{"tag":"LineTo","args":[{"point":[494,857]}]},{"tag":"LineTo","args":[{"point":[554,917]}]},{"tag":"LineTo","args":[{"point":[810,661]}]},{"tag":"LineTo","args":[{"point":[554,405]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_downward","codePoint":59833},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[794,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,711]}]},{"tag":"LineTo","args":[{"point":[554,191]}]},{"tag":"LineTo","args":[{"point":[470,191]}]},{"tag":"LineTo","args":[{"point":[470,711]}]},{"tag":"LineTo","args":[{"point":[232,473]}]},{"tag":"LineTo","args":[{"point":[170,533]}]},{"tag":"LineTo","args":[{"point":[512,875]}]},{"tag":"LineTo","args":[{"point":[854,533]}]},{"tag":"LineTo","args":[{"point":[794,473]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"first_page","codePoint":59834},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[256,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,789]}]},{"tag":"LineTo","args":[{"point":[342,277]}]},{"tag":"LineTo","args":[{"point":[256,277]}]},{"tag":"LineTo","args":[{"point":[256,789]}]}]},{"start":[590,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[786,337]}]},{"tag":"LineTo","args":[{"point":[726,277]}]},{"tag":"LineTo","args":[{"point":[470,533]}]},{"tag":"LineTo","args":[{"point":[726,789]}]},{"tag":"LineTo","args":[{"point":[786,729]}]},{"tag":"LineTo","args":[{"point":[590,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"last_page","codePoint":59835},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,789]}]},{"tag":"LineTo","args":[{"point":[768,277]}]},{"tag":"LineTo","args":[{"point":[682,277]}]},{"tag":"LineTo","args":[{"point":[682,789]}]}]},{"start":[434,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[238,729]}]},{"tag":"LineTo","args":[{"point":[298,789]}]},{"tag":"LineTo","args":[{"point":[554,533]}]},{"tag":"LineTo","args":[{"point":[298,277]}]},{"tag":"LineTo","args":[{"point":[238,337]}]},{"tag":"LineTo","args":[{"point":[434,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_left","codePoint":59836},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[384,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,747]}]},{"tag":"LineTo","args":[{"point":[598,319]}]},{"tag":"LineTo","args":[{"point":[384,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_right","codePoint":59837},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,319]}]},{"tag":"LineTo","args":[{"point":[426,747]}]},{"tag":"LineTo","args":[{"point":[640,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_back_ios","codePoint":59838},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[422,111],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[0,533]}]},{"tag":"LineTo","args":[{"point":[422,955]}]},{"tag":"LineTo","args":[{"point":[498,879]}]},{"tag":"LineTo","args":[{"point":[152,533]}]},{"tag":"LineTo","args":[{"point":[498,187]}]},{"tag":"LineTo","args":[{"point":[422,111]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"arrow_forward_ios","codePoint":59839},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[588,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[250,869]}]},{"tag":"LineTo","args":[{"point":[342,959]}]},{"tag":"LineTo","args":[{"point":[768,533]}]},{"tag":"LineTo","args":[{"point":[342,107]}]},{"tag":"LineTo","args":[{"point":[250,197]}]},{"tag":"LineTo","args":[{"point":[588,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder_special","codePoint":59840},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,673],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[514,747]}]},{"tag":"LineTo","args":[{"point":[548,605]}]},{"tag":"LineTo","args":[{"point":[438,509]}]},{"tag":"LineTo","args":[{"point":[582,497]}]},{"tag":"LineTo","args":[{"point":[640,363]}]},{"tag":"LineTo","args":[{"point":[698,497]}]},{"tag":"LineTo","args":[{"point":[842,509]}]},{"tag":"LineTo","args":[{"point":[732,605]}]},{"tag":"LineTo","args":[{"point":[766,747]}]},{"tag":"LineTo","args":[{"point":[640,673]}]}]},{"start":[512,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,329],"end":[913,303]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,277],"end":[854,277]}]}]},{"tag":"LineTo","args":[{"point":[512,277]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"priority_high","codePoint":59841},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[426,661],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,661]}]},{"tag":"LineTo","args":[{"point":[598,149]}]},{"tag":"LineTo","args":[{"point":[426,149]}]},{"tag":"LineTo","args":[{"point":[426,661]}]}]},{"start":[451,892],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[451,892]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[476,917],"end":[512,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,917],"end":[573,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,867],"end":[598,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,795],"end":[573,771]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,747],"end":[512,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[476,747],"end":[451,771]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,795],"end":[426,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,867],"end":[451,892]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"notifications","codePoint":59842},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[768,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,391],"end":[717,317]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,243],"end":[576,221]}]}]},{"tag":"LineTo","args":[{"point":[576,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,165],"end":[558,146]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[540,127],"end":[512,127]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[484,127],"end":[466,146]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,165],"end":[448,191]}]}]},{"tag":"LineTo","args":[{"point":[448,221]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,243],"end":[307,317]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,391],"end":[256,491]}]}]},{"tag":"LineTo","args":[{"point":[256,703]}]},{"tag":"LineTo","args":[{"point":[170,789]}]},{"tag":"LineTo","args":[{"point":[170,831]}]},{"tag":"LineTo","args":[{"point":[854,831]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"LineTo","args":[{"point":[768,703]}]},{"tag":"LineTo","args":[{"point":[768,491]}]}]},{"start":[572,934],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[572,934]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,909],"end":[598,875]}]}]},{"tag":"LineTo","args":[{"point":[426,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,911],"end":[451,935]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[476,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,959],"end":[572,934]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"notifications_none","codePoint":59843},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[342,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[342,491]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,409],"end":[388,354]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[434,299],"end":[512,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[590,299],"end":[636,354]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,409],"end":[682,491]}]}]},{"tag":"LineTo","args":[{"point":[682,747]}]},{"tag":"LineTo","args":[{"point":[342,747]}]}]},{"start":[768,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,391],"end":[717,317]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,243],"end":[576,221]}]}]},{"tag":"LineTo","args":[{"point":[576,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,165],"end":[558,146]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[540,127],"end":[512,127]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[484,127],"end":[466,146]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,165],"end":[448,191]}]}]},{"tag":"LineTo","args":[{"point":[448,221]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,243],"end":[307,317]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,391],"end":[256,491]}]}]},{"tag":"LineTo","args":[{"point":[256,703]}]},{"tag":"LineTo","args":[{"point":[170,789]}]},{"tag":"LineTo","args":[{"point":[170,831]}]},{"tag":"LineTo","args":[{"point":[854,831]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"LineTo","args":[{"point":[768,703]}]},{"tag":"LineTo","args":[{"point":[768,491]}]}]},{"start":[572,934],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[572,934]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,909],"end":[598,875]}]}]},{"tag":"LineTo","args":[{"point":[426,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,909],"end":[452,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,959],"end":[572,934]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"person","codePoint":59844},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[287,666],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[287,666]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,713],"end":[170,789]}]}]},{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,713],"end":[737,666]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,619],"end":[512,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,619],"end":[287,666]}]}]}]},{"start":[632,483],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[632,483]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,433],"end":[682,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,293],"end":[632,242]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[442,191],"end":[392,242]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,293],"end":[342,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,433],"end":[392,483]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[442,533],"end":[512,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,533],"end":[632,483]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"public","codePoint":59845},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,703]}]},{"tag":"LineTo","args":[{"point":[640,703]}]},{"tag":"LineTo","args":[{"point":[640,575]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,557],"end":[628,545]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[616,533],"end":[598,533]}]}]},{"tag":"LineTo","args":[{"point":[342,533]}]},{"tag":"LineTo","args":[{"point":[342,447]}]},{"tag":"LineTo","args":[{"point":[426,447]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[444,447],"end":[457,435]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,423],"end":[470,405]}]}]},{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[588,319],"end":[614,294]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,269],"end":[640,235]}]}]},{"tag":"LineTo","args":[{"point":[640,217]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,255],"end":[795,341]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,427],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,667],"end":[764,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[742,703],"end":[682,703]}]}]}]},{"start":[257,759],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[257,759]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,663],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,497],"end":[180,457]}]}]},{"tag":"LineTo","args":[{"point":[384,661]}]},{"tag":"LineTo","args":[{"point":[384,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,737],"end":[410,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,789],"end":[470,789]}]}]},{"tag":"LineTo","args":[{"point":[470,871]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[344,855],"end":[257,759]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"share","codePoint":59846},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[684,739],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[684,739]}]},{"tag":"LineTo","args":[{"point":[380,563]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,543],"end":[384,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,523],"end":[380,503]}]}]},{"tag":"LineTo","args":[{"point":[680,327]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[720,363],"end":[768,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[820,363],"end":[858,325]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,287],"end":[896,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,183],"end":[858,145]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[820,107],"end":[768,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[716,107],"end":[678,145]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,183],"end":[640,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,245],"end":[644,265]}]}]},{"tag":"LineTo","args":[{"point":[344,439]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[306,405],"end":[256,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[204,405],"end":[166,443]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,481],"end":[128,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,585],"end":[166,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[204,661],"end":[256,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[306,661],"end":[344,627]}]}]},{"tag":"LineTo","args":[{"point":[646,803]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,811],"end":[644,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,883],"end":[681,920]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[718,957],"end":[768,957]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[818,957],"end":[855,920]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[892,883],"end":[892,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[892,781],"end":[856,744]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[820,707],"end":[768,707]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[720,707],"end":[684,739]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"sentiment_dissatisfied","codePoint":59847},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[596,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[596,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,743],"end":[617,722]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,701],"end":[640,699]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,661],"end":[512,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[440,661],"end":[384,699]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,721],"end":[406,723]}]}]},{"tag":"LineTo","args":[{"point":[428,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[468,725],"end":[512,725]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[558,725],"end":[596,747]}]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[317,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[317,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,491],"end":[362,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,491],"end":[407,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,453],"end":[426,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,401],"end":[407,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,363],"end":[362,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,363],"end":[317,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,401],"end":[298,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,453],"end":[317,472]}]}]}]},{"start":[617,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[617,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,491],"end":[662,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,491],"end":[707,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,453],"end":[726,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,401],"end":[707,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,363],"end":[662,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,363],"end":[617,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,401],"end":[598,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,453],"end":[617,472]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"sentiment_neutral","codePoint":59848},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[317,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[317,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,491],"end":[362,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,491],"end":[407,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,453],"end":[426,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,401],"end":[407,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,363],"end":[362,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,363],"end":[317,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,401],"end":[298,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,453],"end":[317,472]}]}]}]},{"start":[617,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[617,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,491],"end":[662,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,491],"end":[707,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,453],"end":[726,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,401],"end":[707,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,363],"end":[662,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,363],"end":[617,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,401],"end":[598,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,453],"end":[617,472]}]}]}]},{"start":[384,725],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,725]}]},{"tag":"LineTo","args":[{"point":[640,683]}]},{"tag":"LineTo","args":[{"point":[384,683]}]},{"tag":"LineTo","args":[{"point":[384,725]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"sentiment_satisfied","codePoint":59849},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[428,681],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[428,681]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[410,699],"end":[386,731]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[444,767],"end":[512,767]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,767],"end":[640,731]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[634,725],"end":[596,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,703],"end":[512,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[466,703],"end":[428,681]}]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[317,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[317,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,491],"end":[362,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,491],"end":[407,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,453],"end":[426,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,401],"end":[407,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,363],"end":[362,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,363],"end":[317,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,401],"end":[298,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,453],"end":[317,472]}]}]}]},{"start":[617,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[617,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,491],"end":[662,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,491],"end":[707,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,453],"end":[726,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,401],"end":[707,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,363],"end":[662,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,363],"end":[617,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,401],"end":[598,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,453],"end":[617,472]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"sentiment_very_dissatisfied","codePoint":59850},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[379,660],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[379,660]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,701],"end":[294,767]}]}]},{"tag":"LineTo","args":[{"point":[364,767]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[414,683],"end":[512,683]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[610,683],"end":[660,767]}]}]},{"tag":"LineTo","args":[{"point":[730,767]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,701],"end":[645,660]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[586,619],"end":[512,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,619],"end":[379,660]}]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[317,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[317,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,491],"end":[362,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,491],"end":[407,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,453],"end":[426,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,401],"end":[407,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,363],"end":[362,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,363],"end":[317,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,401],"end":[298,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,453],"end":[317,472]}]}]}]},{"start":[617,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[617,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,491],"end":[662,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,491],"end":[707,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,453],"end":[726,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,401],"end":[707,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,363],"end":[662,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,363],"end":[617,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,401],"end":[598,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,453],"end":[617,472]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"sentiment_very_satisfied","codePoint":59851},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[382,742],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[382,742]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[440,789],"end":[512,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[584,789],"end":[642,742]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,695],"end":[726,619]}]}]},{"tag":"LineTo","args":[{"point":[298,619]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[324,695],"end":[382,742]}]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[317,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[317,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,491],"end":[362,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,491],"end":[407,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,453],"end":[426,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,401],"end":[407,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,363],"end":[362,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,363],"end":[317,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,401],"end":[298,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,453],"end":[317,472]}]}]}]},{"start":[617,472],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[617,472]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,491],"end":[662,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,491],"end":[707,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,453],"end":[726,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,401],"end":[707,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,363],"end":[662,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,363],"end":[617,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,401],"end":[598,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,453],"end":[617,472]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"stargrade","codePoint":59852},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[776,917],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[706,617]}]},{"tag":"LineTo","args":[{"point":[938,415]}]},{"tag":"LineTo","args":[{"point":[632,389]}]},{"tag":"LineTo","args":[{"point":[512,107]}]},{"tag":"LineTo","args":[{"point":[392,389]}]},{"tag":"LineTo","args":[{"point":[86,415]}]},{"tag":"LineTo","args":[{"point":[318,617]}]},{"tag":"LineTo","args":[{"point":[248,917]}]},{"tag":"LineTo","args":[{"point":[512,757]}]},{"tag":"LineTo","args":[{"point":[776,917]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"star_half","codePoint":59853},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,281],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[584,453]}]},{"tag":"LineTo","args":[{"point":[772,469]}]},{"tag":"LineTo","args":[{"point":[630,593]}]},{"tag":"LineTo","args":[{"point":[672,775]}]},{"tag":"LineTo","args":[{"point":[512,679]}]},{"tag":"LineTo","args":[{"point":[512,281]}]}]},{"start":[632,389],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,107]}]},{"tag":"LineTo","args":[{"point":[392,389]}]},{"tag":"LineTo","args":[{"point":[86,415]}]},{"tag":"LineTo","args":[{"point":[318,617]}]},{"tag":"LineTo","args":[{"point":[248,917]}]},{"tag":"LineTo","args":[{"point":[512,757]}]},{"tag":"LineTo","args":[{"point":[776,917]}]},{"tag":"LineTo","args":[{"point":[706,617]}]},{"tag":"LineTo","args":[{"point":[938,415]}]},{"tag":"LineTo","args":[{"point":[632,389]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"star_outline","codePoint":59854},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[352,775],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[394,593]}]},{"tag":"LineTo","args":[{"point":[252,469]}]},{"tag":"LineTo","args":[{"point":[440,453]}]},{"tag":"LineTo","args":[{"point":[512,281]}]},{"tag":"LineTo","args":[{"point":[584,453]}]},{"tag":"LineTo","args":[{"point":[772,469]}]},{"tag":"LineTo","args":[{"point":[630,593]}]},{"tag":"LineTo","args":[{"point":[672,775]}]},{"tag":"LineTo","args":[{"point":[512,679]}]},{"tag":"LineTo","args":[{"point":[352,775]}]}]},{"start":[632,389],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,107]}]},{"tag":"LineTo","args":[{"point":[392,389]}]},{"tag":"LineTo","args":[{"point":[86,415]}]},{"tag":"LineTo","args":[{"point":[318,617]}]},{"tag":"LineTo","args":[{"point":[248,917]}]},{"tag":"LineTo","args":[{"point":[512,757]}]},{"tag":"LineTo","args":[{"point":[776,917]}]},{"tag":"LineTo","args":[{"point":[706,617]}]},{"tag":"LineTo","args":[{"point":[938,415]}]},{"tag":"LineTo","args":[{"point":[632,389]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"account_box","codePoint":59855},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[344,652],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[344,652]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,615],"end":[512,615]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,615],"end":[680,652]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,689],"end":[768,747]}]}]},{"tag":"LineTo","args":[{"point":[768,789]}]},{"tag":"LineTo","args":[{"point":[256,789]}]},{"tag":"LineTo","args":[{"point":[256,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,689],"end":[344,652]}]}]}]},{"start":[602,495],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[602,495]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,533],"end":[512,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,533],"end":[422,495]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,457],"end":[384,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,353],"end":[422,315]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,277],"end":[512,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,277],"end":[602,315]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,353],"end":[640,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,457],"end":[602,495]}]}]}]},{"start":[128,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,867],"end":[153,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,201],"end":[870,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,149],"end":[810,149]}]}]},{"tag":"LineTo","args":[{"point":[214,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,149],"end":[153,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,199],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"account_circle","codePoint":59856},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[369,801],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[369,801]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[294,761],"end":[256,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[258,645],"end":[346,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[434,571],"end":[512,571]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[590,571],"end":[678,609]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[766,647],"end":[768,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,761],"end":[655,801]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,841],"end":[512,841]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[444,841],"end":[369,801]}]}]}]},{"start":[602,273],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[602,273]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,311],"end":[640,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,415],"end":[602,453]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,491],"end":[512,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,491],"end":[422,453]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,415],"end":[384,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,311],"end":[422,273]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,235],"end":[512,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,235],"end":[602,273]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"android-full","codePoint":59857},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,191]}]},{"tag":"LineTo","args":[{"point":[640,191]}]},{"tag":"LineTo","args":[{"point":[640,235]}]},{"tag":"LineTo","args":[{"point":[598,235]}]}]},{"start":[384,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[384,191]}]},{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[426,235]}]},{"tag":"LineTo","args":[{"point":[384,235]}]}]},{"start":[718,57],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,41],"end":[718,27]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,13],"end":[688,27]}]}]},{"tag":"LineTo","args":[{"point":[624,91]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,63],"end":[512,63]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[450,63],"end":[398,91]}]}]},{"tag":"LineTo","args":[{"point":[334,27]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,13],"end":[304,27]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,41],"end":[304,57]}]}]},{"tag":"LineTo","args":[{"point":[360,113]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[316,145],"end":[286,205]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,265],"end":[256,319]}]}]},{"tag":"LineTo","args":[{"point":[768,319]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,189],"end":[662,113]}]}]},{"tag":"LineTo","args":[{"point":[718,57]}]}]},{"start":[829,382],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[829,382]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,401],"end":[810,427]}]}]},{"tag":"LineTo","args":[{"point":[810,725]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,753],"end":[829,771]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[848,789],"end":[874,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[900,789],"end":[919,771]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,753],"end":[938,725]}]}]},{"tag":"LineTo","args":[{"point":[938,427]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,401],"end":[919,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[900,363],"end":[874,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[848,363],"end":[829,382]}]}]}]},{"start":[105,382],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[105,382]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,401],"end":[86,427]}]}]},{"tag":"LineTo","args":[{"point":[86,725]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,753],"end":[105,771]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[124,789],"end":[150,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[176,789],"end":[195,771]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,753],"end":[214,725]}]}]},{"tag":"LineTo","args":[{"point":[214,427]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,401],"end":[195,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[176,363],"end":[150,363]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[124,363],"end":[105,382]}]}]}]},{"start":[268,819],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[268,819]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[280,831],"end":[298,831]}]}]},{"tag":"LineTo","args":[{"point":[342,831]}]},{"tag":"LineTo","args":[{"point":[342,981]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,1009],"end":[361,1027]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,1045],"end":[406,1045]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,1045],"end":[451,1027]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,1009],"end":[470,981]}]}]},{"tag":"LineTo","args":[{"point":[470,831]}]},{"tag":"LineTo","args":[{"point":[554,831]}]},{"tag":"LineTo","args":[{"point":[554,981]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,1009],"end":[573,1027]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,1045],"end":[618,1045]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,1045],"end":[663,1027]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,1009],"end":[682,981]}]}]},{"tag":"LineTo","args":[{"point":[682,831]}]},{"tag":"LineTo","args":[{"point":[726,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[744,831],"end":[756,819]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,807],"end":[768,789]}]}]},{"tag":"LineTo","args":[{"point":[768,363]}]},{"tag":"LineTo","args":[{"point":[256,363]}]},{"tag":"LineTo","args":[{"point":[256,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,807],"end":[268,819]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"autorenew","codePoint":59858},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[738,413],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,473],"end":[768,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,639],"end":[693,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,789],"end":[512,789]}]}]},{"tag":"LineTo","args":[{"point":[512,661]}]},{"tag":"LineTo","args":[{"point":[342,831]}]},{"tag":"LineTo","args":[{"point":[512,1003]}]},{"tag":"LineTo","args":[{"point":[512,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,433],"end":[800,351]}]}]},{"tag":"LineTo","args":[{"point":[738,413]}]}]},{"start":[512,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,235]}]},{"tag":"LineTo","args":[{"point":[512,63]}]},{"tag":"LineTo","args":[{"point":[512,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,633],"end":[224,715]}]}]},{"tag":"LineTo","args":[{"point":[286,653]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,597],"end":[256,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,427],"end":[331,352]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,277],"end":[512,277]}]}]},{"tag":"LineTo","args":[{"point":[512,405]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"cached","codePoint":59859},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[331,352],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[331,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,277],"end":[512,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,277],"end":[632,307]}]}]},{"tag":"LineTo","args":[{"point":[694,245]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[612,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[170,533]}]}]},{"tag":"LineTo","args":[{"point":[42,533]}]},{"tag":"LineTo","args":[{"point":[214,703]}]},{"tag":"LineTo","args":[{"point":[384,533]}]},{"tag":"LineTo","args":[{"point":[256,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,427],"end":[331,352]}]}]}]},{"start":[640,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,639],"end":[693,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,789],"end":[512,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,789],"end":[392,759]}]}]},{"tag":"LineTo","args":[{"point":[330,821]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[412,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[854,533]}]}]},{"tag":"LineTo","args":[{"point":[982,533]}]},{"tag":"LineTo","args":[{"point":[810,363]}]},{"tag":"LineTo","args":[{"point":[640,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"check_circle","codePoint":59860},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[274,473]}]},{"tag":"LineTo","args":[{"point":[426,625]}]},{"tag":"LineTo","args":[{"point":[750,301]}]},{"tag":"LineTo","args":[{"point":[810,363]}]},{"tag":"LineTo","args":[{"point":[426,747]}]},{"tag":"LineTo","args":[{"point":[214,533]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"code","codePoint":59861},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[938,533]}]},{"tag":"LineTo","args":[{"point":[682,277]}]},{"tag":"LineTo","args":[{"point":[622,337]}]},{"tag":"LineTo","args":[{"point":[820,533]}]},{"tag":"LineTo","args":[{"point":[622,729]}]},{"tag":"LineTo","args":[{"point":[682,789]}]}]},{"start":[204,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[402,337]}]},{"tag":"LineTo","args":[{"point":[342,277]}]},{"tag":"LineTo","args":[{"point":[86,533]}]},{"tag":"LineTo","args":[{"point":[342,789]}]},{"tag":"LineTo","args":[{"point":[402,729]}]},{"tag":"LineTo","args":[{"point":[204,533]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"delete","codePoint":59862},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[662,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[618,149]}]},{"tag":"LineTo","args":[{"point":[406,149]}]},{"tag":"LineTo","args":[{"point":[362,191]}]},{"tag":"LineTo","args":[{"point":[214,191]}]},{"tag":"LineTo","args":[{"point":[214,277]}]},{"tag":"LineTo","args":[{"point":[810,277]}]},{"tag":"LineTo","args":[{"point":[810,191]}]},{"tag":"LineTo","args":[{"point":[662,191]}]}]},{"start":[282,891],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[282,891]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,917],"end":[342,917]}]}]},{"tag":"LineTo","args":[{"point":[682,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[716,917],"end":[742,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,865],"end":[768,831]}]}]},{"tag":"LineTo","args":[{"point":[768,319]}]},{"tag":"LineTo","args":[{"point":[256,319]}]},{"tag":"LineTo","args":[{"point":[256,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,865],"end":[282,891]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"exit_to_app","codePoint":59863},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,149],"end":[153,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,199],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,405]}]},{"tag":"LineTo","args":[{"point":[214,405]}]},{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"LineTo","args":[{"point":[810,235]}]},{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[214,831]}]},{"tag":"LineTo","args":[{"point":[214,661]}]},{"tag":"LineTo","args":[{"point":[128,661]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,867],"end":[153,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,201],"end":[870,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,149],"end":[810,149]}]}]},{"tag":"LineTo","args":[{"point":[214,149]}]}]},{"start":[490,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[704,533]}]},{"tag":"LineTo","args":[{"point":[490,319]}]},{"tag":"LineTo","args":[{"point":[430,379]}]},{"tag":"LineTo","args":[{"point":[540,491]}]},{"tag":"LineTo","args":[{"point":[128,491]}]},{"tag":"LineTo","args":[{"point":[128,575]}]},{"tag":"LineTo","args":[{"point":[540,575]}]},{"tag":"LineTo","args":[{"point":[430,687]}]},{"tag":"LineTo","args":[{"point":[490,747]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"extension","codePoint":59864},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[810,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[810,319]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,285],"end":[785,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[760,235],"end":[726,235]}]}]},{"tag":"LineTo","args":[{"point":[554,235]}]},{"tag":"LineTo","args":[{"point":[554,171]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,127],"end":[523,95]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[492,63],"end":[448,63]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,63],"end":[373,95]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,127],"end":[342,171]}]}]},{"tag":"LineTo","args":[{"point":[342,235]}]},{"tag":"LineTo","args":[{"point":[170,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,235],"end":[111,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,285],"end":[86,319]}]}]},{"tag":"LineTo","args":[{"point":[86,481]}]},{"tag":"LineTo","args":[{"point":[150,481]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,481],"end":[231,515]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,549],"end":[264,597]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,645],"end":[231,679]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,713],"end":[150,713]}]}]},{"tag":"LineTo","args":[{"point":[86,713]}]},{"tag":"LineTo","args":[{"point":[86,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,909],"end":[111,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,959],"end":[170,959]}]}]},{"tag":"LineTo","args":[{"point":[332,959]}]},{"tag":"LineTo","args":[{"point":[332,895]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[332,847],"end":[366,814]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[400,781],"end":[448,781]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[496,781],"end":[530,814]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,847],"end":[564,895]}]}]},{"tag":"LineTo","args":[{"point":[564,959]}]},{"tag":"LineTo","args":[{"point":[726,959]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[760,959],"end":[785,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,909],"end":[810,875]}]}]},{"tag":"LineTo","args":[{"point":[810,703]}]},{"tag":"LineTo","args":[{"point":[874,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[918,703],"end":[950,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,641],"end":[982,597]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,553],"end":[950,522]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[918,491],"end":[874,491]}]}]},{"tag":"LineTo","args":[{"point":[810,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"favorite","codePoint":59865},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[574,877],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[712,753],"end":[773,691]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[834,629],"end":[886,545]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,461],"end":[938,383]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,285],"end":[871,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[804,149],"end":[704,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[588,149],"end":[512,239]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,149],"end":[320,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[220,149],"end":[153,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,285],"end":[86,383]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,441],"end":[108,496]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[130,551],"end":[189,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[248,687],"end":[296,733]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[344,779],"end":[450,875]}]}]},{"tag":"LineTo","args":[{"point":[512,931]}]},{"tag":"LineTo","args":[{"point":[574,877]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"favorite_outline","codePoint":59866},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,817],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[508,813]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[412,727],"end":[366,683]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[320,639],"end":[266,579]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[212,519],"end":[191,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,427],"end":[170,383]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,319],"end":[213,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,235],"end":[320,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[370,235],"end":[413,263]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[456,291],"end":[472,335]}]}]},{"tag":"LineTo","args":[{"point":[552,335]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[568,291],"end":[611,263]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,235],"end":[704,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,235],"end":[811,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,319],"end":[854,383]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,427],"end":[833,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[812,519],"end":[758,579]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,639],"end":[658,683]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[612,727],"end":[516,813]}]}]},{"tag":"LineTo","args":[{"point":[512,817]}]}]},{"start":[512,239],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,239]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,149],"end":[320,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[220,149],"end":[153,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,285],"end":[86,383]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,461],"end":[138,545]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[190,629],"end":[251,691]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[312,753],"end":[450,877]}]}]},{"tag":"LineTo","args":[{"point":[512,931]}]},{"tag":"LineTo","args":[{"point":[574,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,779],"end":[728,733]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[776,687],"end":[835,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[894,551],"end":[916,496]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,441],"end":[938,383]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,285],"end":[871,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[804,149],"end":[704,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[588,149],"end":[512,239]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"help","codePoint":59867},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[604,541],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,595],"end":[554,661]}]}]},{"tag":"LineTo","args":[{"point":[470,661]}]},{"tag":"LineTo","args":[{"point":[470,639]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[470,573],"end":[520,519]}]}]},{"tag":"LineTo","args":[{"point":[572,465]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,439],"end":[598,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,371],"end":[572,345]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,319],"end":[512,319]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,319],"end":[452,345]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,371],"end":[426,405]}]}]},{"tag":"LineTo","args":[{"point":[342,405]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,335],"end":[392,285]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[442,235],"end":[512,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,235],"end":[632,285]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,335],"end":[682,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,461],"end":[642,501]}]}]},{"tag":"LineTo","args":[{"point":[604,541]}]}]},{"start":[470,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[554,831]}]},{"tag":"LineTo","args":[{"point":[470,831]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"highlight_remove","codePoint":59868},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[512,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[402,363]}]},{"tag":"LineTo","args":[{"point":[342,423]}]},{"tag":"LineTo","args":[{"point":[452,533]}]},{"tag":"LineTo","args":[{"point":[342,643]}]},{"tag":"LineTo","args":[{"point":[402,703]}]},{"tag":"LineTo","args":[{"point":[512,593]}]},{"tag":"LineTo","args":[{"point":[622,703]}]},{"tag":"LineTo","args":[{"point":[682,643]}]},{"tag":"LineTo","args":[{"point":[572,533]}]},{"tag":"LineTo","args":[{"point":[682,423]}]},{"tag":"LineTo","args":[{"point":[622,363]}]},{"tag":"LineTo","args":[{"point":[512,473]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"historyrestore","codePoint":59869},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[694,685]}]},{"tag":"LineTo","args":[{"point":[726,633]}]},{"tag":"LineTo","args":[{"point":[576,543]}]},{"tag":"LineTo","args":[{"point":[576,363]}]},{"tag":"LineTo","args":[{"point":[512,363]}]},{"tag":"LineTo","args":[{"point":[512,575]}]}]},{"start":[283,261],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[283,261]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,373],"end":[170,533]}]}]},{"tag":"LineTo","args":[{"point":[42,533]}]},{"tag":"LineTo","args":[{"point":[208,699]}]},{"tag":"LineTo","args":[{"point":[212,705]}]},{"tag":"LineTo","args":[{"point":[384,533]}]},{"tag":"LineTo","args":[{"point":[256,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,409],"end":[343,322]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[430,235],"end":[554,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[678,235],"end":[766,322]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,409],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,657],"end":[766,744]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[678,831],"end":[554,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,831],"end":[344,743]}]}]},{"tag":"LineTo","args":[{"point":[284,805]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[330,851],"end":[409,884]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[488,917],"end":[554,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[712,917],"end":[825,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,693],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,373],"end":[825,261]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[712,149],"end":[554,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,149],"end":[283,261]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"home","codePoint":59870},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[426,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,619]}]},{"tag":"LineTo","args":[{"point":[598,875]}]},{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"LineTo","args":[{"point":[810,533]}]},{"tag":"LineTo","args":[{"point":[938,533]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[86,533]}]},{"tag":"LineTo","args":[{"point":[214,533]}]},{"tag":"LineTo","args":[{"point":[214,875]}]},{"tag":"LineTo","args":[{"point":[426,875]}]},{"tag":"LineTo","args":[{"point":[426,619]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"httpslock","codePoint":59871},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[380,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[380,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,223],"end":[419,184]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[458,145],"end":[512,145]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[566,145],"end":[605,184]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,223],"end":[644,277]}]}]},{"tag":"LineTo","args":[{"point":[644,363]}]},{"tag":"LineTo","args":[{"point":[380,363]}]}]},{"start":[452,721],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[452,721]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,695],"end":[426,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,627],"end":[452,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,575],"end":[512,575]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,575],"end":[572,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,627],"end":[598,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,695],"end":[572,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,747],"end":[512,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,747],"end":[452,721]}]}]}]},{"start":[726,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[726,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,189],"end":[663,126]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[600,63],"end":[512,63]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[424,63],"end":[361,126]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,189],"end":[298,277]}]}]},{"tag":"LineTo","args":[{"point":[298,363]}]},{"tag":"LineTo","args":[{"point":[256,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,363],"end":[196,388]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,413],"end":[170,447]}]}]},{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,909],"end":[196,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,959],"end":[256,959]}]}]},{"tag":"LineTo","args":[{"point":[768,959]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[802,959],"end":[828,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,909],"end":[854,875]}]}]},{"tag":"LineTo","args":[{"point":[854,447]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,413],"end":[828,388]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[802,363],"end":[768,363]}]}]},{"tag":"LineTo","args":[{"point":[726,363]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"info","codePoint":59872},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[470,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[554,405]}]},{"tag":"LineTo","args":[{"point":[470,405]}]}]},{"start":[470,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[470,747]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"info_outline","codePoint":59873},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[470,405]}]},{"tag":"LineTo","args":[{"point":[554,405]}]}]},{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[554,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,491]}]},{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[554,747]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"input","codePoint":59874},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,363]}]},{"tag":"LineTo","args":[{"point":[470,491]}]},{"tag":"LineTo","args":[{"point":[42,491]}]},{"tag":"LineTo","args":[{"point":[42,575]}]},{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[470,703]}]},{"tag":"LineTo","args":[{"point":[640,533]}]}]},{"start":[128,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,149],"end":[68,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,201],"end":[42,235]}]}]},{"tag":"LineTo","args":[{"point":[42,405]}]},{"tag":"LineTo","args":[{"point":[128,405]}]},{"tag":"LineTo","args":[{"point":[128,233]}]},{"tag":"LineTo","args":[{"point":[896,233]}]},{"tag":"LineTo","args":[{"point":[896,833]}]},{"tag":"LineTo","args":[{"point":[128,833]}]},{"tag":"LineTo","args":[{"point":[128,661]}]},{"tag":"LineTo","args":[{"point":[42,661]}]},{"tag":"LineTo","args":[{"point":[42,833]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,867],"end":[68,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,917],"end":[128,917]}]}]},{"tag":"LineTo","args":[{"point":[896,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,917],"end":[956,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,867],"end":[982,833]}]}]},{"tag":"LineTo","args":[{"point":[982,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,199],"end":[957,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[932,149],"end":[896,149]}]}]},{"tag":"LineTo","args":[{"point":[128,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"label","codePoint":59875},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,235]}]},{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,235],"end":[154,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,285],"end":[128,319]}]}]},{"tag":"LineTo","args":[{"point":[128,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,781],"end":[154,806]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,831],"end":[214,831]}]}]},{"tag":"LineTo","args":[{"point":[682,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,831],"end":[752,795]}]}]},{"tag":"LineTo","args":[{"point":[938,533]}]},{"tag":"LineTo","args":[{"point":[752,271]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,235],"end":[682,235]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"label_outline","codePoint":59876},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,747],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,319]}]},{"tag":"LineTo","args":[{"point":[682,319]}]},{"tag":"LineTo","args":[{"point":[834,533]}]},{"tag":"LineTo","args":[{"point":[682,747]}]},{"tag":"LineTo","args":[{"point":[214,747]}]}]},{"start":[682,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,235]}]},{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,235],"end":[154,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,285],"end":[128,319]}]}]},{"tag":"LineTo","args":[{"point":[128,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,781],"end":[154,806]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,831],"end":[214,831]}]}]},{"tag":"LineTo","args":[{"point":[682,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,831],"end":[752,795]}]}]},{"tag":"LineTo","args":[{"point":[938,533]}]},{"tag":"LineTo","args":[{"point":[752,271]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,235],"end":[682,235]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"perm_media","codePoint":59877},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[490,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,597]}]},{"tag":"LineTo","args":[{"point":[746,469]}]},{"tag":"LineTo","args":[{"point":[896,661]}]},{"tag":"LineTo","args":[{"point":[298,661]}]},{"tag":"LineTo","args":[{"point":[490,405]}]}]},{"start":[598,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,107]}]},{"tag":"LineTo","args":[{"point":[256,107]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,107],"end":[197,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[172,157],"end":[172,191]}]}]},{"tag":"LineTo","args":[{"point":[170,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,737],"end":[196,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,789],"end":[256,789]}]}]},{"tag":"LineTo","args":[{"point":[938,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[972,789],"end":[998,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,737],"end":[1024,703]}]}]},{"tag":"LineTo","args":[{"point":[1024,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,243],"end":[998,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[972,191],"end":[938,191]}]}]},{"tag":"LineTo","args":[{"point":[598,191]}]}]},{"start":[0,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[0,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,909],"end":[26,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[52,959],"end":[86,959]}]}]},{"tag":"LineTo","args":[{"point":[854,959]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"LineTo","args":[{"point":[86,875]}]},{"tag":"LineTo","args":[{"point":[86,277]}]},{"tag":"LineTo","args":[{"point":[0,277]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"power_settings_new","codePoint":59878},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[700,303],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[746,339],"end":[778,407]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,475],"end":[810,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,657],"end":[723,744]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,831],"end":[512,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,831],"end":[301,744]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,657],"end":[214,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,475],"end":[246,407]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[278,339],"end":[324,301]}]}]},{"tag":"LineTo","args":[{"point":[264,241]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[208,289],"end":[168,375]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,461],"end":[128,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,693],"end":[240,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,917],"end":[512,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,917],"end":[784,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,693],"end":[896,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,357],"end":[760,241]}]}]},{"tag":"LineTo","args":[{"point":[700,303]}]}]},{"start":[470,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[554,575]}]},{"tag":"LineTo","args":[{"point":[554,149]}]},{"tag":"LineTo","args":[{"point":[470,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"search","codePoint":59879},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[270,563],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[270,563]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,507],"end":[214,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,347],"end":[270,291]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,235],"end":[406,235]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[486,235],"end":[542,291]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,347],"end":[598,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,507],"end":[542,563]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[486,619],"end":[406,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,619],"end":[270,563]}]}]}]},{"start":[628,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[616,607]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[642,575],"end":[662,522]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,469],"end":[682,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,311],"end":[602,230]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,149],"end":[406,149]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,149],"end":[209,230]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,311],"end":[128,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,543],"end":[209,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,703],"end":[406,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[510,703],"end":[586,637]}]}]},{"tag":"LineTo","args":[{"point":[598,649]}]},{"tag":"LineTo","args":[{"point":[598,683]}]},{"tag":"LineTo","args":[{"point":[810,895]}]},{"tag":"LineTo","args":[{"point":[874,831]}]},{"tag":"LineTo","args":[{"point":[662,619]}]},{"tag":"LineTo","args":[{"point":[628,619]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"settings","codePoint":59880},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[406,639],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[406,639]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,595],"end":[362,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[362,471],"end":[406,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[450,383],"end":[512,383]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,383],"end":[618,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[662,471],"end":[662,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[662,595],"end":[618,639]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,683],"end":[512,683]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[450,683],"end":[406,639]}]}]}]},{"start":[832,533],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[832,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,505],"end":[830,491]}]}]},{"tag":"LineTo","args":[{"point":[920,421]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[934,411],"end":[924,393]}]}]},{"tag":"LineTo","args":[{"point":[838,245]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[830,231],"end":[812,237]}]}]},{"tag":"LineTo","args":[{"point":[706,279]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,253],"end":[634,237]}]}]},{"tag":"LineTo","args":[{"point":[618,125]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,107],"end":[598,107]}]}]},{"tag":"LineTo","args":[{"point":[426,107]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[410,107],"end":[406,125]}]}]},{"tag":"LineTo","args":[{"point":[390,237]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[360,249],"end":[318,279]}]}]},{"tag":"LineTo","args":[{"point":[212,237]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[194,231],"end":[186,245]}]}]},{"tag":"LineTo","args":[{"point":[100,393]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[90,411],"end":[104,421]}]}]},{"tag":"LineTo","args":[{"point":[194,491]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,505],"end":[192,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[192,561],"end":[194,575]}]}]},{"tag":"LineTo","args":[{"point":[104,645]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[90,655],"end":[100,673]}]}]},{"tag":"LineTo","args":[{"point":[186,821]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[194,835],"end":[212,829]}]}]},{"tag":"LineTo","args":[{"point":[318,787]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,813],"end":[390,829]}]}]},{"tag":"LineTo","args":[{"point":[406,941]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[410,959],"end":[426,959]}]}]},{"tag":"LineTo","args":[{"point":[598,959]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,959],"end":[618,941]}]}]},{"tag":"LineTo","args":[{"point":[634,829]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[664,817],"end":[706,787]}]}]},{"tag":"LineTo","args":[{"point":[812,829]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[830,835],"end":[838,821]}]}]},{"tag":"LineTo","args":[{"point":[924,673]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[934,655],"end":[920,645]}]}]},{"tag":"LineTo","args":[{"point":[830,575]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,561],"end":[832,533]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"settings_applications","codePoint":59881},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[734,563],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[734,563]}]},{"tag":"LineTo","args":[{"point":[798,611]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[808,619],"end":[800,631]}]}]},{"tag":"LineTo","args":[{"point":[740,733]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,745],"end":[722,741]}]}]},{"tag":"LineTo","args":[{"point":[648,711]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[630,725],"end":[598,739]}]}]},{"tag":"LineTo","args":[{"point":[586,819]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[584,831],"end":[572,831]}]}]},{"tag":"LineTo","args":[{"point":[452,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[442,831],"end":[438,819]}]}]},{"tag":"LineTo","args":[{"point":[426,741]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[402,731],"end":[376,711]}]}]},{"tag":"LineTo","args":[{"point":[302,741]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,745],"end":[284,735]}]}]},{"tag":"LineTo","args":[{"point":[224,631]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,629],"end":[222,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,613],"end":[226,611]}]}]},{"tag":"LineTo","args":[{"point":[290,563]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,553],"end":[288,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,513],"end":[290,503]}]}]},{"tag":"LineTo","args":[{"point":[226,455]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,453],"end":[222,443]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[222,437],"end":[224,435]}]}]},{"tag":"LineTo","args":[{"point":[284,333]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[292,321],"end":[302,325]}]}]},{"tag":"LineTo","args":[{"point":[376,355]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[394,341],"end":[426,327]}]}]},{"tag":"LineTo","args":[{"point":[438,247]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[440,235],"end":[452,235]}]}]},{"tag":"LineTo","args":[{"point":[572,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,235],"end":[586,247]}]}]},{"tag":"LineTo","args":[{"point":[598,325]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[622,335],"end":[648,355]}]}]},{"tag":"LineTo","args":[{"point":[722,325]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,321],"end":[740,331]}]}]},{"tag":"LineTo","args":[{"point":[800,435]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[808,447],"end":[798,455]}]}]},{"tag":"LineTo","args":[{"point":[734,503]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,513],"end":[736,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,553],"end":[734,563]}]}]}]},{"start":[214,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,149],"end":[153,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,199],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,867],"end":[153,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[846,917],"end":[871,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,867],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,235]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,199],"end":[871,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[846,149],"end":[810,149]}]}]},{"tag":"LineTo","args":[{"point":[214,149]}]}]},{"start":[452,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[452,473]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,499],"end":[426,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,567],"end":[452,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,619],"end":[512,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,619],"end":[572,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,567],"end":[598,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[598,499],"end":[572,473]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,447],"end":[512,447]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,447],"end":[452,473]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"shop","codePoint":59882},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[384,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[704,575]}]},{"tag":"LineTo","args":[{"point":[384,789]}]},{"tag":"LineTo","args":[{"point":[384,405]}]}]},{"start":[598,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,277]}]},{"tag":"LineTo","args":[{"point":[426,277]}]},{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[598,191]}]}]},{"start":[682,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,155],"end":[658,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[634,107],"end":[598,107]}]}]},{"tag":"LineTo","args":[{"point":[426,107]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[390,107],"end":[366,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,155],"end":[342,191]}]}]},{"tag":"LineTo","args":[{"point":[342,277]}]},{"tag":"LineTo","args":[{"point":[86,277]}]},{"tag":"LineTo","args":[{"point":[86,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,867],"end":[110,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,917],"end":[170,917]}]}]},{"tag":"LineTo","args":[{"point":[854,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,917],"end":[914,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,867],"end":[938,831]}]}]},{"tag":"LineTo","args":[{"point":[938,277]}]},{"tag":"LineTo","args":[{"point":[682,277]}]},{"tag":"LineTo","args":[{"point":[682,191]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"spellcheck","codePoint":59883},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[576,861],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[420,703]}]},{"tag":"LineTo","args":[{"point":[360,763]}]},{"tag":"LineTo","args":[{"point":[576,981]}]},{"tag":"LineTo","args":[{"point":[982,575]}]},{"tag":"LineTo","args":[{"point":[922,515]}]},{"tag":"LineTo","args":[{"point":[576,861]}]}]},{"start":[362,255],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[450,491]}]},{"tag":"LineTo","args":[{"point":[274,491]}]},{"tag":"LineTo","args":[{"point":[362,255]}]}]},{"start":[620,703],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[402,149]}]},{"tag":"LineTo","args":[{"point":[322,149]}]},{"tag":"LineTo","args":[{"point":[104,703]}]},{"tag":"LineTo","args":[{"point":[194,703]}]},{"tag":"LineTo","args":[{"point":[242,575]}]},{"tag":"LineTo","args":[{"point":[482,575]}]},{"tag":"LineTo","args":[{"point":[532,703]}]},{"tag":"LineTo","args":[{"point":[620,703]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"stars","codePoint":59884},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,681],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[332,789]}]},{"tag":"LineTo","args":[{"point":[380,583]}]},{"tag":"LineTo","args":[{"point":[220,445]}]},{"tag":"LineTo","args":[{"point":[430,429]}]},{"tag":"LineTo","args":[{"point":[512,235]}]},{"tag":"LineTo","args":[{"point":[594,427]}]},{"tag":"LineTo","args":[{"point":[804,445]}]},{"tag":"LineTo","args":[{"point":[644,583]}]},{"tag":"LineTo","args":[{"point":[692,789]}]},{"tag":"LineTo","args":[{"point":[512,681]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"translate","codePoint":59885},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[746,561],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[816,747]}]},{"tag":"LineTo","args":[{"point":[678,747]}]},{"tag":"LineTo","args":[{"point":[746,561]}]}]},{"start":[704,447],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,959]}]},{"tag":"LineTo","args":[{"point":[598,959]}]},{"tag":"LineTo","args":[{"point":[646,831]}]},{"tag":"LineTo","args":[{"point":[848,831]}]},{"tag":"LineTo","args":[{"point":[896,959]}]},{"tag":"LineTo","args":[{"point":[982,959]}]},{"tag":"LineTo","args":[{"point":[790,447]}]},{"tag":"LineTo","args":[{"point":[704,447]}]}]},{"start":[440,557],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[442,555]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[488,503],"end":[534,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,341],"end":[600,277]}]}]},{"tag":"LineTo","args":[{"point":[726,277]}]},{"tag":"LineTo","args":[{"point":[726,191]}]},{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[426,107]}]},{"tag":"LineTo","args":[{"point":[342,107]}]},{"tag":"LineTo","args":[{"point":[342,191]}]},{"tag":"LineTo","args":[{"point":[42,191]}]},{"tag":"LineTo","args":[{"point":[42,277]}]},{"tag":"LineTo","args":[{"point":[520,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[476,403],"end":[384,505]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[328,443],"end":[286,363]}]}]},{"tag":"LineTo","args":[{"point":[200,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[248,469],"end":[328,557]}]}]},{"tag":"LineTo","args":[{"point":[110,771]}]},{"tag":"LineTo","args":[{"point":[170,831]}]},{"tag":"LineTo","args":[{"point":[384,619]}]},{"tag":"LineTo","args":[{"point":[516,751]}]},{"tag":"LineTo","args":[{"point":[550,663]}]},{"tag":"LineTo","args":[{"point":[440,557]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"visibility_off","codePoint":59886},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,541],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,481],"end":[602,443]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,405],"end":[512,405]}]}]},{"tag":"LineTo","args":[{"point":[506,405]}]},{"tag":"LineTo","args":[{"point":[640,541]}]}]},{"start":[388,505],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,521],"end":[384,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,585],"end":[422,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,661],"end":[512,661]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[524,661],"end":[540,657]}]}]},{"tag":"LineTo","args":[{"point":[606,723]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[558,747],"end":[512,747]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[424,747],"end":[361,684]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,621],"end":[298,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[298,487],"end":[322,439]}]}]},{"tag":"LineTo","args":[{"point":[388,505]}]}]},{"start":[135,253],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[135,253]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,297],"end":[202,321]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[158,355],"end":[111,418]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[64,481],"end":[42,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[98,677],"end":[226,765]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[354,853],"end":[512,853]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[612,853],"end":[698,817]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,845],"end":[779,897]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,949],"end":[842,959]}]}]},{"tag":"LineTo","args":[{"point":[896,905]}]},{"tag":"LineTo","args":[{"point":[140,149]}]},{"tag":"LineTo","args":[{"point":[86,203]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[92,209],"end":[135,253]}]}]}]},{"start":[663,382],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[663,382]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,445],"end":[726,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,573],"end":[710,611]}]}]},{"tag":"LineTo","args":[{"point":[834,735]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[932,651],"end":[980,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[924,389],"end":[797,301]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[670,213],"end":[512,213]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,213],"end":[342,243]}]}]},{"tag":"LineTo","args":[{"point":[434,335]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[472,319],"end":[512,319]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[600,319],"end":[663,382]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"update","codePoint":59887},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[470,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,575]}]},{"tag":"LineTo","args":[{"point":[652,685]}]},{"tag":"LineTo","args":[{"point":[682,633]}]},{"tag":"LineTo","args":[{"point":[534,543]}]},{"tag":"LineTo","args":[{"point":[534,363]}]},{"tag":"LineTo","args":[{"point":[470,363]}]}]},{"start":[896,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[780,269]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,223],"end":[655,190]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,157],"end":[510,157]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[444,157],"end":[365,190]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[286,223],"end":[240,269]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,381],"end":[128,538]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,695],"end":[240,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,917],"end":[512,917]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,917],"end":[784,805]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,695],"end":[896,537]}]}]},{"tag":"LineTo","args":[{"point":[810,537]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[810,659],"end":[724,745]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,781],"end":[625,807]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[562,833],"end":[512,833]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[462,833],"end":[400,807]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[338,781],"end":[302,745]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[266,709],"end":[240,648]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,587],"end":[214,537]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,487],"end":[240,426]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[266,365],"end":[302,329]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[390,243],"end":[513,244]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[636,245],"end":[724,333]}]}]},{"tag":"LineTo","args":[{"point":[606,453]}]},{"tag":"LineTo","args":[{"point":[896,453]}]},{"tag":"LineTo","args":[{"point":[896,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"g_translate","codePoint":59888},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[905,926],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[905,926]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[892,939],"end":[874,939]}]}]},{"tag":"LineTo","args":[{"point":[598,939]}]},{"tag":"LineTo","args":[{"point":[682,831]}]},{"tag":"LineTo","args":[{"point":[638,699]}]},{"tag":"LineTo","args":[{"point":[770,831]}]},{"tag":"LineTo","args":[{"point":[810,793]}]},{"tag":"LineTo","args":[{"point":[670,653]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[742,573],"end":[772,473]}]}]},{"tag":"LineTo","args":[{"point":[854,473]}]},{"tag":"LineTo","args":[{"point":[854,417]}]},{"tag":"LineTo","args":[{"point":[660,417]}]},{"tag":"LineTo","args":[{"point":[660,363]}]},{"tag":"LineTo","args":[{"point":[606,363]}]},{"tag":"LineTo","args":[{"point":[606,417]}]},{"tag":"LineTo","args":[{"point":[544,417]}]},{"tag":"LineTo","args":[{"point":[488,255]}]},{"tag":"LineTo","args":[{"point":[874,255]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[892,255],"end":[905,268]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[918,281],"end":[918,299]}]}]},{"tag":"LineTo","args":[{"point":[918,895]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[918,913],"end":[905,926]}]}]}]},{"start":[720,473],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[694,553],"end":[632,621]}]}]},{"tag":"LineTo","args":[{"point":[596,573]}]},{"tag":"LineTo","args":[{"point":[562,473]}]},{"tag":"LineTo","args":[{"point":[720,473]}]}]},{"start":[148,641],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[148,641]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,579],"end":[86,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,403],"end":[148,340]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[210,277],"end":[298,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,277],"end":[442,333]}]}]},{"tag":"LineTo","args":[{"point":[386,387]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[352,353],"end":[298,353]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[242,353],"end":[203,393]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[164,433],"end":[164,491]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[164,547],"end":[203,587]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[242,627],"end":[298,627]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[356,627],"end":[388,595]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,563],"end":[424,525]}]}]},{"tag":"LineTo","args":[{"point":[298,525]}]},{"tag":"LineTo","args":[{"point":[298,451]}]},{"tag":"LineTo","args":[{"point":[498,451]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[504,493],"end":[504,495]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[504,587],"end":[447,645]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[390,703],"end":[298,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[210,703],"end":[148,641]}]}]}]},{"start":[470,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,63]}]},{"tag":"LineTo","args":[{"point":[128,63]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,63],"end":[68,89]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,115],"end":[42,149]}]}]},{"tag":"LineTo","args":[{"point":[42,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[42,823],"end":[68,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[94,875],"end":[128,875]}]}]},{"tag":"LineTo","args":[{"point":[470,875]}]},{"tag":"LineTo","args":[{"point":[512,1003]}]},{"tag":"LineTo","args":[{"point":[896,1003]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,1003],"end":[956,977]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,951],"end":[982,917]}]}]},{"tag":"LineTo","args":[{"point":[982,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,243],"end":[956,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,191],"end":[896,191]}]}]},{"tag":"LineTo","args":[{"point":[470,191]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"check_circle_outline","codePoint":59889},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[271,774],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[271,774]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,673],"end":[170,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,393],"end":[271,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,191],"end":[512,191]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,191],"end":[753,292]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,393],"end":[854,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,673],"end":[753,774]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,875],"end":[512,875]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[372,875],"end":[271,774]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]},{"start":[426,625],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[274,473]}]},{"tag":"LineTo","args":[{"point":[214,533]}]},{"tag":"LineTo","args":[{"point":[426,747]}]},{"tag":"LineTo","args":[{"point":[768,405]}]},{"tag":"LineTo","args":[{"point":[708,345]}]},{"tag":"LineTo","args":[{"point":[426,625]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"delete_outline","codePoint":59890},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[618,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[406,149]}]},{"tag":"LineTo","args":[{"point":[362,191]}]},{"tag":"LineTo","args":[{"point":[214,191]}]},{"tag":"LineTo","args":[{"point":[214,277]}]},{"tag":"LineTo","args":[{"point":[810,277]}]},{"tag":"LineTo","args":[{"point":[810,191]}]},{"tag":"LineTo","args":[{"point":[662,191]}]},{"tag":"LineTo","args":[{"point":[618,149]}]}]},{"start":[682,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,831]}]},{"tag":"LineTo","args":[{"point":[342,831]}]},{"tag":"LineTo","args":[{"point":[342,405]}]},{"tag":"LineTo","args":[{"point":[682,405]}]}]},{"start":[282,891],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[282,891]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,917],"end":[342,917]}]}]},{"tag":"LineTo","args":[{"point":[682,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[716,917],"end":[742,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,865],"end":[768,831]}]}]},{"tag":"LineTo","args":[{"point":[768,319]}]},{"tag":"LineTo","args":[{"point":[256,319]}]},{"tag":"LineTo","args":[{"point":[256,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,865],"end":[282,891]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"drive_folder_upload","codePoint":59891},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[402,637],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,569]}]},{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[554,569]}]},{"tag":"LineTo","args":[{"point":[622,637]}]},{"tag":"LineTo","args":[{"point":[682,577]}]},{"tag":"LineTo","args":[{"point":[512,405]}]},{"tag":"LineTo","args":[{"point":[342,577]}]},{"tag":"LineTo","args":[{"point":[402,637]}]}]},{"start":[170,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,363]}]},{"tag":"LineTo","args":[{"point":[854,363]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"LineTo","args":[{"point":[170,789]}]}]},{"start":[512,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,329],"end":[913,303]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,277],"end":[854,277]}]}]},{"tag":"LineTo","args":[{"point":[512,277]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"library_add_check","codePoint":59892},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[86,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[86,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,909],"end":[111,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,959],"end":[170,959]}]}]},{"tag":"LineTo","args":[{"point":[768,959]}]},{"tag":"LineTo","args":[{"point":[768,875]}]},{"tag":"LineTo","args":[{"point":[170,875]}]},{"tag":"LineTo","args":[{"point":[170,277]}]},{"tag":"LineTo","args":[{"point":[86,277]}]}]},{"start":[384,469],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[444,409]}]},{"tag":"LineTo","args":[{"point":[532,497]}]},{"tag":"LineTo","args":[{"point":[750,277]}]},{"tag":"LineTo","args":[{"point":[810,337]}]},{"tag":"LineTo","args":[{"point":[532,619]}]},{"tag":"LineTo","args":[{"point":[384,469]}]}]},{"start":[342,107],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,107],"end":[282,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,157],"end":[256,191]}]}]},{"tag":"LineTo","args":[{"point":[256,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,737],"end":[282,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,789],"end":[342,789]}]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,789],"end":[913,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,737],"end":[938,703]}]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,157],"end":[913,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,107],"end":[854,107]}]}]},{"tag":"LineTo","args":[{"point":[342,107]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"replay_circle_filled","codePoint":59893},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[693,714],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[693,714]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,789],"end":[512,789]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[406,789],"end":[331,714]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,639],"end":[256,533]}]}]},{"tag":"LineTo","args":[{"point":[342,533]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[342,603],"end":[392,653]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[442,703],"end":[512,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,703],"end":[632,653]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,603],"end":[682,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[682,463],"end":[632,413]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[582,363],"end":[512,363]}]}]},{"tag":"LineTo","args":[{"point":[512,491]}]},{"tag":"LineTo","args":[{"point":[342,319]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[512,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,277],"end":[693,352]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,427],"end":[768,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,639],"end":[693,714]}]}]}]},{"start":[211,232],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[211,232]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,357],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,709],"end":[211,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,959],"end":[512,959]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,959],"end":[813,834]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,709],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,357],"end":[813,232]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,107],"end":[512,107]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[336,107],"end":[211,232]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"redo","codePoint":59894},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[490,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[490,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[344,363],"end":[227,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[110,535],"end":[66,671]}]}]},{"tag":"LineTo","args":[{"point":[166,703]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[198,605],"end":[293,537]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,469],"end":[490,469]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,469],"end":[710,549]}]}]},{"tag":"LineTo","args":[{"point":[554,703]}]},{"tag":"LineTo","args":[{"point":[938,703]}]},{"tag":"LineTo","args":[{"point":[938,319]}]},{"tag":"LineTo","args":[{"point":[786,473]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[660,363],"end":[490,363]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"save","codePoint":59895},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[214,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"LineTo","args":[{"point":[640,235]}]},{"tag":"LineTo","args":[{"point":[640,405]}]},{"tag":"LineTo","args":[{"point":[214,405]}]}]},{"start":[422,793],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[422,793]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,755],"end":[384,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[384,651],"end":[422,613]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,575],"end":[512,575]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,575],"end":[602,613]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,651],"end":[640,703]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,755],"end":[602,793]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,831],"end":[512,831]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[460,831],"end":[422,793]}]}]}]},{"start":[214,149],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,149],"end":[153,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,199],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,867],"end":[153,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,319]}]},{"tag":"LineTo","args":[{"point":[726,149]}]},{"tag":"LineTo","args":[{"point":[214,149]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"zip","codePoint":59896},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,578],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,578]}]},{"tag":"LineTo","args":[{"point":[512,578]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[485,583],"end":[466.5,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,627],"end":[448,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,689],"end":[471.5,712.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[495,736],"end":[528,736]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,736],"end":[584.5,712.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,689],"end":[608,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,627],"end":[590,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,583],"end":[544,578]}]}]},{"tag":"LineTo","args":[{"point":[544,512]}]},{"tag":"LineTo","args":[{"point":[576,512]}]},{"tag":"LineTo","args":[{"point":[576,480]}]},{"tag":"LineTo","args":[{"point":[544,480]}]},{"tag":"LineTo","args":[{"point":[544,448]}]},{"tag":"LineTo","args":[{"point":[576,448]}]},{"tag":"LineTo","args":[{"point":[576,416]}]},{"tag":"LineTo","args":[{"point":[544,416]}]},{"tag":"LineTo","args":[{"point":[544,384]}]},{"tag":"LineTo","args":[{"point":[576,384]}]},{"tag":"LineTo","args":[{"point":[576,352]}]},{"tag":"LineTo","args":[{"point":[544,352]}]},{"tag":"LineTo","args":[{"point":[544,320]}]},{"tag":"LineTo","args":[{"point":[576,320]}]},{"tag":"LineTo","args":[{"point":[576,288]}]},{"tag":"LineTo","args":[{"point":[544,288]}]},{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"LineTo","args":[{"point":[576,224]}]},{"tag":"LineTo","args":[{"point":[544,224]}]},{"tag":"LineTo","args":[{"point":[544,192]}]},{"tag":"LineTo","args":[{"point":[576,192]}]},{"tag":"LineTo","args":[{"point":[576,160]}]},{"tag":"LineTo","args":[{"point":[544,160]}]},{"tag":"LineTo","args":[{"point":[544,128]}]},{"tag":"LineTo","args":[{"point":[576,128]}]},{"tag":"LineTo","args":[{"point":[576,96]}]},{"tag":"LineTo","args":[{"point":[704,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,96],"end":[794.5,133.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,171],"end":[832,224]}]}]},{"tag":"LineTo","args":[{"point":[832,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,885],"end":[794.5,922.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,960],"end":[704,960]}]}]},{"tag":"LineTo","args":[{"point":[352,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,960],"end":[261.5,922.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,885],"end":[224,832]}]}]},{"tag":"LineTo","args":[{"point":[224,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,171],"end":[261.5,133.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,96],"end":[352,96]}]}]},{"tag":"LineTo","args":[{"point":[512,96]}]},{"tag":"LineTo","args":[{"point":[512,128]}]},{"tag":"LineTo","args":[{"point":[480,128]}]},{"tag":"LineTo","args":[{"point":[480,160]}]},{"tag":"LineTo","args":[{"point":[512,160]}]},{"tag":"LineTo","args":[{"point":[512,192]}]},{"tag":"LineTo","args":[{"point":[480,192]}]},{"tag":"LineTo","args":[{"point":[480,224]}]},{"tag":"LineTo","args":[{"point":[512,224]}]},{"tag":"LineTo","args":[{"point":[512,256]}]},{"tag":"LineTo","args":[{"point":[480,256]}]},{"tag":"LineTo","args":[{"point":[480,288]}]},{"tag":"LineTo","args":[{"point":[512,288]}]},{"tag":"LineTo","args":[{"point":[512,320]}]},{"tag":"LineTo","args":[{"point":[480,320]}]},{"tag":"LineTo","args":[{"point":[480,352]}]},{"tag":"LineTo","args":[{"point":[512,352]}]},{"tag":"LineTo","args":[{"point":[512,384]}]},{"tag":"LineTo","args":[{"point":[480,384]}]},{"tag":"LineTo","args":[{"point":[480,416]}]},{"tag":"LineTo","args":[{"point":[512,416]}]},{"tag":"LineTo","args":[{"point":[512,448]}]},{"tag":"LineTo","args":[{"point":[480,448]}]},{"tag":"LineTo","args":[{"point":[480,480]}]},{"tag":"LineTo","args":[{"point":[512,480]}]},{"tag":"LineTo","args":[{"point":[512,512]}]},{"tag":"LineTo","args":[{"point":[480,512]}]},{"tag":"LineTo","args":[{"point":[480,544]}]},{"tag":"LineTo","args":[{"point":[512,544]}]},{"tag":"LineTo","args":[{"point":[512,578]}]}]},{"start":[528,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,608],"end":[562,622]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,636],"end":[576,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,676],"end":[562,690]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,704],"end":[528,704]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[508,704],"end":[494,690]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,676],"end":[480,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,636],"end":[494,622]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[508,608],"end":[528,608]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"zip-outline","codePoint":59897},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,561],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,544]}]},{"tag":"LineTo","args":[{"point":[480,544]}]},{"tag":"LineTo","args":[{"point":[480,512]}]},{"tag":"LineTo","args":[{"point":[512,512]}]},{"tag":"LineTo","args":[{"point":[512,480]}]},{"tag":"LineTo","args":[{"point":[480,480]}]},{"tag":"LineTo","args":[{"point":[480,448]}]},{"tag":"LineTo","args":[{"point":[512,448]}]},{"tag":"LineTo","args":[{"point":[512,416]}]},{"tag":"LineTo","args":[{"point":[480,416]}]},{"tag":"LineTo","args":[{"point":[480,384]}]},{"tag":"LineTo","args":[{"point":[512,384]}]},{"tag":"LineTo","args":[{"point":[512,352]}]},{"tag":"LineTo","args":[{"point":[480,352]}]},{"tag":"LineTo","args":[{"point":[480,320]}]},{"tag":"LineTo","args":[{"point":[512,320]}]},{"tag":"LineTo","args":[{"point":[512,288]}]},{"tag":"LineTo","args":[{"point":[480,288]}]},{"tag":"LineTo","args":[{"point":[480,256]}]},{"tag":"LineTo","args":[{"point":[512,256]}]},{"tag":"LineTo","args":[{"point":[512,224]}]},{"tag":"LineTo","args":[{"point":[480,224]}]},{"tag":"LineTo","args":[{"point":[480,192]}]},{"tag":"LineTo","args":[{"point":[512,192]}]},{"tag":"LineTo","args":[{"point":[512,160]}]},{"tag":"LineTo","args":[{"point":[480,160]}]},{"tag":"LineTo","args":[{"point":[480,128]}]},{"tag":"LineTo","args":[{"point":[352,128]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[312,128],"end":[284,156]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,184],"end":[256,224]}]}]},{"tag":"LineTo","args":[{"point":[256,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,872],"end":[284,900]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[312,928],"end":[352,928]}]}]},{"tag":"LineTo","args":[{"point":[704,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[744,928],"end":[772,900]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,872],"end":[800,832]}]}]},{"tag":"LineTo","args":[{"point":[800,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,184],"end":[772,156]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[744,128],"end":[704,128]}]}]},{"tag":"LineTo","args":[{"point":[544,128]}]},{"tag":"LineTo","args":[{"point":[544,160]}]},{"tag":"LineTo","args":[{"point":[576,160]}]},{"tag":"LineTo","args":[{"point":[576,192]}]},{"tag":"LineTo","args":[{"point":[544,192]}]},{"tag":"LineTo","args":[{"point":[544,224]}]},{"tag":"LineTo","args":[{"point":[576,224]}]},{"tag":"LineTo","args":[{"point":[576,256]}]},{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"LineTo","args":[{"point":[544,288]}]},{"tag":"LineTo","args":[{"point":[576,288]}]},{"tag":"LineTo","args":[{"point":[576,320]}]},{"tag":"LineTo","args":[{"point":[544,320]}]},{"tag":"LineTo","args":[{"point":[544,352]}]},{"tag":"LineTo","args":[{"point":[576,352]}]},{"tag":"LineTo","args":[{"point":[576,384]}]},{"tag":"LineTo","args":[{"point":[544,384]}]},{"tag":"LineTo","args":[{"point":[544,416]}]},{"tag":"LineTo","args":[{"point":[576,416]}]},{"tag":"LineTo","args":[{"point":[576,448]}]},{"tag":"LineTo","args":[{"point":[544,448]}]},{"tag":"LineTo","args":[{"point":[544,480]}]},{"tag":"LineTo","args":[{"point":[576,480]}]},{"tag":"LineTo","args":[{"point":[576,512]}]},{"tag":"LineTo","args":[{"point":[544,512]}]},{"tag":"LineTo","args":[{"point":[544,578]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,583],"end":[590,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,627],"end":[608,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,689],"end":[584.5,712.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,736],"end":[528,736]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[495,736],"end":[471.5,712.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,689],"end":[448,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[448,627],"end":[466.5,605]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[485,583],"end":[512,578]}]}]},{"tag":"LineTo","args":[{"point":[512,561]}]}]},{"start":[352,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[704,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,96],"end":[794.5,133.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,171],"end":[832,224]}]}]},{"tag":"LineTo","args":[{"point":[832,832]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,885],"end":[794.5,922.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[757,960],"end":[704,960]}]}]},{"tag":"LineTo","args":[{"point":[352,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,960],"end":[261.5,922.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,885],"end":[224,832]}]}]},{"tag":"LineTo","args":[{"point":[224,224]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,171],"end":[261.5,133.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[299,96],"end":[352,96]}]}]}]},{"start":[494,622],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,636],"end":[480,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[480,676],"end":[494,690]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[508,704],"end":[528,704]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,704],"end":[562,690]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,676],"end":[576,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,636],"end":[562,622]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,608],"end":[528,608]}]}]},{"tag":"LineTo","args":[{"point":[528,608]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[508,608],"end":[494,622]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"logout","codePoint":59898},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[512,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[170,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,149],"end":[111,175]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,201],"end":[86,235]}]}]},{"tag":"LineTo","args":[{"point":[86,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,865],"end":[111,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,917],"end":[170,917]}]}]},{"tag":"LineTo","args":[{"point":[512,917]}]},{"tag":"LineTo","args":[{"point":[512,831]}]},{"tag":"LineTo","args":[{"point":[170,831]}]},{"tag":"LineTo","args":[{"point":[170,235]}]},{"tag":"LineTo","args":[{"point":[512,235]}]}]},{"start":[666,379],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[776,491]}]},{"tag":"LineTo","args":[{"point":[342,491]}]},{"tag":"LineTo","args":[{"point":[342,575]}]},{"tag":"LineTo","args":[{"point":[776,575]}]},{"tag":"LineTo","args":[{"point":[666,685]}]},{"tag":"LineTo","args":[{"point":[726,747]}]},{"tag":"LineTo","args":[{"point":[938,533]}]},{"tag":"LineTo","args":[{"point":[726,319]}]},{"tag":"LineTo","args":[{"point":[666,379]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder_open","codePoint":59899},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,363]}]},{"tag":"LineTo","args":[{"point":[854,363]}]},{"tag":"LineTo","args":[{"point":[854,789]}]},{"tag":"LineTo","args":[{"point":[170,789]}]}]},{"start":[512,277],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,329],"end":[913,303]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,277],"end":[854,277]}]}]},{"tag":"LineTo","args":[{"point":[512,277]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"launchopen_in_new","codePoint":59900},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[598,235],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[750,235]}]},{"tag":"LineTo","args":[{"point":[332,653]}]},{"tag":"LineTo","args":[{"point":[392,713]}]},{"tag":"LineTo","args":[{"point":[810,295]}]},{"tag":"LineTo","args":[{"point":[810,447]}]},{"tag":"LineTo","args":[{"point":[896,447]}]},{"tag":"LineTo","args":[{"point":[896,149]}]},{"tag":"LineTo","args":[{"point":[598,149]}]},{"tag":"LineTo","args":[{"point":[598,235]}]}]},{"start":[214,831],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[214,235]}]},{"tag":"LineTo","args":[{"point":[512,235]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[214,149]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,149],"end":[153,174]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,199],"end":[128,235]}]}]},{"tag":"LineTo","args":[{"point":[128,831]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,867],"end":[153,892]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,917],"end":[214,917]}]}]},{"tag":"LineTo","args":[{"point":[810,917]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,917],"end":[870,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,865],"end":[896,831]}]}]},{"tag":"LineTo","args":[{"point":[896,533]}]},{"tag":"LineTo","args":[{"point":[810,533]}]},{"tag":"LineTo","args":[{"point":[810,831]}]},{"tag":"LineTo","args":[{"point":[214,831]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"open_in_browser","codePoint":59901},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[342,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[470,619]}]},{"tag":"LineTo","args":[{"point":[470,875]}]},{"tag":"LineTo","args":[{"point":[554,875]}]},{"tag":"LineTo","args":[{"point":[554,619]}]},{"tag":"LineTo","args":[{"point":[682,619]}]},{"tag":"LineTo","args":[{"point":[512,447]}]},{"tag":"LineTo","args":[{"point":[342,619]}]}]},{"start":[214,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,191],"end":[153,216]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,241],"end":[128,277]}]}]},{"tag":"LineTo","args":[{"point":[128,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[128,825],"end":[153,850]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[178,875],"end":[214,875]}]}]},{"tag":"LineTo","args":[{"point":[384,875]}]},{"tag":"LineTo","args":[{"point":[384,789]}]},{"tag":"LineTo","args":[{"point":[214,789]}]},{"tag":"LineTo","args":[{"point":[214,363]}]},{"tag":"LineTo","args":[{"point":[810,363]}]},{"tag":"LineTo","args":[{"point":[810,789]}]},{"tag":"LineTo","args":[{"point":[640,789]}]},{"tag":"LineTo","args":[{"point":[640,875]}]},{"tag":"LineTo","args":[{"point":[810,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[844,875],"end":[870,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,823],"end":[896,789]}]}]},{"tag":"LineTo","args":[{"point":[896,277]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[896,241],"end":[871,216]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[846,191],"end":[810,191]}]}]},{"tag":"LineTo","args":[{"point":[214,191]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"vue","codePoint":59902},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[1024,69],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,955]}]},{"tag":"LineTo","args":[{"point":[0,69]}]},{"tag":"LineTo","args":[{"point":[205,69]}]},{"tag":"LineTo","args":[{"point":[205,68]}]},{"tag":"LineTo","args":[{"point":[394,68]}]},{"tag":"LineTo","args":[{"point":[512,273]}]},{"tag":"LineTo","args":[{"point":[630,68]}]},{"tag":"LineTo","args":[{"point":[819,68]}]},{"tag":"LineTo","args":[{"point":[819,69]}]},{"tag":"LineTo","args":[{"point":[1024,69]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"angularuniversal","codePoint":59903},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[666,481],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[666,481]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,473],"end":[660,467]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,461],"end":[645,461]}]}]},{"tag":"LineTo","args":[{"point":[379,461]}]},{"tag":"LineTo","args":[{"point":[379,461]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[370,461],"end":[364,467]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,473],"end":[358,481]}]}]},{"tag":"LineTo","args":[{"point":[358,543]}]},{"tag":"LineTo","args":[{"point":[358,543]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,551],"end":[364,557]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[370,563],"end":[379,563]}]}]},{"tag":"LineTo","args":[{"point":[645,563]}]},{"tag":"LineTo","args":[{"point":[645,563]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,563],"end":[660,557]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,551],"end":[666,543]}]}]},{"tag":"LineTo","args":[{"point":[666,481]}]}]},{"start":[512,666],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[533,666],"end":[548,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[563,696],"end":[563,717]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[563,738],"end":[548,753]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[533,768],"end":[512,768]}]}]},{"tag":"LineTo","args":[{"point":[512,768]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[491,768],"end":[476,753]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[461,738],"end":[461,717]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[461,696],"end":[476,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[491,666],"end":[512,666]}]}]}]},{"start":[645,307],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,307],"end":[660,313]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,319],"end":[666,328]}]}]},{"tag":"LineTo","args":[{"point":[666,389]}]},{"tag":"LineTo","args":[{"point":[666,389]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,398],"end":[660,404]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,410],"end":[645,410]}]}]},{"tag":"LineTo","args":[{"point":[379,410]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[370,410],"end":[364,404]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,398],"end":[358,389]}]}]},{"tag":"LineTo","args":[{"point":[358,328]}]},{"tag":"LineTo","args":[{"point":[358,328]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[358,319],"end":[364,313]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[370,307],"end":[379,307]}]}]},{"tag":"LineTo","args":[{"point":[645,307]}]}]},{"start":[511,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[36,170]}]},{"tag":"LineTo","args":[{"point":[108,799]}]},{"tag":"LineTo","args":[{"point":[511,1024]}]},{"tag":"LineTo","args":[{"point":[915,799]}]},{"tag":"LineTo","args":[{"point":[988,170]}]},{"tag":"LineTo","args":[{"point":[511,0]}]}]},{"start":[717,758],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[717,758]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,783],"end":[699,801]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[681,819],"end":[655,819]}]}]},{"tag":"LineTo","args":[{"point":[369,819]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[343,819],"end":[325,801]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[307,783],"end":[307,758]}]}]},{"tag":"LineTo","args":[{"point":[307,266]}]},{"tag":"LineTo","args":[{"point":[307,266]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[307,241],"end":[325,223]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[343,205],"end":[369,205]}]}]},{"tag":"LineTo","args":[{"point":[655,205]}]},{"tag":"LineTo","args":[{"point":[655,205]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[681,205],"end":[699,223]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,241],"end":[717,266]}]}]},{"tag":"LineTo","args":[{"point":[717,758]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"linkinsert_link","codePoint":59904},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[554,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[554,401]}]},{"tag":"LineTo","args":[{"point":[726,401]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[780,401],"end":[819,440]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,479],"end":[858,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,587],"end":[819,626]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[780,665],"end":[726,665]}]}]},{"tag":"LineTo","args":[{"point":[554,665]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[726,747]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[814,747],"end":[876,684]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,621],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,445],"end":[876,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[814,319],"end":[726,319]}]}]},{"tag":"LineTo","args":[{"point":[554,319]}]}]},{"start":[682,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[682,491]}]},{"tag":"LineTo","args":[{"point":[342,491]}]},{"tag":"LineTo","args":[{"point":[342,575]}]},{"tag":"LineTo","args":[{"point":[682,575]}]}]},{"start":[205,440],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[205,440]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[244,401],"end":[298,401]}]}]},{"tag":"LineTo","args":[{"point":[470,401]}]},{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[298,319]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[210,319],"end":[148,382]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,445],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,621],"end":[148,684]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[210,747],"end":[298,747]}]}]},{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[470,665]}]},{"tag":"LineTo","args":[{"point":[298,665]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[244,665],"end":[205,626]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[166,587],"end":[166,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[166,479],"end":[205,440]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-text2","codePoint":59905},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[608,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,352]}]},{"tag":"LineTo","args":[{"point":[672,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,352],"end":[627,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[608,96]}]}]},{"start":[480,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[480,352]}]},{"tag":"LineTo","args":[{"point":[320,352]}]},{"tag":"LineTo","args":[{"point":[320,320]}]},{"tag":"LineTo","args":[{"point":[480,320]}]}]},{"start":[544,224],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"LineTo","args":[{"point":[320,256]}]},{"tag":"LineTo","args":[{"point":[320,224]}]},{"tag":"LineTo","args":[{"point":[544,224]}]}]},{"start":[736,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,448]}]},{"tag":"LineTo","args":[{"point":[320,448]}]},{"tag":"LineTo","args":[{"point":[320,416]}]},{"tag":"LineTo","args":[{"point":[736,416]}]}]},{"start":[640,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,544]}]},{"tag":"LineTo","args":[{"point":[320,544]}]},{"tag":"LineTo","args":[{"point":[320,512]}]},{"tag":"LineTo","args":[{"point":[640,512]}]}]},{"start":[736,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,640]}]},{"tag":"LineTo","args":[{"point":[320,640]}]},{"tag":"LineTo","args":[{"point":[320,608]}]},{"tag":"LineTo","args":[{"point":[736,608]}]}]},{"start":[608,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,736]}]},{"tag":"LineTo","args":[{"point":[320,736]}]},{"tag":"LineTo","args":[{"point":[320,704]}]},{"tag":"LineTo","args":[{"point":[608,704]}]}]},{"start":[736,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,832]}]},{"tag":"LineTo","args":[{"point":[320,832]}]},{"tag":"LineTo","args":[{"point":[320,800]}]},{"tag":"LineTo","args":[{"point":[736,800]}]}]},{"start":[832,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[832,320]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-text2-outline","codePoint":59906},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[624,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,320]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[624,96]}]}]},{"start":[608,128],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[626.5,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,352],"end":[672,352]}]}]},{"tag":"LineTo","args":[{"point":[800,352]}]},{"tag":"LineTo","args":[{"point":[800,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,909],"end":[790.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[781,928],"end":[768,928]}]}]},{"tag":"LineTo","args":[{"point":[288,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,928],"end":[265.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,909],"end":[256,896]}]}]},{"tag":"LineTo","args":[{"point":[256,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,147],"end":[265.5,137.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,128],"end":[288,128]}]}]},{"tag":"LineTo","args":[{"point":[608,128]}]}]},{"start":[640,144],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[790,320]}]},{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,144]}]}]},{"start":[480,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[480,352]}]},{"tag":"LineTo","args":[{"point":[320,352]}]},{"tag":"LineTo","args":[{"point":[320,320]}]},{"tag":"LineTo","args":[{"point":[480,320]}]}]},{"start":[544,224],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"LineTo","args":[{"point":[320,256]}]},{"tag":"LineTo","args":[{"point":[320,224]}]},{"tag":"LineTo","args":[{"point":[544,224]}]}]},{"start":[736,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,448]}]},{"tag":"LineTo","args":[{"point":[320,448]}]},{"tag":"LineTo","args":[{"point":[320,416]}]},{"tag":"LineTo","args":[{"point":[736,416]}]}]},{"start":[640,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,544]}]},{"tag":"LineTo","args":[{"point":[320,544]}]},{"tag":"LineTo","args":[{"point":[320,512]}]},{"tag":"LineTo","args":[{"point":[640,512]}]}]},{"start":[736,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,640]}]},{"tag":"LineTo","args":[{"point":[320,640]}]},{"tag":"LineTo","args":[{"point":[320,608]}]},{"tag":"LineTo","args":[{"point":[736,608]}]}]},{"start":[608,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,736]}]},{"tag":"LineTo","args":[{"point":[320,736]}]},{"tag":"LineTo","args":[{"point":[320,704]}]},{"tag":"LineTo","args":[{"point":[608,704]}]}]},{"start":[736,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,832]}]},{"tag":"LineTo","args":[{"point":[320,832]}]},{"tag":"LineTo","args":[{"point":[320,800]}]},{"tag":"LineTo","args":[{"point":[736,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-text4","codePoint":59907},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[608,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,352]}]},{"tag":"LineTo","args":[{"point":[672,352]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[646,352],"end":[627,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[608,288]}]}]},{"tag":"LineTo","args":[{"point":[608,96]}]}]},{"start":[832,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[832,320]}]}]},{"start":[544,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[544,352]}]},{"tag":"LineTo","args":[{"point":[320,352]}]},{"tag":"LineTo","args":[{"point":[320,320]}]},{"tag":"LineTo","args":[{"point":[544,320]}]}]},{"start":[544,224],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"LineTo","args":[{"point":[320,256]}]},{"tag":"LineTo","args":[{"point":[320,224]}]},{"tag":"LineTo","args":[{"point":[544,224]}]}]},{"start":[736,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,448]}]},{"tag":"LineTo","args":[{"point":[320,448]}]},{"tag":"LineTo","args":[{"point":[320,416]}]},{"tag":"LineTo","args":[{"point":[736,416]}]}]},{"start":[736,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,544]}]},{"tag":"LineTo","args":[{"point":[320,544]}]},{"tag":"LineTo","args":[{"point":[320,512]}]},{"tag":"LineTo","args":[{"point":[736,512]}]}]},{"start":[736,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,640]}]},{"tag":"LineTo","args":[{"point":[320,640]}]},{"tag":"LineTo","args":[{"point":[320,608]}]},{"tag":"LineTo","args":[{"point":[736,608]}]}]},{"start":[736,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,736]}]},{"tag":"LineTo","args":[{"point":[320,736]}]},{"tag":"LineTo","args":[{"point":[320,704]}]},{"tag":"LineTo","args":[{"point":[736,704]}]}]},{"start":[736,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,832]}]},{"tag":"LineTo","args":[{"point":[320,832]}]},{"tag":"LineTo","args":[{"point":[320,800]}]},{"tag":"LineTo","args":[{"point":[736,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"document-text5","codePoint":59908},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[624,96],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[288,96]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[262,96],"end":[243,115]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,134],"end":[224,160]}]}]},{"tag":"LineTo","args":[{"point":[224,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[224,922],"end":[242.5,941]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[261,960],"end":[288,960]}]}]},{"tag":"LineTo","args":[{"point":[768,960]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[795,960],"end":[813.5,941.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[832,923],"end":[832,896]}]}]},{"tag":"LineTo","args":[{"point":[832,320]}]},{"tag":"LineTo","args":[{"point":[640,96]}]},{"tag":"LineTo","args":[{"point":[624,96]}]}]},{"start":[608,128],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[608,288]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[608,315],"end":[626.5,333.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[645,352],"end":[672,352]}]}]},{"tag":"LineTo","args":[{"point":[800,352]}]},{"tag":"LineTo","args":[{"point":[800,896]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[800,909],"end":[790.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[781,928],"end":[768,928]}]}]},{"tag":"LineTo","args":[{"point":[288,928]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,928],"end":[265.5,918.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,909],"end":[256,896]}]}]},{"tag":"LineTo","args":[{"point":[256,160]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[256,147],"end":[265.5,137.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,128],"end":[288,128]}]}]},{"tag":"LineTo","args":[{"point":[608,128]}]}]},{"start":[640,144],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[790,320]}]},{"tag":"LineTo","args":[{"point":[672,320]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[659,320],"end":[649.5,310.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,301],"end":[640,288]}]}]},{"tag":"LineTo","args":[{"point":[640,144]}]}]},{"start":[544,320],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[544,352]}]},{"tag":"LineTo","args":[{"point":[320,352]}]},{"tag":"LineTo","args":[{"point":[320,320]}]},{"tag":"LineTo","args":[{"point":[544,320]}]}]},{"start":[544,224],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[544,256]}]},{"tag":"LineTo","args":[{"point":[320,256]}]},{"tag":"LineTo","args":[{"point":[320,224]}]},{"tag":"LineTo","args":[{"point":[544,224]}]}]},{"start":[736,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,448]}]},{"tag":"LineTo","args":[{"point":[320,448]}]},{"tag":"LineTo","args":[{"point":[320,416]}]},{"tag":"LineTo","args":[{"point":[736,416]}]}]},{"start":[736,512],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,544]}]},{"tag":"LineTo","args":[{"point":[320,544]}]},{"tag":"LineTo","args":[{"point":[320,512]}]},{"tag":"LineTo","args":[{"point":[736,512]}]}]},{"start":[736,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,640]}]},{"tag":"LineTo","args":[{"point":[320,640]}]},{"tag":"LineTo","args":[{"point":[320,608]}]},{"tag":"LineTo","args":[{"point":[736,608]}]}]},{"start":[736,704],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,736]}]},{"tag":"LineTo","args":[{"point":[320,736]}]},{"tag":"LineTo","args":[{"point":[320,704]}]},{"tag":"LineTo","args":[{"point":[736,704]}]}]},{"start":[736,800],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[736,832]}]},{"tag":"LineTo","args":[{"point":[320,832]}]},{"tag":"LineTo","args":[{"point":[320,800]}]},{"tag":"LineTo","args":[{"point":[736,800]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"folder4","codePoint":59909},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,191],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,191],"end":[111,217]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,243],"end":[86,277]}]}]},{"tag":"LineTo","args":[{"point":[86,789]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,823],"end":[111,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,875],"end":[170,875]}]}]},{"tag":"LineTo","args":[{"point":[854,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,875],"end":[913,849]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,823],"end":[938,789]}]}]},{"tag":"LineTo","args":[{"point":[938,363]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,329],"end":[913,303]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,277],"end":[854,277]}]}]},{"tag":"LineTo","args":[{"point":[512,277]}]},{"tag":"LineTo","args":[{"point":[426,191]}]},{"tag":"LineTo","args":[{"point":[170,191]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"shift","codePoint":59910},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":968,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[192,419],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[201,419],"end":[209.5,419.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[218,420],"end":[227,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[236,423],"end":[245,425.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[254,428],"end":[263,431]}]}]},{"tag":"LineTo","args":[{"point":[263,431]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[269,433],"end":[272.5,438]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,443],"end":[276,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,449],"end":[276,449]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,449],"end":[276,449]}]}]},{"tag":"LineTo","args":[{"point":[276,478]}]},{"tag":"LineTo","args":[{"point":[276,478]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,486],"end":[270.5,492]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[265,498],"end":[256,498]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[254,498],"end":[252,497.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[250,497],"end":[248,496]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[240,492],"end":[233,489.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[226,487],"end":[220,485]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[213,483],"end":[207,482.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[201,482],"end":[196,482]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[186,482],"end":[179.5,483.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[173,485],"end":[170,488]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[167,490],"end":[166,491.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[165,493],"end":[165,498]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[165,502],"end":[165.5,503.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[166,505],"end":[167,505]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[167,506],"end":[173.5,508.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[180,511],"end":[191,513]}]}]},{"tag":"LineTo","args":[{"point":[191,513]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[191,513],"end":[191,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[191,513],"end":[191,513]}]}]},{"tag":"LineTo","args":[{"point":[209,516]}]},{"tag":"LineTo","args":[{"point":[209,516]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[227,520],"end":[242,526.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[257,533],"end":[268,544]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[278,555],"end":[283.5,569.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[289,584],"end":[289,600]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[289,620],"end":[282,636.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,653],"end":[260,664]}]}]},{"tag":"LineTo","args":[{"point":[260,664]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[246,675],"end":[227,680]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[208,685],"end":[186,685]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[177,685],"end":[167.5,684]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[158,683],"end":[149,681]}]}]},{"tag":"LineTo","args":[{"point":[149,681]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[149,681],"end":[149,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[149,681],"end":[149,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[149,681],"end":[149,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[149,681],"end":[149,681]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[139,679],"end":[129.5,676]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[120,673],"end":[110,670]}]}]},{"tag":"LineTo","args":[{"point":[110,670]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[104,667],"end":[100.5,662]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[97,657],"end":[97,651]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[97,651],"end":[97,651]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[97,651],"end":[97,651]}]}]},{"tag":"LineTo","args":[{"point":[97,621]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[97,613],"end":[102.5,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[108,601],"end":[117,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[119,601],"end":[121.5,601.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[124,602],"end":[126,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,608],"end":[142,611.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[150,615],"end":[157,617]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[165,619],"end":[172,620]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[179,621],"end":[186,621]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[197,621],"end":[203.5,619.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[210,618],"end":[213,615]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[217,613],"end":[218,610.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[219,608],"end":[219,603]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[219,598],"end":[218,596]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[217,594],"end":[215,592]}]}]},{"tag":"LineTo","args":[{"point":[215,592]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[215,592],"end":[215,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[215,592],"end":[215,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[214,590],"end":[208,587.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[202,585],"end":[192,583]}]}]},{"tag":"LineTo","args":[{"point":[175,580]}]},{"tag":"LineTo","args":[{"point":[175,580]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[175,580],"end":[175,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[175,580],"end":[175,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[175,580],"end":[175,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[175,580],"end":[175,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[156,576],"end":[141.5,570]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[127,564],"end":[117,554]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[106,544],"end":[101,530]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[96,516],"end":[96,501]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[96,483],"end":[103,467]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[110,451],"end":[124,440]}]}]},{"tag":"LineTo","args":[{"point":[123,440]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[123,440],"end":[123.5,440]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[124,440],"end":[124,440]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[137,429],"end":[154.5,424]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[172,419],"end":[192,419]}]}]}]},{"start":[345,422],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[353,422],"end":[359,428]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[365,434],"end":[365,442]}]}]},{"tag":"LineTo","args":[{"point":[365,512]}]},{"tag":"LineTo","args":[{"point":[433,512]}]},{"tag":"LineTo","args":[{"point":[433,442]}]},{"tag":"LineTo","args":[{"point":[433,442]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[433,434],"end":[439,428]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[445,422],"end":[453,422]}]}]},{"tag":"LineTo","args":[{"point":[482,422]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[491,422],"end":[496.5,428]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[502,434],"end":[502,442]}]}]},{"tag":"LineTo","args":[{"point":[502,661]}]},{"tag":"LineTo","args":[{"point":[502,661]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[502,669],"end":[496.5,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[491,680],"end":[482,680]}]}]},{"tag":"LineTo","args":[{"point":[453,680]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[445,680],"end":[439,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[433,669],"end":[433,661]}]}]},{"tag":"LineTo","args":[{"point":[433,576]}]},{"tag":"LineTo","args":[{"point":[365,576]}]},{"tag":"LineTo","args":[{"point":[365,661]}]},{"tag":"LineTo","args":[{"point":[365,661]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[365,669],"end":[359,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[353,680],"end":[345,680]}]}]},{"tag":"LineTo","args":[{"point":[316,680]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,680],"end":[302,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,669],"end":[296,661]}]}]},{"tag":"LineTo","args":[{"point":[296,442]}]},{"tag":"LineTo","args":[{"point":[296,442]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[296,434],"end":[302,428]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[308,422],"end":[316,422]}]}]},{"tag":"LineTo","args":[{"point":[345,422]}]}]},{"start":[541,422],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[667,422]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[669,422],"end":[670,422.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[671,423],"end":[673,423]}]}]},{"tag":"LineTo","args":[{"point":[673,423]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[674,423],"end":[675.5,422.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,422],"end":[679,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[679,422],"end":[679,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[679,422],"end":[679,422]}]}]},{"tag":"LineTo","args":[{"point":[864,422]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[872,422],"end":[877.5,428]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[883,434],"end":[883,442]}]}]},{"tag":"LineTo","args":[{"point":[883,467]}]},{"tag":"LineTo","args":[{"point":[883,467]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[883,475],"end":[877.5,481]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[872,487],"end":[864,487]}]}]},{"tag":"LineTo","args":[{"point":[806,487]}]},{"tag":"LineTo","args":[{"point":[806,661]}]},{"tag":"LineTo","args":[{"point":[806,661]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[806,669],"end":[800,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[794,680],"end":[786,680]}]}]},{"tag":"LineTo","args":[{"point":[756,680]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[748,680],"end":[742.5,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[737,669],"end":[737,661]}]}]},{"tag":"LineTo","args":[{"point":[737,487]}]},{"tag":"LineTo","args":[{"point":[679,487]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,487],"end":[675.5,486.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[674,486],"end":[673,486]}]}]},{"tag":"LineTo","args":[{"point":[673,486]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,486],"end":[670.5,486.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[669,487],"end":[667,487]}]}]},{"tag":"LineTo","args":[{"point":[591,487]}]},{"tag":"LineTo","args":[{"point":[591,512]}]},{"tag":"LineTo","args":[{"point":[658,512]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,512],"end":[671.5,517.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,523],"end":[677,531]}]}]},{"tag":"LineTo","args":[{"point":[677,556]}]},{"tag":"LineTo","args":[{"point":[677,556]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,564],"end":[671.5,570]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[666,576],"end":[658,576]}]}]},{"tag":"LineTo","args":[{"point":[591,576]}]},{"tag":"LineTo","args":[{"point":[591,661]}]},{"tag":"LineTo","args":[{"point":[591,661]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[591,669],"end":[585,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[579,680],"end":[571,680]}]}]},{"tag":"LineTo","args":[{"point":[541,680]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[533,680],"end":[527.5,674.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,669],"end":[522,661]}]}]},{"tag":"LineTo","args":[{"point":[522,442]}]},{"tag":"LineTo","args":[{"point":[522,442]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[522,434],"end":[527.5,428]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[533,422],"end":[541,422]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"replace","codePoint":59911},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":968,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[942,570],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[942,570]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[953,570],"end":[960.5,577.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[968,585],"end":[968,595]}]}]},{"tag":"LineTo","args":[{"point":[968,998]}]},{"tag":"LineTo","args":[{"point":[968,998]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[968,1009],"end":[960.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[953,1024],"end":[942,1024]}]}]},{"tag":"LineTo","args":[{"point":[539,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[529,1024],"end":[521.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,1009],"end":[514,998]}]}]},{"tag":"LineTo","args":[{"point":[514,595]}]},{"tag":"LineTo","args":[{"point":[514,595]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,585],"end":[521.5,577.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[529,570],"end":[539,570]}]}]},{"tag":"LineTo","args":[{"point":[942,570]}]}]},{"start":[565,973],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[917,973]}]},{"tag":"LineTo","args":[{"point":[917,621]}]},{"tag":"LineTo","args":[{"point":[565,621]}]},{"tag":"LineTo","args":[{"point":[565,973]}]}]},{"start":[777,220],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[777,542]}]},{"tag":"LineTo","args":[{"point":[777,542]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[777,542],"end":[777,542]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[777,542],"end":[777,542]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[777,553],"end":[769.5,560.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[762,568],"end":[751,568]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[740,568],"end":[732.5,560.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,553],"end":[725,542]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,542],"end":[725,542]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[725,542],"end":[725,542]}]}]},{"tag":"LineTo","args":[{"point":[725,271]}]},{"tag":"LineTo","args":[{"point":[441,271]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[441,271],"end":[440.5,271.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[440,272],"end":[440,272]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[430,272],"end":[422.5,264]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[415,256],"end":[415,246]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[415,235],"end":[422.5,227.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[430,220],"end":[440,220]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[440,220],"end":[440.5,220]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[441,220],"end":[441,220]}]}]},{"tag":"LineTo","args":[{"point":[777,220]}]}]},{"start":[26,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[77,0]}]},{"tag":"LineTo","args":[{"point":[93,6]}]},{"tag":"LineTo","args":[{"point":[102,24]}]},{"tag":"LineTo","args":[{"point":[96,43]}]},{"tag":"LineTo","args":[{"point":[77,51]}]},{"tag":"LineTo","args":[{"point":[26,51]}]},{"tag":"LineTo","args":[{"point":[9,46]}]},{"tag":"LineTo","args":[{"point":[0,27]}]},{"tag":"LineTo","args":[{"point":[7,8]}]},{"tag":"LineTo","args":[{"point":[26,0]}]}]},{"start":[231,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[282,0]}]},{"tag":"LineTo","args":[{"point":[298,6]}]},{"tag":"LineTo","args":[{"point":[307,24]}]},{"tag":"LineTo","args":[{"point":[301,43]}]},{"tag":"LineTo","args":[{"point":[282,51]}]},{"tag":"LineTo","args":[{"point":[231,51]}]},{"tag":"LineTo","args":[{"point":[215,46]}]},{"tag":"LineTo","args":[{"point":[205,27]}]},{"tag":"LineTo","args":[{"point":[212,8]}]},{"tag":"LineTo","args":[{"point":[231,0]}]}]},{"start":[427,7],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[446,14]}]},{"tag":"LineTo","args":[{"point":[454,33]}]},{"tag":"LineTo","args":[{"point":[454,84]}]},{"tag":"LineTo","args":[{"point":[449,100]}]},{"tag":"LineTo","args":[{"point":[430,110]}]},{"tag":"LineTo","args":[{"point":[411,103]}]},{"tag":"LineTo","args":[{"point":[403,84]}]},{"tag":"LineTo","args":[{"point":[403,33]}]},{"tag":"LineTo","args":[{"point":[409,17]}]},{"tag":"LineTo","args":[{"point":[427,7]}]}]},{"start":[24,126],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[43,132]}]},{"tag":"LineTo","args":[{"point":[51,151]}]},{"tag":"LineTo","args":[{"point":[51,202]}]},{"tag":"LineTo","args":[{"point":[46,218]}]},{"tag":"LineTo","args":[{"point":[27,228]}]},{"tag":"LineTo","args":[{"point":[8,221]}]},{"tag":"LineTo","args":[{"point":[0,202]}]},{"tag":"LineTo","args":[{"point":[0,151]}]},{"tag":"LineTo","args":[{"point":[6,135]}]},{"tag":"LineTo","args":[{"point":[24,126]}]}]},{"start":[427,212],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[446,219]}]},{"tag":"LineTo","args":[{"point":[454,238]}]},{"tag":"LineTo","args":[{"point":[454,289]}]},{"tag":"LineTo","args":[{"point":[449,305]}]},{"tag":"LineTo","args":[{"point":[430,315]}]},{"tag":"LineTo","args":[{"point":[411,308]}]},{"tag":"LineTo","args":[{"point":[403,289]}]},{"tag":"LineTo","args":[{"point":[403,238]}]},{"tag":"LineTo","args":[{"point":[409,222]}]},{"tag":"LineTo","args":[{"point":[427,212]}]}]},{"start":[24,331],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[43,337]}]},{"tag":"LineTo","args":[{"point":[51,356]}]},{"tag":"LineTo","args":[{"point":[51,407]}]},{"tag":"LineTo","args":[{"point":[46,424]}]},{"tag":"LineTo","args":[{"point":[27,433]}]},{"tag":"LineTo","args":[{"point":[8,426]}]},{"tag":"LineTo","args":[{"point":[0,407]}]},{"tag":"LineTo","args":[{"point":[0,356]}]},{"tag":"LineTo","args":[{"point":[6,340]}]},{"tag":"LineTo","args":[{"point":[24,331]}]}]},{"start":[158,403],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[209,403]}]},{"tag":"LineTo","args":[{"point":[226,409]}]},{"tag":"LineTo","args":[{"point":[235,427]}]},{"tag":"LineTo","args":[{"point":[228,446]}]},{"tag":"LineTo","args":[{"point":[209,454]}]},{"tag":"LineTo","args":[{"point":[158,454]}]},{"tag":"LineTo","args":[{"point":[142,449]}]},{"tag":"LineTo","args":[{"point":[133,430]}]},{"tag":"LineTo","args":[{"point":[139,411]}]},{"tag":"LineTo","args":[{"point":[158,403]}]}]},{"start":[363,403],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[414,403]}]},{"tag":"LineTo","args":[{"point":[431,409]}]},{"tag":"LineTo","args":[{"point":[440,427]}]},{"tag":"LineTo","args":[{"point":[433,446]}]},{"tag":"LineTo","args":[{"point":[414,454]}]},{"tag":"LineTo","args":[{"point":[363,454]}]},{"tag":"LineTo","args":[{"point":[347,449]}]},{"tag":"LineTo","args":[{"point":[338,430]}]},{"tag":"LineTo","args":[{"point":[344,411]}]},{"tag":"LineTo","args":[{"point":[363,403]}]}]},{"start":[629,394],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[629,394]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,394],"end":[629,394]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,394],"end":[629,394]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[634,394],"end":[639,396]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,398],"end":[647,402]}]}]},{"tag":"LineTo","args":[{"point":[751,505]}]},{"tag":"LineTo","args":[{"point":[855,402]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,398],"end":[863,396]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[868,394],"end":[873,394]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[884,394],"end":[891.5,401.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[899,409],"end":[899,419]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[899,425],"end":[897,429.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[895,434],"end":[891,438]}]}]},{"tag":"LineTo","args":[{"point":[769,560]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[766,563],"end":[761,565]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[756,567],"end":[751,567]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[746,567],"end":[741,565]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,563],"end":[733,560]}]}]},{"tag":"LineTo","args":[{"point":[611,438]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[607,434],"end":[605,429.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[603,425],"end":[603,419]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[603,409],"end":[610.5,401.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,394],"end":[629,394]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"replace_all","codePoint":59912},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":968,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[778,253],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[778,672]}]},{"tag":"LineTo","args":[{"point":[778,672]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[778,672],"end":[778,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[778,672],"end":[778,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[778,682],"end":[771,689]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[764,696],"end":[753,696]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[743,696],"end":[735.5,689]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[728,682],"end":[728,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[728,672],"end":[728,672]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[728,672],"end":[728,672]}]}]},{"tag":"LineTo","args":[{"point":[728,300]}]},{"tag":"LineTo","args":[{"point":[438,300]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,300],"end":[438,300]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,300],"end":[438,300]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[428,300],"end":[420.5,293]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,286],"end":[413,277]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,267],"end":[420.5,260]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[428,253],"end":[438,253]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,253],"end":[438,253]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,253],"end":[438,253]}]}]},{"tag":"LineTo","args":[{"point":[778,253]}]}]},{"start":[26,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[77,0]}]},{"tag":"LineTo","args":[{"point":[93,6]}]},{"tag":"LineTo","args":[{"point":[102,24]}]},{"tag":"LineTo","args":[{"point":[96,43]}]},{"tag":"LineTo","args":[{"point":[77,51]}]},{"tag":"LineTo","args":[{"point":[26,51]}]},{"tag":"LineTo","args":[{"point":[9,46]}]},{"tag":"LineTo","args":[{"point":[0,27]}]},{"tag":"LineTo","args":[{"point":[7,8]}]},{"tag":"LineTo","args":[{"point":[26,0]}]}]},{"start":[231,0],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[282,0]}]},{"tag":"LineTo","args":[{"point":[298,6]}]},{"tag":"LineTo","args":[{"point":[307,24]}]},{"tag":"LineTo","args":[{"point":[301,43]}]},{"tag":"LineTo","args":[{"point":[282,51]}]},{"tag":"LineTo","args":[{"point":[231,51]}]},{"tag":"LineTo","args":[{"point":[215,46]}]},{"tag":"LineTo","args":[{"point":[205,27]}]},{"tag":"LineTo","args":[{"point":[212,8]}]},{"tag":"LineTo","args":[{"point":[231,0]}]}]},{"start":[427,7],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[446,14]}]},{"tag":"LineTo","args":[{"point":[454,33]}]},{"tag":"LineTo","args":[{"point":[454,84]}]},{"tag":"LineTo","args":[{"point":[449,100]}]},{"tag":"LineTo","args":[{"point":[430,110]}]},{"tag":"LineTo","args":[{"point":[411,103]}]},{"tag":"LineTo","args":[{"point":[403,84]}]},{"tag":"LineTo","args":[{"point":[403,33]}]},{"tag":"LineTo","args":[{"point":[409,17]}]},{"tag":"LineTo","args":[{"point":[427,7]}]}]},{"start":[24,126],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[43,132]}]},{"tag":"LineTo","args":[{"point":[51,151]}]},{"tag":"LineTo","args":[{"point":[51,202]}]},{"tag":"LineTo","args":[{"point":[46,218]}]},{"tag":"LineTo","args":[{"point":[27,228]}]},{"tag":"LineTo","args":[{"point":[8,221]}]},{"tag":"LineTo","args":[{"point":[0,202]}]},{"tag":"LineTo","args":[{"point":[0,151]}]},{"tag":"LineTo","args":[{"point":[6,135]}]},{"tag":"LineTo","args":[{"point":[24,126]}]}]},{"start":[427,212],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[446,219]}]},{"tag":"LineTo","args":[{"point":[454,238]}]},{"tag":"LineTo","args":[{"point":[454,289]}]},{"tag":"LineTo","args":[{"point":[449,305]}]},{"tag":"LineTo","args":[{"point":[430,315]}]},{"tag":"LineTo","args":[{"point":[411,308]}]},{"tag":"LineTo","args":[{"point":[403,289]}]},{"tag":"LineTo","args":[{"point":[403,238]}]},{"tag":"LineTo","args":[{"point":[409,222]}]},{"tag":"LineTo","args":[{"point":[427,212]}]}]},{"start":[24,331],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[43,337]}]},{"tag":"LineTo","args":[{"point":[51,356]}]},{"tag":"LineTo","args":[{"point":[51,407]}]},{"tag":"LineTo","args":[{"point":[46,424]}]},{"tag":"LineTo","args":[{"point":[27,433]}]},{"tag":"LineTo","args":[{"point":[8,426]}]},{"tag":"LineTo","args":[{"point":[0,407]}]},{"tag":"LineTo","args":[{"point":[0,356]}]},{"tag":"LineTo","args":[{"point":[6,340]}]},{"tag":"LineTo","args":[{"point":[24,331]}]}]},{"start":[158,403],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[209,403]}]},{"tag":"LineTo","args":[{"point":[226,409]}]},{"tag":"LineTo","args":[{"point":[235,427]}]},{"tag":"LineTo","args":[{"point":[228,446]}]},{"tag":"LineTo","args":[{"point":[209,454]}]},{"tag":"LineTo","args":[{"point":[158,454]}]},{"tag":"LineTo","args":[{"point":[142,449]}]},{"tag":"LineTo","args":[{"point":[133,430]}]},{"tag":"LineTo","args":[{"point":[139,411]}]},{"tag":"LineTo","args":[{"point":[158,403]}]}]},{"start":[363,403],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[414,403]}]},{"tag":"LineTo","args":[{"point":[431,409]}]},{"tag":"LineTo","args":[{"point":[440,427]}]},{"tag":"LineTo","args":[{"point":[433,446]}]},{"tag":"LineTo","args":[{"point":[414,454]}]},{"tag":"LineTo","args":[{"point":[363,454]}]},{"tag":"LineTo","args":[{"point":[347,449]}]},{"tag":"LineTo","args":[{"point":[338,430]}]},{"tag":"LineTo","args":[{"point":[344,411]}]},{"tag":"LineTo","args":[{"point":[363,403]}]}]},{"start":[629,580],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[629,580]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,580],"end":[629,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,580],"end":[629,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[634,580],"end":[639,582.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,585],"end":[647,588]}]}]},{"tag":"LineTo","args":[{"point":[751,692]}]},{"tag":"LineTo","args":[{"point":[855,588]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[858,585],"end":[863,582.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[868,580],"end":[873,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[884,580],"end":[891.5,587.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[899,595],"end":[899,606]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[899,611],"end":[897,616]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[895,621],"end":[891,624]}]}]},{"tag":"LineTo","args":[{"point":[769,746]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[766,750],"end":[761,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[756,754],"end":[751,754]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[746,754],"end":[741,752]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[736,750],"end":[733,746]}]}]},{"tag":"LineTo","args":[{"point":[611,624]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[607,621],"end":[605,616]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[603,611],"end":[603,606]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[603,595],"end":[610.5,587.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[618,580],"end":[629,580]}]}]}]},{"start":[372,764],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[372,764]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[382,764],"end":[389.5,771.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[397,779],"end":[397,790]}]}]},{"tag":"LineTo","args":[{"point":[397,998]}]},{"tag":"LineTo","args":[{"point":[397,998]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[397,1009],"end":[389.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[382,1024],"end":[372,1024]}]}]},{"tag":"LineTo","args":[{"point":[163,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[153,1024],"end":[145.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[138,1009],"end":[138,998]}]}]},{"tag":"LineTo","args":[{"point":[138,790]}]},{"tag":"LineTo","args":[{"point":[138,790]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[138,779],"end":[145.5,771.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[153,764],"end":[163,764]}]}]},{"tag":"LineTo","args":[{"point":[372,764]}]}]},{"start":[189,973],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[346,973]}]},{"tag":"LineTo","args":[{"point":[346,815]}]},{"tag":"LineTo","args":[{"point":[189,815]}]},{"tag":"LineTo","args":[{"point":[189,973]}]}]},{"start":[657,764],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[657,764]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[668,764],"end":[675.5,771.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,779],"end":[683,790]}]}]},{"tag":"LineTo","args":[{"point":[683,998]}]},{"tag":"LineTo","args":[{"point":[683,998]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,1009],"end":[675.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[668,1024],"end":[657,1024]}]}]},{"tag":"LineTo","args":[{"point":[449,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,1024],"end":[430.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[423,1009],"end":[423,998]}]}]},{"tag":"LineTo","args":[{"point":[423,790]}]},{"tag":"LineTo","args":[{"point":[423,790]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[423,779],"end":[430.5,771.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[438,764],"end":[449,764]}]}]},{"tag":"LineTo","args":[{"point":[657,764]}]}]},{"start":[474,973],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[631,973]}]},{"tag":"LineTo","args":[{"point":[631,815]}]},{"tag":"LineTo","args":[{"point":[474,815]}]},{"tag":"LineTo","args":[{"point":[474,973]}]}]},{"start":[942,764],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[942,764]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[953,764],"end":[960.5,771.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[968,779],"end":[968,790]}]}]},{"tag":"LineTo","args":[{"point":[968,998]}]},{"tag":"LineTo","args":[{"point":[968,998]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[968,1009],"end":[960.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[953,1024],"end":[942,1024]}]}]},{"tag":"LineTo","args":[{"point":[734,1024]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,1024],"end":[715.5,1016.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[708,1009],"end":[708,998]}]}]},{"tag":"LineTo","args":[{"point":[708,790]}]},{"tag":"LineTo","args":[{"point":[708,790]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[708,779],"end":[715.5,771.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,764],"end":[734,764]}]}]},{"tag":"LineTo","args":[{"point":[942,764]}]}]},{"start":[759,973],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[917,973]}]},{"tag":"LineTo","args":[{"point":[917,815]}]},{"tag":"LineTo","args":[{"point":[759,815]}]},{"tag":"LineTo","args":[{"point":[759,973]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"moveline-up","codePoint":59913},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":968,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[530,304],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,304],"end":[530,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,304],"end":[530,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[525,304],"end":[520,302]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,300],"end":[512,296]}]}]},{"tag":"LineTo","args":[{"point":[408,192]}]},{"tag":"LineTo","args":[{"point":[304,296]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[300,300],"end":[295.5,302]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[291,304],"end":[285,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,304],"end":[267.5,296.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,289],"end":[260,279]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,273],"end":[262,268.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,264],"end":[268,260]}]}]},{"tag":"LineTo","args":[{"point":[390,138]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[393,135],"end":[398,133]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[403,131],"end":[408,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,131],"end":[417.5,133]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,135],"end":[426,138]}]}]},{"tag":"LineTo","args":[{"point":[548,260]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[552,264],"end":[554,268.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,273],"end":[556,278]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,289],"end":[548.5,296.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[541,304],"end":[530,304]}]}]}]},{"start":[408,899],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[408,899]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,899],"end":[408,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,899],"end":[408,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,899],"end":[388,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,883],"end":[380,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,871],"end":[380,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,871],"end":[380,871]}]}]},{"tag":"LineTo","args":[{"point":[380,192]}]},{"tag":"LineTo","args":[{"point":[380,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,192],"end":[380,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,192],"end":[380,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,180],"end":[388,172]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,164],"end":[408,164]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,164],"end":[428,172]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,180],"end":[436,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,192],"end":[436,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,192],"end":[436,192]}]}]},{"tag":"LineTo","args":[{"point":[436,871]}]},{"tag":"LineTo","args":[{"point":[436,871]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,871],"end":[436,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,871],"end":[436,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,883],"end":[428,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,899],"end":[408,899]}]}]}]},{"start":[521,394],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[566,394]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[568,394],"end":[570,395.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,397],"end":[573,399]}]}]},{"tag":"LineTo","args":[{"point":[622,530]}]},{"tag":"LineTo","args":[{"point":[671,399]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,397],"end":[674,395.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[676,394],"end":[679,394]}]}]},{"tag":"LineTo","args":[{"point":[723,394]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,394],"end":[728.5,396.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[731,399],"end":[731,402]}]}]},{"tag":"LineTo","args":[{"point":[731,622]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[731,625],"end":[728.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,630],"end":[723,630]}]}]},{"tag":"LineTo","args":[{"point":[694,630]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[691,630],"end":[688.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[686,625],"end":[686,622]}]}]},{"tag":"LineTo","args":[{"point":[686,471]}]},{"tag":"LineTo","args":[{"point":[644,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,585],"end":[642,586.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,588],"end":[637,588]}]}]},{"tag":"LineTo","args":[{"point":[607,588]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[605,588],"end":[603,586.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[601,585],"end":[600,583]}]}]},{"tag":"LineTo","args":[{"point":[558,471]}]},{"tag":"LineTo","args":[{"point":[558,622]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[558,625],"end":[555.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[553,630],"end":[550,630]}]}]},{"tag":"LineTo","args":[{"point":[521,630]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[518,630],"end":[516,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,625],"end":[514,622]}]}]},{"tag":"LineTo","args":[{"point":[514,402]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,399],"end":[516,396.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[518,394],"end":[521,394]}]}]}]},{"start":[697.5,422],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[698,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[698,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[698,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[697.5,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[697,422],"end":[697,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[697,422],"end":[697.5,422]}]}]}]},{"start":[715,613],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[715,615]}]},{"tag":"LineTo","args":[{"point":[702,615]}]},{"tag":"LineTo","args":[{"point":[702,613]}]},{"tag":"LineTo","args":[{"point":[702,615]}]},{"tag":"LineTo","args":[{"point":[715,615]}]},{"tag":"LineTo","args":[{"point":[715,613]}]}]},{"start":[529,615],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[543,615]}]},{"tag":"LineTo","args":[{"point":[536,615]}]},{"tag":"LineTo","args":[{"point":[529,615]}]},{"tag":"LineTo","args":[{"point":[529,613]}]},{"tag":"LineTo","args":[{"point":[529,615]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"moveline-down","codePoint":59914},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":968,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[285,726],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[285,726]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[285,726],"end":[285.5,726]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[286,726],"end":[286,726]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[291,726],"end":[295.5,728]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[300,730],"end":[304,733]}]}]},{"tag":"LineTo","args":[{"point":[408,837]}]},{"tag":"LineTo","args":[{"point":[512,733]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,730],"end":[520,728]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[525,726],"end":[530,726]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[541,726],"end":[548.5,733.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,741],"end":[556,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,757],"end":[553.5,761.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,766],"end":[548,770]}]}]},{"tag":"LineTo","args":[{"point":[426,892]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,895],"end":[417.5,897]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,899],"end":[408,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[402,899],"end":[397.5,897]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[393,895],"end":[390,892]}]}]},{"tag":"LineTo","args":[{"point":[268,770]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,766],"end":[262,761.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,757],"end":[260,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,741],"end":[267.5,733.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,726],"end":[285,726]}]}]}]},{"start":[407,131],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[407,131],"end":[407.5,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,131],"end":[408,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[419,131],"end":[427.5,139]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,147],"end":[436,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,159],"end":[436,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,159],"end":[436,159]}]}]},{"tag":"LineTo","args":[{"point":[436,837]}]},{"tag":"LineTo","args":[{"point":[436,837]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,837],"end":[436,837.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,838],"end":[436,838]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,849],"end":[427.5,857.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[419,866],"end":[408,866]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,866],"end":[388,857.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,849],"end":[380,838]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,838],"end":[380,837.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,837],"end":[380,837]}]}]},{"tag":"LineTo","args":[{"point":[380,159]}]},{"tag":"LineTo","args":[{"point":[380,159]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,159],"end":[380,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,159],"end":[380,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,147],"end":[388,139]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,131],"end":[407,131]}]}]}]},{"start":[521,394],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[566,394]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[568,394],"end":[570,395.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,397],"end":[573,399]}]}]},{"tag":"LineTo","args":[{"point":[622,530]}]},{"tag":"LineTo","args":[{"point":[671,399]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,397],"end":[674,395.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[676,394],"end":[679,394]}]}]},{"tag":"LineTo","args":[{"point":[723,394]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,394],"end":[728.5,396.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[731,399],"end":[731,402]}]}]},{"tag":"LineTo","args":[{"point":[731,622]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[731,625],"end":[728.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[726,630],"end":[723,630]}]}]},{"tag":"LineTo","args":[{"point":[694,630]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[691,630],"end":[688.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[686,625],"end":[686,622]}]}]},{"tag":"LineTo","args":[{"point":[686,471]}]},{"tag":"LineTo","args":[{"point":[644,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[644,585],"end":[642,586.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[640,588],"end":[637,588]}]}]},{"tag":"LineTo","args":[{"point":[607,588]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[605,588],"end":[603,586.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[601,585],"end":[600,583]}]}]},{"tag":"LineTo","args":[{"point":[558,471]}]},{"tag":"LineTo","args":[{"point":[558,622]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[558,625],"end":[555.5,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[553,630],"end":[550,630]}]}]},{"tag":"LineTo","args":[{"point":[521,630]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[518,630],"end":[516,627.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,625],"end":[514,622]}]}]},{"tag":"LineTo","args":[{"point":[514,402]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[514,399],"end":[516,396.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[518,394],"end":[521,394]}]}]}]},{"start":[697.5,422],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[698,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[698,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[698,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,422],"end":[697.5,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[697,422],"end":[697,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[697,422],"end":[697.5,422]}]}]}]},{"start":[715,613],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[715,615]}]},{"tag":"LineTo","args":[{"point":[702,615]}]},{"tag":"LineTo","args":[{"point":[702,613]}]},{"tag":"LineTo","args":[{"point":[702,615]}]},{"tag":"LineTo","args":[{"point":[715,615]}]},{"tag":"LineTo","args":[{"point":[715,613]}]}]},{"start":[529,615],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[543,615]}]},{"tag":"LineTo","args":[{"point":[536,615]}]},{"tag":"LineTo","args":[{"point":[529,615]}]},{"tag":"LineTo","args":[{"point":[529,613]}]},{"tag":"LineTo","args":[{"point":[529,615]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"copyline-up","codePoint":59915},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":968,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[530,304],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,304],"end":[530,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[530,304],"end":[530,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[525,304],"end":[520,302]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,300],"end":[512,296]}]}]},{"tag":"LineTo","args":[{"point":[408,192]}]},{"tag":"LineTo","args":[{"point":[304,296]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[300,300],"end":[295.5,302]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[291,304],"end":[285,304]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,304],"end":[267.5,296.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,289],"end":[260,279]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,273],"end":[262,268.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,264],"end":[268,260]}]}]},{"tag":"LineTo","args":[{"point":[390,138]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[393,135],"end":[398,133]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[403,131],"end":[408,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,131],"end":[417.5,133]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,135],"end":[426,138]}]}]},{"tag":"LineTo","args":[{"point":[548,260]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[552,264],"end":[554,268.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,273],"end":[556,278]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,289],"end":[548.5,296.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[541,304],"end":[530,304]}]}]}]},{"start":[408,899],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[408,899]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,899],"end":[408,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,899],"end":[408,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,899],"end":[388,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,883],"end":[380,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,871],"end":[380,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,871],"end":[380,871]}]}]},{"tag":"LineTo","args":[{"point":[380,192]}]},{"tag":"LineTo","args":[{"point":[380,192]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,192],"end":[380,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,192],"end":[380,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,180],"end":[388,172]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,164],"end":[408,164]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,164],"end":[428,172]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,180],"end":[436,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,192],"end":[436,192]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,192],"end":[436,192]}]}]},{"tag":"LineTo","args":[{"point":[436,871]}]},{"tag":"LineTo","args":[{"point":[436,871]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,871],"end":[436,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,871],"end":[436,871]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,883],"end":[428,891]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[420,899],"end":[408,899]}]}]}]},{"start":[643,393],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,393],"end":[665,394.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[676,396],"end":[686,399]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[686,399],"end":[686,399]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[686,399],"end":[686,399]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[696,401],"end":[705.5,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[715,409],"end":[724,415]}]}]},{"tag":"LineTo","args":[{"point":[724,415]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[727,416],"end":[728.5,418.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,421],"end":[730,424]}]}]},{"tag":"LineTo","args":[{"point":[730,454]}]},{"tag":"LineTo","args":[{"point":[730,454]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,458],"end":[726.5,461.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,465],"end":[719,465]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,465],"end":[715,464.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[713,464],"end":[712,462]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,456],"end":[696,451.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,447],"end":[680,444]}]}]},{"tag":"LineTo","args":[{"point":[680,444]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,444],"end":[680,444]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,444],"end":[680,444]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,442],"end":[663,440.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,439],"end":[645,439]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[625,439],"end":[612,443.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[599,448],"end":[590,457]}]}]},{"tag":"LineTo","args":[{"point":[590,457]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[590,457],"end":[590,457]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[590,457],"end":[590,457]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,466],"end":[576,479.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[571,493],"end":[571,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[571,531],"end":[575.5,544.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,558],"end":[590,567]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[599,576],"end":[612,580.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[625,585],"end":[645,585]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,585],"end":[663,583.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,582],"end":[680,580]}]}]},{"tag":"LineTo","args":[{"point":[680,580]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,580],"end":[680,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,580],"end":[680,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,580],"end":[680,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,580],"end":[680,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,577],"end":[696,572.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,568],"end":[712,562]}]}]},{"tag":"LineTo","args":[{"point":[712,562]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[713,560],"end":[715,559.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,559],"end":[719,559]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,559],"end":[726.5,562.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,566],"end":[730,570]}]}]},{"tag":"LineTo","args":[{"point":[730,600]}]},{"tag":"LineTo","args":[{"point":[730,600]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,603],"end":[728.5,605.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[727,608],"end":[724,609]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[715,615],"end":[705.5,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[696,623],"end":[686,625]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[675,628],"end":[664.5,629.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,631],"end":[643,631]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,631],"end":[590.5,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[567,615],"end":[549,599]}]}]},{"tag":"LineTo","args":[{"point":[549,599]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,599],"end":[549,599]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,599],"end":[549,599]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[532,583],"end":[523.5,561]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,539],"end":[515,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,485],"end":[523.5,463]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[532,441],"end":[549,425]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[567,409],"end":[590.5,401]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,393],"end":[643,393]}]}]}]},{"start":[643,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,416],"end":[597.5,422.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,429],"end":[565,441]}]}]},{"tag":"LineTo","args":[{"point":[565,441]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[565,441],"end":[565,441]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[565,441],"end":[565,441]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,454],"end":[544,471.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,489],"end":[537,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,535],"end":[544,552.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,570],"end":[565,583]}]}]},{"tag":"LineTo","args":[{"point":[565,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,595],"end":[597.5,601.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,608],"end":[643,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[653,608],"end":[662,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[671,606],"end":[680,604]}]}]},{"tag":"LineTo","args":[{"point":[680,604]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[687,602],"end":[694,599]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,596],"end":[708,592]}]}]},{"tag":"LineTo","args":[{"point":[708,589]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[703,592],"end":[698,595.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[693,599],"end":[688,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,604],"end":[666.5,606]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[656,608],"end":[645,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[623,608],"end":[605,601.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,595],"end":[574,583]}]}]},{"tag":"LineTo","args":[{"point":[574,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,583],"end":[574,583]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,583],"end":[574,583]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,570],"end":[555,552]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,534],"end":[549,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,490],"end":[555,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,454],"end":[574,441]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,429],"end":[605,422.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[623,416],"end":[645,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[656,416],"end":[666.5,418]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,420],"end":[688,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[693,425],"end":[698,428.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[703,432],"end":[708,435]}]}]},{"tag":"LineTo","args":[{"point":[708,431]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,428],"end":[694.5,425]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,422],"end":[680,420]}]}]},{"tag":"LineTo","args":[{"point":[680,420]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,420],"end":[680,420]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,420],"end":[680,420]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[671,418],"end":[662,417]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[653,416],"end":[643,416]}]}]}]},{"start":[635,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[649,609],"end":[661.5,607.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[674,606],"end":[685,602]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[689,601],"end":[692.5,599.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[696,598],"end":[701,595]}]}]},{"tag":"LineTo","args":[{"point":[707,592]}]},{"tag":"LineTo","args":[{"point":[707,591]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[707,590],"end":[706.5,590]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,590],"end":[702,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[695,597],"end":[692,598.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[689,600],"end":[684,602]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,604],"end":[669.5,605.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[662,607],"end":[655,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[651,608],"end":[644,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[637,608],"end":[634,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[622,607],"end":[612,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[602,601],"end":[594,597]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,589],"end":[567.5,575.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,562],"end":[552,543]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,529],"end":[549,511.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,494],"end":[552,480]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[555,470],"end":[559.5,461.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[564,453],"end":[570,446]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,443],"end":[575.5,440]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[579,437],"end":[581,435]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[593,426],"end":[607,421.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[621,417],"end":[639,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[650,416],"end":[661,417.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,419],"end":[683,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,423],"end":[691.5,425]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[695,427],"end":[701,431]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[703,432],"end":[704.5,433]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,434],"end":[707,434]}]}]},{"tag":"LineTo","args":[{"point":[707,435]}]},{"tag":"LineTo","args":[{"point":[707,431]}]},{"tag":"LineTo","args":[{"point":[702,429]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,428],"end":[697.5,427]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[695,426],"end":[694,425]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[686,422],"end":[676.5,419.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[667,417],"end":[656,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,416],"end":[643,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[634,416],"end":[630,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[612,418],"end":[599.5,422]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,426],"end":[576,433]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[572,435],"end":[569,437.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[566,440],"end":[563,443]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,449],"end":[553.5,454]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,459],"end":[547,465]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[542,475],"end":[539.5,486.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,498],"end":[537,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,522],"end":[538,530.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[539,539],"end":[542,547]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[545,557],"end":[550,565]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[555,573],"end":[562,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[575,593],"end":[593.5,600]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[612,607],"end":[635,608]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"copyline-down","codePoint":59916},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":968,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[285,726],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[285,726]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[285,726],"end":[285.5,726]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[286,726],"end":[286,726]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[291,726],"end":[295.5,728]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[300,730],"end":[304,733]}]}]},{"tag":"LineTo","args":[{"point":[408,837]}]},{"tag":"LineTo","args":[{"point":[512,733]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,730],"end":[520,728]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[525,726],"end":[530,726]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[541,726],"end":[548.5,733.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,741],"end":[556,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[556,757],"end":[553.5,761.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,766],"end":[548,770]}]}]},{"tag":"LineTo","args":[{"point":[426,892]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[422,895],"end":[417.5,897]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,899],"end":[408,899]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[402,899],"end":[397.5,897]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[393,895],"end":[390,892]}]}]},{"tag":"LineTo","args":[{"point":[268,770]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[264,766],"end":[262,761.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,757],"end":[260,751]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[260,741],"end":[267.5,733.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[275,726],"end":[285,726]}]}]}]},{"start":[407,131],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[407,131],"end":[407.5,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,131],"end":[408,131]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[419,131],"end":[427.5,139]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,147],"end":[436,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,159],"end":[436,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,159],"end":[436,159]}]}]},{"tag":"LineTo","args":[{"point":[436,837]}]},{"tag":"LineTo","args":[{"point":[436,837]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,837],"end":[436,837.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,838],"end":[436,838]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,849],"end":[427.5,857.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[419,866],"end":[408,866]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,866],"end":[388,857.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,849],"end":[380,838]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,838],"end":[380,837.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,837],"end":[380,837]}]}]},{"tag":"LineTo","args":[{"point":[380,159]}]},{"tag":"LineTo","args":[{"point":[380,159]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,159],"end":[380,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,159],"end":[380,159]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[380,147],"end":[388,139]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[396,131],"end":[407,131]}]}]}]},{"start":[643,393],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,393],"end":[665,394.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[676,396],"end":[686,399]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[686,399],"end":[686,399]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[686,399],"end":[686,399]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[696,401],"end":[705.5,405]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[715,409],"end":[724,415]}]}]},{"tag":"LineTo","args":[{"point":[724,415]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[727,416],"end":[728.5,418.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,421],"end":[730,424]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,424],"end":[730,424]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,424],"end":[730,424]}]}]},{"tag":"LineTo","args":[{"point":[730,454]}]},{"tag":"LineTo","args":[{"point":[730,454]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,458],"end":[726.5,461.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,465],"end":[719,465]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,465],"end":[715,464.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[713,464],"end":[712,462]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,456],"end":[696,451.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,447],"end":[680,444]}]}]},{"tag":"LineTo","args":[{"point":[680,444]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,444],"end":[680,444]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,444],"end":[680,444]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,442],"end":[663,440.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,439],"end":[645,439]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[625,439],"end":[612,443.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[599,448],"end":[590,457]}]}]},{"tag":"LineTo","args":[{"point":[590,457]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[590,457],"end":[590,457]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[590,457],"end":[590,457]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[581,466],"end":[576,479.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[571,493],"end":[571,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[571,531],"end":[575.5,544.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,558],"end":[590,567]}]}]},{"tag":"LineTo","args":[{"point":[590,567]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[599,576],"end":[612,580.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[625,585],"end":[645,585]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,585],"end":[663,583.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[672,582],"end":[680,580]}]}]},{"tag":"LineTo","args":[{"point":[680,580]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,580],"end":[680,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,580],"end":[680,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,577],"end":[696,572.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,568],"end":[712,562]}]}]},{"tag":"LineTo","args":[{"point":[712,562]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[713,560],"end":[715,559.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[717,559],"end":[719,559]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,559],"end":[726.5,562.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,566],"end":[730,570]}]}]},{"tag":"LineTo","args":[{"point":[730,600]}]},{"tag":"LineTo","args":[{"point":[730,600]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[730,603],"end":[728.5,605.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[727,608],"end":[724,609]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[715,615],"end":[705.5,619]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[696,623],"end":[686,625]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[675,628],"end":[664.5,629.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[654,631],"end":[643,631]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,631],"end":[590.5,623]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[567,615],"end":[549,599]}]}]},{"tag":"LineTo","args":[{"point":[549,599]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,599],"end":[549,599]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,599],"end":[549,599]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[532,583],"end":[523.5,561]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,539],"end":[515,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[515,485],"end":[523.5,463]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[532,441],"end":[549,425]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[567,409],"end":[590.5,401]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,393],"end":[643,393]}]}]}]},{"start":[643,416],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,416],"end":[597.5,422.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,429],"end":[565,441]}]}]},{"tag":"LineTo","args":[{"point":[565,441]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[565,441],"end":[565,441]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[565,441],"end":[565,441]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,454],"end":[544,471.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,489],"end":[537,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,535],"end":[544,552.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,570],"end":[565,583]}]}]},{"tag":"LineTo","args":[{"point":[565,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,595],"end":[597.5,601.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[617,608],"end":[643,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[653,608],"end":[662,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[671,606],"end":[680,604]}]}]},{"tag":"LineTo","args":[{"point":[680,604]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,604],"end":[680,604]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[687,602],"end":[694,599]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,596],"end":[708,592]}]}]},{"tag":"LineTo","args":[{"point":[708,589]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[703,592],"end":[698,595.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[693,599],"end":[688,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,604],"end":[666.5,606]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[656,608],"end":[645,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[623,608],"end":[605,601.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,595],"end":[574,583]}]}]},{"tag":"LineTo","args":[{"point":[574,583]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,583],"end":[574,583]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[574,583],"end":[574,583]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,570],"end":[555,552]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,534],"end":[549,512]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,490],"end":[555,472]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,454],"end":[574,441]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,429],"end":[605,422.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[623,416],"end":[645,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[656,416],"end":[666.5,418]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,420],"end":[688,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[693,425],"end":[698,428.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[703,432],"end":[708,435]}]}]},{"tag":"LineTo","args":[{"point":[708,431]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,428],"end":[694.5,425]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[688,422],"end":[680,420]}]}]},{"tag":"LineTo","args":[{"point":[680,420]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,420],"end":[680,420]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,420],"end":[680,420]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[671,418],"end":[662,417]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[653,416],"end":[643,416]}]}]}]},{"start":[637,608],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[653,609],"end":[666.5,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[680,605],"end":[691,600]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[694,599],"end":[698,597]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[702,595],"end":[705,594]}]}]},{"tag":"LineTo","args":[{"point":[707,592]}]},{"tag":"LineTo","args":[{"point":[707,591]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[707,590],"end":[707,590]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[707,590],"end":[707,590]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[707,590],"end":[705.5,591]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[704,592],"end":[702,593]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[695,597],"end":[692,598.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[689,600],"end":[684,602]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[679,603],"end":[673,604.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[667,606],"end":[662,607]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[657,607],"end":[654,607.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[651,608],"end":[644,608]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[637,608],"end":[633,607.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[629,607],"end":[623,606]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,602],"end":[577,585.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[559,569],"end":[552,543]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[549,531],"end":[548.5,516]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[548,501],"end":[551,488]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[553,474],"end":[559,462]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[565,450],"end":[574,441]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[585,431],"end":[599.5,424.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,418],"end":[633,417]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[637,416],"end":[645,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[653,416],"end":[657,417]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[665,418],"end":[673,419.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[681,421],"end":[688,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[690,424],"end":[693.5,426]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[697,428],"end":[700,430]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[701,431],"end":[703,432]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[705,433],"end":[705,433]}]}]},{"tag":"LineTo","args":[{"point":[707,435]}]},{"tag":"LineTo","args":[{"point":[707,431]}]},{"tag":"LineTo","args":[{"point":[702,429]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[695,425],"end":[690,423]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[685,421],"end":[678,420]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[673,418],"end":[668,417.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[663,417],"end":[656,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,416],"end":[641.5,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[631,416],"end":[627,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,418],"end":[604,420.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[594,423],"end":[584,428]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[573,434],"end":[563.5,443]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[554,452],"end":[548,463]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[544,471],"end":[541.5,479.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[539,488],"end":[538,500]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,503],"end":[537,511.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[537,520],"end":[537,523]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[539,542],"end":[545,555.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[551,569],"end":[562,580]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[576,594],"end":[594.5,601]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[613,608],"end":[637,608]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"acode","codePoint":59917},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[976,526],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[976,531],"end":[974.5,537.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[973,544],"end":[967,550]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[965,552],"end":[963,554]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[961,556],"end":[959,558]}]}]},{"tag":"LineTo","args":[{"point":[959,558]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,589],"end":[885.5,635]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[841,681],"end":[813,709]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[808,714],"end":[803,717]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[798,720],"end":[792,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[777,720],"end":[750,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,720],"end":[708,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[706,720],"end":[704,719.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[702,719],"end":[701,719]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[705,746],"end":[709.5,772]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[714,798],"end":[718,822]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[720,833],"end":[710,843.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[700,854],"end":[690,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[675,854],"end":[655,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[635,854],"end":[624,854]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[616,854],"end":[606.5,846.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[597,839],"end":[595,824]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[587,785],"end":[578,734]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[568,683],"end":[557,625.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[546,568],"end":[534,508]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[523,447],"end":[512,390]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[501,447],"end":[489,508]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,568],"end":[467,626]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[456,684],"end":[446,735]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[436,786],"end":[429,824]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[426,840],"end":[417,847.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,855],"end":[400,855]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[388,855],"end":[368.5,855]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[349,855],"end":[334,855]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[324,855],"end":[313.5,844.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[303,834],"end":[305,823]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[309,799],"end":[313.5,773]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[318,747],"end":[323,719]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[321,720],"end":[319,720.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[317,721],"end":[315,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[300,721],"end":[273.5,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[247,721],"end":[231,721]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[226,721],"end":[220.5,718]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[215,715],"end":[210,710]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[183,682],"end":[138,636]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[93,590],"end":[64,559]}]}]},{"tag":"LineTo","args":[{"point":[64,559]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[62,557],"end":[60,555]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[58,553],"end":[56,551]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,545],"end":[49,538.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[47,532],"end":[47,527]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[47,526],"end":[47.5,525]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[48,524],"end":[48,523]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[48,518],"end":[50,512.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[52,507],"end":[56,502]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[58,500],"end":[60,498]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[62,496],"end":[64,494]}]}]},{"tag":"LineTo","args":[{"point":[64,494]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[93,463],"end":[138,417]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[183,371],"end":[210,343]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[215,338],"end":[220.5,335]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[226,332],"end":[231,332]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[247,332],"end":[273.5,332]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[300,332],"end":[315,332]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[324,332],"end":[332,338.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[340,345],"end":[343,352]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[347,362],"end":[346,374]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[345,386],"end":[337,395]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[314,418],"end":[264.5,469]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[215,520],"end":[209,526]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[215,533],"end":[262.5,582]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[310,631],"end":[334,656]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[345,595],"end":[355,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[366,471],"end":[376,412]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[386,353],"end":[395,299]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[404,246],"end":[411,203]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[413,191],"end":[422.5,181]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[432,171],"end":[441,171]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[452,171],"end":[468.5,171]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[485,171],"end":[500,171]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[501,170],"end":[502.5,170]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[504,170],"end":[506,170]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[520,170],"end":[544,170]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[568,170],"end":[583,170]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[592,170],"end":[601,180]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[610,190],"end":[612,202]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[620,245],"end":[629,298]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[638,352],"end":[648,410.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[658,469],"end":[669,532]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[679,594],"end":[690,654]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[714,630],"end":[761,581]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[808,532],"end":[814,525]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[808,519],"end":[759,468]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[710,417],"end":[687,394]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[678,385],"end":[677.5,373]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[677,361],"end":[681,351]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[683,344],"end":[691,337.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[699,331],"end":[708,331]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[723,331],"end":[750,331]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[777,331],"end":[792,331]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[798,331],"end":[803,334]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[808,337],"end":[813,342]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[841,370],"end":[885.5,416]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[930,462],"end":[959,493]}]}]},{"tag":"LineTo","args":[{"point":[959,493]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[961,495],"end":[963,497]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[965,499],"end":[967,501]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[972,506],"end":[973.5,511.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[975,517],"end":[976,522]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[976,523],"end":[976,524]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[976,525],"end":[976,526]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"patreon","codePoint":59919},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[799,51],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[799,51]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[866,80],"end":[916,130.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[966,181],"end":[995,248]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,315],"end":[1024,391]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,467],"end":[995,534]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[966,601],"end":[916,651]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[866,701],"end":[799,730]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,758],"end":[656,758]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,758],"end":[513,730]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[446,701],"end":[396,651]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[346,601],"end":[317,534]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,467],"end":[288,391]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[288,315],"end":[317,248]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[346,181],"end":[396,130.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[446,80],"end":[513,51]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[580,22],"end":[656,22]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[732,22],"end":[799,51]}]}]}]},{"start":[0,22],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[180,22]}]},{"tag":"LineTo","args":[{"point":[180,1004]}]},{"tag":"LineTo","args":[{"point":[0,1004]}]},{"tag":"LineTo","args":[{"point":[0,22]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"paypal","codePoint":59920},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[295,1024],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[295,1023]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[316,1023],"end":[334.5,1008.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[353,994],"end":[358,973]}]}]},{"tag":"LineTo","args":[{"point":[403,777]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[408,756],"end":[427,741]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[446,726],"end":[467,726]}]}]},{"tag":"LineTo","args":[{"point":[505,726]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[566,726],"end":[619,720]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[673,714],"end":[720.5,701.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[768,689],"end":[808,670]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[848,652],"end":[882,627]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[950,577],"end":[983.5,511.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1017,446],"end":[1017,365]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1017,330],"end":[1010.5,300]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1004,270],"end":[992,246]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[979,222],"end":[961,203]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[943,184],"end":[919,169]}]}]},{"tag":"LineTo","args":[{"point":[913,166]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[913,166],"end":[913,166.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[913,167],"end":[913,168]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[926,191],"end":[932.5,221]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[939,251],"end":[939,287]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[939,368],"end":[905,433]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[871,498],"end":[804,548]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[770,573],"end":[730,592]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[689,611],"end":[642,623.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[595,636],"end":[541,642]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[487,648],"end":[427,648]}]}]},{"tag":"LineTo","args":[{"point":[389,648]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[368,648],"end":[349.5,663]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[331,678],"end":[326,699]}]}]},{"tag":"LineTo","args":[{"point":[280,894]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,915],"end":[257,930]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[238,945],"end":[217,945]}]}]},{"tag":"LineTo","args":[{"point":[129,945]}]},{"tag":"LineTo","args":[{"point":[122,974]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[118,994],"end":[129.5,1009]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[141,1024],"end":[162,1024]}]}]},{"tag":"LineTo","args":[{"point":[295,1024]}]}]},{"start":[181,909],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[180,907]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[201,907],"end":[219.5,892.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[238,878],"end":[243,857]}]}]},{"tag":"LineTo","args":[{"point":[288,662]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[293,641],"end":[311.5,626.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[330,612],"end":[352,612]}]}]},{"tag":"LineTo","args":[{"point":[389,612]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[450,612],"end":[504,606]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[557,600],"end":[604.5,587.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[652,575],"end":[692,556]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[733,538],"end":[766,513]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[834,463],"end":[867.5,398]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[901,333],"end":[901,251]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[901,216],"end":[895,186]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[889,156],"end":[876,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[864,108],"end":[845.5,88.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[827,69],"end":[804,55]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[779,39],"end":[750.5,28.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[722,18],"end":[689,12]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[656,6],"end":[617,3]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[578,0],"end":[534,0]}]}]},{"tag":"LineTo","args":[{"point":[258,0]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[237,0],"end":[218.5,15]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[200,30],"end":[195,50]}]}]},{"tag":"LineTo","args":[{"point":[8,859]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[3,879],"end":[15,894]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[27,909],"end":[48,909]}]}]},{"tag":"LineTo","args":[{"point":[181,909]}]}]},{"start":[498,168],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[497,168]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[535,168],"end":[564,174.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[593,181],"end":[612,194]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[631,206],"end":[640.5,226]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[650,246],"end":[650,272]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[650,312],"end":[635.5,343.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[621,375],"end":[591,396]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[561,418],"end":[519.5,429]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[478,440],"end":[424,440]}]}]},{"tag":"LineTo","args":[{"point":[392,440]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[371,440],"end":[359,425]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[347,410],"end":[352,390]}]}]},{"tag":"LineTo","args":[{"point":[392,218]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[397,197],"end":[415.5,182.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[434,168],"end":[455,168]}]}]},{"tag":"LineTo","args":[{"point":[498,168]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"ruby","codePoint":59921},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[860,4],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[860,3]}]},{"tag":"LineTo","args":[{"point":[704,90]}]},{"tag":"LineTo","args":[{"point":[974,310]}]},{"tag":"LineTo","args":[{"point":[1000,332]}]},{"tag":"LineTo","args":[{"point":[624,356]}]},{"tag":"LineTo","args":[{"point":[666,523]}]},{"tag":"LineTo","args":[{"point":[726,755]}]},{"tag":"LineTo","args":[{"point":[331,628]}]},{"tag":"LineTo","args":[{"point":[330,630]}]},{"tag":"LineTo","args":[{"point":[332,629]}]},{"tag":"LineTo","args":[{"point":[212,1020]}]},{"tag":"LineTo","args":[{"point":[70,689]}]},{"tag":"LineTo","args":[{"point":[0,817]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[2,890],"end":[27,931]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[51,971],"end":[83.5,991]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[116,1011],"end":[151,1015]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[186,1020],"end":[209,1021]}]}]},{"tag":"LineTo","args":[{"point":[966,969]}]},{"tag":"LineTo","args":[{"point":[1024,206]}]},{"tag":"LineTo","args":[{"point":[1023,207]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1025,138],"end":[991,79]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[957,20],"end":[860,4]}]}]}]},{"start":[219,216],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[219,217]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[162,273],"end":[120,335]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[78,396],"end":[56.5,452.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[35,509],"end":[36,557]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[36,604],"end":[65,633]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[93,661],"end":[141,659]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[189,658],"end":[246.5,633]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[304,608],"end":[366,564]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[428,520],"end":[486,464]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[543,407],"end":[585,347]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[627,286],"end":[649,230]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[671,174],"end":[671,128]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[670,81],"end":[642,53]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[614,24],"end":[566,25]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[517,26],"end":[459,49.5]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[401,73],"end":[339,117]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[276,160],"end":[219,216]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"font_download","codePoint":59922},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[632,683],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[392,683]}]},{"tag":"LineTo","args":[{"point":[344,811]}]},{"tag":"LineTo","args":[{"point":[254,811]}]},{"tag":"LineTo","args":[{"point":[472,255]}]},{"tag":"LineTo","args":[{"point":[552,255]}]},{"tag":"LineTo","args":[{"point":[770,811]}]},{"tag":"LineTo","args":[{"point":[680,811]}]},{"tag":"LineTo","args":[{"point":[632,683]}]}]},{"start":[170,107],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,107],"end":[111,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,157],"end":[86,191]}]}]},{"tag":"LineTo","args":[{"point":[86,875]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,909],"end":[111,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[136,959],"end":[170,959]}]}]},{"tag":"LineTo","args":[{"point":[854,959]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,959],"end":[913,934]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,909],"end":[938,875]}]}]},{"tag":"LineTo","args":[{"point":[938,191]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,157],"end":[913,132]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[888,107],"end":[854,107]}]}]},{"tag":"LineTo","args":[{"point":[170,107]}]}]},{"start":[600,597],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,361]}]},{"tag":"LineTo","args":[{"point":[424,597]}]},{"tag":"LineTo","args":[{"point":[600,597]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"notes","codePoint":59923},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[896,575],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,491]}]},{"tag":"LineTo","args":[{"point":[128,491]}]},{"tag":"LineTo","args":[{"point":[128,575]}]},{"tag":"LineTo","args":[{"point":[896,575]}]}]},{"start":[128,363],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[896,363]}]},{"tag":"LineTo","args":[{"point":[896,277]}]},{"tag":"LineTo","args":[{"point":[128,277]}]},{"tag":"LineTo","args":[{"point":[128,363]}]}]},{"start":[640,789],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[640,703]}]},{"tag":"LineTo","args":[{"point":[128,703]}]},{"tag":"LineTo","args":[{"point":[128,789]}]},{"tag":"LineTo","args":[{"point":[640,789]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"http","codePoint":59924},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[832,511],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[832,469]}]},{"tag":"LineTo","args":[{"point":[918,469]}]},{"tag":"LineTo","args":[{"point":[918,511]}]},{"tag":"LineTo","args":[{"point":[832,511]}]}]},{"start":[768,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[768,661]}]},{"tag":"LineTo","args":[{"point":[832,661]}]},{"tag":"LineTo","args":[{"point":[832,575]}]},{"tag":"LineTo","args":[{"point":[918,575]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[944,575],"end":[963,556]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,537],"end":[982,511]}]}]},{"tag":"LineTo","args":[{"point":[982,469]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[982,443],"end":[963,424]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[944,405],"end":[918,405]}]}]},{"tag":"LineTo","args":[{"point":[768,405]}]}]},{"start":[598,469],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[598,661]}]},{"tag":"LineTo","args":[{"point":[662,661]}]},{"tag":"LineTo","args":[{"point":[662,469]}]},{"tag":"LineTo","args":[{"point":[726,469]}]},{"tag":"LineTo","args":[{"point":[726,405]}]},{"tag":"LineTo","args":[{"point":[534,405]}]},{"tag":"LineTo","args":[{"point":[534,469]}]},{"tag":"LineTo","args":[{"point":[598,469]}]}]},{"start":[362,469],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[362,661]}]},{"tag":"LineTo","args":[{"point":[426,661]}]},{"tag":"LineTo","args":[{"point":[426,469]}]},{"tag":"LineTo","args":[{"point":[490,469]}]},{"tag":"LineTo","args":[{"point":[490,405]}]},{"tag":"LineTo","args":[{"point":[298,405]}]},{"tag":"LineTo","args":[{"point":[298,469]}]},{"tag":"LineTo","args":[{"point":[362,469]}]}]},{"start":[106,491],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[106,405]}]},{"tag":"LineTo","args":[{"point":[42,405]}]},{"tag":"LineTo","args":[{"point":[42,661]}]},{"tag":"LineTo","args":[{"point":[106,661]}]},{"tag":"LineTo","args":[{"point":[106,555]}]},{"tag":"LineTo","args":[{"point":[192,555]}]},{"tag":"LineTo","args":[{"point":[192,661]}]},{"tag":"LineTo","args":[{"point":[256,661]}]},{"tag":"LineTo","args":[{"point":[256,405]}]},{"tag":"LineTo","args":[{"point":[192,405]}]},{"tag":"LineTo","args":[{"point":[192,491]}]},{"tag":"LineTo","args":[{"point":[106,491]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"compare_arrows","codePoint":59925},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[640,447],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[938,447]}]},{"tag":"LineTo","args":[{"point":[938,363]}]},{"tag":"LineTo","args":[{"point":[640,363]}]},{"tag":"LineTo","args":[{"point":[640,235]}]},{"tag":"LineTo","args":[{"point":[470,405]}]},{"tag":"LineTo","args":[{"point":[640,575]}]},{"tag":"LineTo","args":[{"point":[640,447]}]}]},{"start":[86,619],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[86,703]}]},{"tag":"LineTo","args":[{"point":[384,703]}]},{"tag":"LineTo","args":[{"point":[384,831]}]},{"tag":"LineTo","args":[{"point":[554,661]}]},{"tag":"LineTo","args":[{"point":[384,491]}]},{"tag":"LineTo","args":[{"point":[384,619]}]},{"tag":"LineTo","args":[{"point":[86,619]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"home_filled","codePoint":59926},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[170,405],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[170,917]}]},{"tag":"LineTo","args":[{"point":[384,917]}]},{"tag":"LineTo","args":[{"point":[384,619]}]},{"tag":"LineTo","args":[{"point":[640,619]}]},{"tag":"LineTo","args":[{"point":[640,917]}]},{"tag":"LineTo","args":[{"point":[854,917]}]},{"tag":"LineTo","args":[{"point":[854,405]}]},{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[170,405]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"height","codePoint":59927},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[682,319],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[512,149]}]},{"tag":"LineTo","args":[{"point":[342,319]}]},{"tag":"LineTo","args":[{"point":[470,319]}]},{"tag":"LineTo","args":[{"point":[470,747]}]},{"tag":"LineTo","args":[{"point":[342,747]}]},{"tag":"LineTo","args":[{"point":[512,917]}]},{"tag":"LineTo","args":[{"point":[682,747]}]},{"tag":"LineTo","args":[{"point":[554,747]}]},{"tag":"LineTo","args":[{"point":[554,319]}]},{"tag":"LineTo","args":[{"point":[682,319]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}},{"extras":{"name":"all_inclusive","codePoint":59928},"node":{"tag":"Element","args":[{"tagName":"svg","attributes":{"viewBox":{"tag":"Value","args":[{"tag":"ViewBox","args":[{"minX":0,"minY":0,"width":1024,"height":1024}]}]}},"children":[{"tag":"Element","args":[{"tagName":"path","attributes":{"d":{"tag":"Value","args":[{"tag":"Paths","args":[[{"start":[632,369],"endings":{"tag":"Connected","args":[]},"cmds":[{"tag":"LineTo","args":[{"point":[632,369]}]},{"tag":"LineTo","args":[{"point":[512,475]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[474,511],"end":[332,635]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,677],"end":[230,677]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,677],"end":[128,635]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,593],"end":[86,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[86,473],"end":[128,431]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[170,389],"end":[230,389]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[290,389],"end":[334,433]}]}]},{"tag":"LineTo","args":[{"point":[382,475]}]},{"tag":"LineTo","args":[{"point":[448,419]}]},{"tag":"LineTo","args":[{"point":[394,371]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[366,343],"end":[319,323]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[272,303],"end":[232,303]}]}]},{"tag":"LineTo","args":[{"point":[230,303]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,303],"end":[67,371]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,439],"end":[0,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[0,627],"end":[67,695]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[134,763],"end":[230,763]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[326,763],"end":[392,697]}]}]},{"tag":"LineTo","args":[{"point":[512,591]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[550,555],"end":[692,431]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,389],"end":[794,389]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,389],"end":[896,431]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,473],"end":[938,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[938,593],"end":[896,635]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[854,677],"end":[794,677]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[734,677],"end":[690,633]}]}]},{"tag":"LineTo","args":[{"point":[640,591]}]},{"tag":"LineTo","args":[{"point":[576,647]}]},{"tag":"LineTo","args":[{"point":[630,695]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[660,723],"end":[707,743]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[754,763],"end":[792,763]}]}]},{"tag":"LineTo","args":[{"point":[794,763]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,763],"end":[957,695]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,627],"end":[1024,533]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[1024,439],"end":[957,371]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[890,303],"end":[794,303]}]}]},{"tag":"BezierCurveTo","args":[{"tag":"QParams","args":[{"c":[698,303],"end":[632,369]}]}]}]}]]}]},"fill":{"tag":"Value","args":[{"tag":"Paint","args":[{"tag":"CurrentColor","args":[]}]}]}},"children":[]}]}]}]},"palettes":{"index":0,"table":[[{"background":{"tag":"AutomaticColor","args":[]},"foreground":{"tag":"AutomaticColor","args":[]}},[]]]}}]} ================================================ FILE: utils/config.js ================================================ const path = require("node:path"); const fs = require("node:fs"); const { promisify } = require("node:util"); const exec = promisify(require("node:child_process").exec); (async () => { const AD_APP_ID = "ca-app-pub-5911839694379275~4255791238"; const CONFIG_ID = /id="([a-z.]+")/; const HTML_ID = /[a-z.]+<\/title>/; const ID_PAID = "com.foxdebug.acode"; const ID_FREE = "com.foxdebug.acodefree"; const arg = process.argv[2]; const arg2 = process.argv[3]; const platformsDir = path.resolve(__dirname, "../platforms/"); const babelrcpath = path.resolve(__dirname, "../.babelrc"); const configpath = path.resolve(__dirname, "../config.xml"); const htmlpath = path.resolve(__dirname, "../www/index.html"); const logopath = path.resolve( __dirname, "../res/android/values/ic_launcher_background.xml", ); const logoTextPaid = `<?xml version="1.0" encoding="utf-8"?> <resources> <color name="ic_launcher_background">#3a3e54</color> <color name="ic_splash_background">#3a3e54</color> </resources>`; const logoTextFree = `<?xml version="1.0" encoding="utf-8"?> <resources> <color name="ic_launcher_background">#ffffff</color> <color name="ic_splash_background">#313131</color> </resources>`; try { let babelrcText = fs.readFileSync(babelrcpath, "utf-8"); let config = fs.readFileSync(configpath, "utf-8"); let html = fs.readFileSync(htmlpath, "utf-8"); let platforms = fs .readdirSync(platformsDir) .filter((file) => !file.startsWith(".")); let logo, id, currentId; currentId = /id="([a-z.]+)"/.exec(config)[1]; babelrc = JSON.parse(babelrcText); if (arg === "d") { babelrc.compact = false; } else if (arg === "p") { babelrc.compact = true; } if (arg2 === "free") { logo = logoTextFree; id = ID_FREE; } else { logo = logoTextPaid; id = ID_PAID; } fs.writeFileSync(babelrcpath, babelrcText, "utf8"); if (currentId !== id) { const promises = []; html = html.replace(HTML_ID, `<title>${id}`); config = config.replace(CONFIG_ID, `id="${id}"`); babelrcText = JSON.stringify(babelrc, undefined, 2); fs.writeFileSync(htmlpath, html, "utf8"); fs.writeFileSync(logopath, logo, "utf8"); fs.writeFileSync(configpath, config, "utf8"); for (let platform of platforms) { if (!platform) continue; promises.push( (async () => { console.log( `|--- Preparing platform ${platform.toUpperCase()} ---|`, ); if (id === ID_FREE) { console.log(`|--- Installing Admob ---|`); await exec( `cordova plugin add cordova-plugin-consent@2.4.0 --save`, ); await exec( `cordova plugin add admob-plus-cordova@2.0.0-alpha.19 --save --variable APP_ID_ANDROID="${AD_APP_ID}" --variable PLAY_SERVICES_VERSION="23.2.0"`, ); console.log("DONE! Installing admob-plus-cordova"); } else { console.log(`|--- Removing Admob ---|`); await exec(`cordova plugin remove cordova-plugin-consent --save`); await exec(`cordova plugin remove admob-plus-cordova --save`); console.log("DONE! Removing admob-plus-cordova"); } console.log(`|--- Reinstalling platform ---|`); const { stderr } = await exec(`npm run clean`); if (stderr) console.error(stderr); else console.log("DONE! Reinstalling platform"); })(), ); } await Promise.all(promises); } process.exit(0); } catch (error) { console.error(error); process.exit(1); } })(); ================================================ FILE: utils/custom-loaders/html-tag-jsx-loader.js ================================================ /** * Custom loader that transforms JSX to html-tag-js tag() calls * This uses Babel's parser/transformer but is lighter than full babel-loader */ const { parse } = require("@babel/parser"); const traverse = require("@babel/traverse").default; const generate = require("@babel/generator").default; const t = require("@babel/types"); module.exports = function htmlTagJsxLoader(source) { const callback = this.async(); // Enable caching for this loader this.cacheable && this.cacheable(); try { // Debug logging - verify loader is running // console.log(`🔧 Custom JSX loader processing: ${this.resourcePath}\n`); // Determine file type from extension const isTypeScript = /\.tsx?$/.test(this.resourcePath); // Quick check: if no JSX syntax at all, pass through unchanged // Look for complete JSX opening tags with proper spacing const hasJSXLike = /<\/?[A-Z][a-zA-Z0-9]*[^>]*>|<\/?[a-z][a-z0-9-]*[^>]*>/.test(source); if (!hasJSXLike) { return callback(null, source); } // Parse with appropriate plugins const parserPlugins = ["jsx"]; if (isTypeScript) { parserPlugins.push("typescript"); } const ast = parse(source, { sourceType: "module", plugins: parserPlugins, }); // Track if we need to add the import let needsTagImport = false; let hasJSX = false; const hasExistingImport = /import\s+(?:\{[^}]*\btag\b[^}]*\}|tag(?:\s+as\s+\w+)?)\s+from\s+['"]html-tag-js['"]/.test( source, ) || /(?:const|let|var)\s+(?:\{[^}]*\btag\b[^}]*\}|tag)\s*=\s*require\s*\(\s*['"]html-tag-js['"]\s*\)/.test( source, ); // Transform JSX elements traverse(ast, { JSXFragment(path) { hasJSX = true; needsTagImport = true; const { node } = path; const { children: childrenNode } = node; const children = []; populateChildren(childrenNode, children, t); const arrayExpression = t.arrayExpression(children); path.replaceWith(arrayExpression); }, JSXElement(path) { hasJSX = true; needsTagImport = true; const { node } = path; const { openingElement: el, children: childrenNode } = node; let { name: tagName } = el.name; const { attributes } = el; let id; let className; const on = []; const args = []; const attrs = []; const children = []; const options = []; const events = {}; let isComponent = /^(?:[A-Z][a-zA-Z0-9_$]*|(?:[a-zA-Z_$][a-zA-Z0-9_$]*\.)+[a-zA-Z_$][a-zA-Z0-9_$]*)$/.test( tagName, ); if (el.name.type === "JSXMemberExpression") { const { object, property } = el.name; tagName = `${object.name}.${property.name}`; isComponent = true; } populateChildren(childrenNode, children, t); for (const attr of attributes) { if (attr.type === "JSXSpreadAttribute") { if (isComponent) { attrs.push(t.spreadElement(attr.argument)); } else { options.push(t.spreadElement(attr.argument)); } continue; } let { name, namespace } = attr.name; if (!isComponent) { if (name === "id") { if (attr.value && attr.value.type === "StringLiteral") { id = attr.value; } else if ( attr.value && attr.value.type === "JSXExpressionContainer" ) { id = attr.value.expression; } continue; } if (["class", "className"].includes(name)) { if (attr.value && attr.value.type === "StringLiteral") { className = attr.value; } else if ( attr.value && attr.value.type === "JSXExpressionContainer" ) { className = attr.value.expression; } continue; } } if (namespace) { namespace = namespace.name; name = name.name; } if (!attr.value) { attrs.push( t.objectProperty(t.stringLiteral(name), t.stringLiteral("")), ); continue; } const { type } = attr.value; const isAttr = /-/.test(name); let value; if (type === "StringLiteral") { value = attr.value; } else { value = attr.value.expression; } if (namespace) { if (!["on", "once", "off"].includes(namespace)) { attrs.push( t.objectProperty( t.stringLiteral( namespace === "attr" ? name : `${namespace}:${name}`, ), value, ), ); continue; } if (namespace === "off") continue; if (!events[name]) { events[name] = []; on.push( t.objectProperty( t.stringLiteral(name), t.arrayExpression(events[name]), ), ); } events[name].push(value); continue; } if (isAttr) { const attrRegex = /^attr-(.+)/; if (attrRegex.test(name)) { [, name] = attrRegex.exec(name); } attrs.push(t.objectProperty(t.stringLiteral(name), value)); continue; } (isComponent ? attrs : options).unshift( t.objectProperty(t.identifier(name), value), ); } if (isComponent) { args.push(t.identifier(tagName)); if (on.length > 0) { attrs.push( t.objectProperty(t.identifier("on"), t.objectExpression(on)), ); } if (attrs.length > 0) { args.push(t.objectExpression(attrs)); } if (children.length > 0) { args.push(t.arrayExpression(children)); } } else { args.push(t.stringLiteral(tagName)); if (on.length > 0) { options.push( t.objectProperty(t.identifier("on"), t.objectExpression(on)), ); } if (attrs.length > 0) { options.push( t.objectProperty(t.identifier("attr"), t.objectExpression(attrs)), ); } if (id || className) { if (className) { args.push(className); } else { args.push(t.nullLiteral()); } if (id) { args.push(id); } else if (className) { // Push null for id when we have className but no id args.push(t.nullLiteral()); } } if (children.length) { args.push(t.arrayExpression(children)); } if (options.length) { args.push(t.objectExpression(options)); } } const identifier = t.identifier("tag"); const callExpression = t.callExpression(identifier, args); path.replaceWith(callExpression); }, }); // If no JSX was found, return original source if (!hasJSX) { return callback(null, source); } // Generate the transformed code const output = generate( ast, { sourceMaps: true, sourceFileName: this.resourcePath, retainLines: false, compact: false, }, source, ); // Add import if needed if (needsTagImport && !hasExistingImport) { output.code = `import tag from 'html-tag-js';\n${output.code}`; } callback(null, output.code, output.map); } catch (error) { const errorMessage = `html-tag-jsx-loader failed to process ${this.resourcePath}: ${error.message}`; const enhancedError = new Error(errorMessage); enhancedError.stack = error.stack; callback(enhancedError); } }; /** * Parse node to expression */ function parseNode(types, node) { const { type } = node; if (type === "JSXText") { const trimmed = node.value.trim(); if (!trimmed) return null; // Preserve original text if it contains non-whitespace // This maintains intentional spacing like "Hello " in Hello return types.stringLiteral(node.value); } if (type === "JSXElement") { return node; } const { expression } = node; const invalidExpressions = ["JSXEmptyExpression"]; if (invalidExpressions.includes(expression.type)) { return null; } return expression; } /** * Populate children */ function populateChildren(childrenNode, children, t) { for (let node of childrenNode) { node = parseNode(t, node); if (!node) continue; children.push(node); } } ================================================ FILE: utils/lang.js ================================================ const path = require("node:path"); const fs = require("node:fs"); const yargs = require("yargs"); const { hideBin } = require("yargs/helpers"); const readline = require("node:readline"); const args = yargs(hideBin(process.argv)) .alias("a", "all") .alias("b", "bulk").argv; const dir = path.resolve(__dirname, "../src/lang"); const read = readline.createInterface({ input: process.stdin, output: process.stdout, }); const enLang = path.join(dir, "en-us.json"); const list = fs.readdirSync(dir); const len = list.length; let command = ""; let arg = ""; let val = ""; if (args._.length > 3) { console.error("Invalid arguments", args._); process.exit(0); } else { command = args._[0]; arg = args._[1]; val = args._[2]; } switch (command) { case "add": case "remove": case "update": case "update-key": case "search": case "check": update(); break; case "add-all": addToAllFiles(); break; case "bulk-add": bulkAddStrings(); break; default: console.error(`Missing/Invalid arguments. use 'add' to add a new string use 'add-all ' to add the same string to ALL language files at once use 'bulk-add ' to add multiple strings from a JSON file to all language files use 'remove' to remove a string use 'search' to search a string use 'update' to update a string use 'update-key' to update a key use 'check' to check a string`); process.exit(); } /** * Adds a key-value pair to ALL language files at once * Usage: pnpm lang add-all "key" "value" */ function addToAllFiles() { if (!arg || !val) { console.error('Usage: pnpm lang add-all "" ""'); console.error('Example: pnpm lang add-all "hello world" "Hello World"'); process.exit(1); } const key = arg.toLowerCase(); let addedCount = 0; let skippedCount = 0; for (const lang of list) { const file = path.resolve(dir, lang); const text = fs.readFileSync(file, "utf8"); const strings = JSON.parse(text); if (key in strings) { console.log(`${lang}: Skipped (already exists)`); skippedCount++; continue; } strings[key] = val; const newText = JSON.stringify(strings, undefined, 2); fs.writeFileSync(file, newText, "utf8"); console.log(`${lang}: Added ✓`); addedCount++; } console.log( `\nDone! Added to ${addedCount} files, skipped ${skippedCount} files.`, ); process.exit(0); } /** * Bulk add multiple strings from a JSON file to ALL language files * Usage: pnpm lang bulk-add strings.json * * JSON file format: * { * "key1": "value1", * "key2": "value2" * } */ function bulkAddStrings() { if (!arg) { console.error("Usage: pnpm lang bulk-add "); console.error("Example: pnpm lang bulk-add new-strings.json"); console.error("\nJSON file format:"); console.error("{"); console.error(' "key1": "value1",'); console.error(' "key2": "value2"'); console.error("}"); process.exit(1); } const jsonFilePath = path.resolve(process.cwd(), arg); if (!fs.existsSync(jsonFilePath)) { console.error(`File not found: ${jsonFilePath}`); process.exit(1); } let newStrings; try { const jsonContent = fs.readFileSync(jsonFilePath, "utf8"); newStrings = JSON.parse(jsonContent); } catch (err) { console.error(`Error parsing JSON file: ${err.message}`); process.exit(1); } const keys = Object.keys(newStrings); if (keys.length === 0) { console.error("No strings found in the JSON file."); process.exit(1); } console.log( `Adding ${keys.length} strings to ${list.length} language files...\n`, ); for (const lang of list) { const file = path.resolve(dir, lang); const text = fs.readFileSync(file, "utf8"); const strings = JSON.parse(text); let addedCount = 0; let skippedCount = 0; for (const key of keys) { const lowerKey = key.toLowerCase(); if (lowerKey in strings) { skippedCount++; continue; } strings[lowerKey] = newStrings[key]; addedCount++; } if (addedCount > 0) { const newText = JSON.stringify(strings, undefined, 2); fs.writeFileSync(file, newText, "utf8"); } console.log(`${lang}: Added ${addedCount}, Skipped ${skippedCount}`); } console.log(`\nDone! Added ${keys.length} strings to all language files.`); process.exit(0); } async function update() { let key; if (command === "check") { let total = 0; let done = 0; fs.readFile(enLang, "utf-8", (err, data) => { if (err) { console.error(err); process.exit(0); return; } let error = false; const fix = arg === "fix"; const enLangData = JSON.parse(data); list.forEach((file, i) => { if (file === "en-us.json") return; let flagError = false; let langFile = path.join(dir, file); const exit = (i, len) => { if (i + 1 === len) { if (!error) { console.log("\nGOOD NEWS! No Error Found\n"); } process.exit(0); } }; fs.readFile(langFile, "utf-8", (err, data) => { if (err) { console.error(err); process.exit(1); return; } let langError = () => { if (!flagError) { error = true; flagError = true; console.log(`-------------- ${file}`); } }; const langData = JSON.parse(data); flagError = false; for (let enKey in enLangData) { const key = Object.keys(langData).find((k) => { try { if (new RegExp(`^${escapeRegExp(k)}$`, "i").test(enKey)) { return true; } return false; } catch (e) { console.log({ e, k }); return false; } }); if (!key) { langError(); if (fix) { langData[enKey] = enLangData[enKey]; } console.log(`Missing: ${enKey} ${fix ? "✔" : ""}`); } else if (key !== enKey) { langError(); console.log(`Fix: "${key} --> ${enKey}" ${fix ? "✔" : ""}`); if (fix) { const val = langData[key]; delete langData[key]; langData[enKey] = val; } } } if (flagError) { if (fix) { total += 1; const langJSONData = JSON.stringify(langData, undefined, 2); fs.writeFile(langFile, langJSONData, (err) => { if (err) { console.error(err); process.exit(1); } done += 1; exit(done, total); }); } console.log("\n"); } if (!fix) { exit(i, len); } }); }); }); return; } if (!arg) { getStr("string: ").then((res) => { key = res.toLowerCase(); arg = res; askTranslation(); }); return; } key = arg.toLowerCase(); let newKey = val; askTranslation(); if (command === "update-key" && !newKey) { newKey = await getStr("new key: "); } function askTranslation(i = 0) { const lang = list[i]; const langName = lang.split(".")[0]; if (command === "add") { if (!args.a) { getStr(`${langName}: `).then(addString); return; } addString(); } else if (command === "remove") { update((strings) => { if (key in strings) { delete strings[key]; console.log(`Removed: ${key}`); return strings; } else { console.error("String not exists"); } }); } else if (command === "update-key") { update((strings) => { const val = strings[key]; delete strings[key]; strings[newKey] = val; return strings; }); } else if (command === "update") { if (val) { update((strings) => { strings[key] = val; return strings; }); } else { getStr(`${langName}: `).then((res) => { res = res || arg; update((strings) => { strings[key] = res; return strings; }); }); } } else if (command === "search") { update((string) => { if (key in string) console.log(`${key}(${langName}): ${string[key]}`); else { console.log(`${key} not exists`); process.exit(); } }); } function update(modify) { const file = path.resolve(dir, lang); const text = fs.readFileSync(file, "utf8"); const strings = modify(JSON.parse(text)); if (strings) { const newText = JSON.stringify(strings, undefined, 2); fs.writeFile(file, newText, "utf8", (err) => { if (err) { console.error(err); process.exit(1); } next(); }); } else { next(); } function next() { if (i === list.length - 1) { process.exit(); } else { askTranslation(++i); } } } function addString(string) { string = string || arg; update((strings) => { if (key in strings) { console.error("String already exists"); process.exit(1); } else { strings[key] = string; return strings; } }); } } } function getStr(str) { return new Promise((resolve, reject) => { if (val) { resolve(val); return; } read.question(str, (res) => { resolve(res); }); }); } function escapeRegExp(text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } ================================================ FILE: utils/loadStyles.js ================================================ const fs = require("node:fs"); const path = require("node:path"); const WWW = path.resolve(__dirname, "../www"); const CSS = path.resolve(WWW, "css/build"); const CSS_PATH = "./css/build/"; const cssFiles = fs.readdirSync(CSS).filter((file) => file.endsWith(".css")); const htmlFiles = fs.readdirSync(WWW).filter((file) => file.endsWith(".html")); try { for (let htmlFile of htmlFiles) { loadStyles(path.resolve(WWW, htmlFile)); } } catch (error) { console.error(error); process.exit(1); } console.log("Styles loaded"); process.exit(0); /** * * @param {String} htmlFile */ function loadStyles(htmlFile) { let styles = ""; for (let cssFile of cssFiles) { styles += `\n`; } styles += "\n\n"; const rgx = /([^<]*(?:<(?!!--styles_end-->)[^<]*)*)\n*/gim; let html = fs.readFileSync(htmlFile, "utf8"); html = html.replace(rgx, ""); html = html.replace("", styles + ""); fs.writeFileSync(htmlFile, html); } ================================================ FILE: utils/scripts/build.sh ================================================ #!/bin/bash # Default values app="paid" mode="d" fdroidFlag="" packageType="apk" # New default: apk or aar webpackmode="development" cordovamode="" # Check all arguments for specific values for arg in "$@"; do case "$arg" in "free"|"paid") app="$arg" ;; "p"|"prod"|"d"|"dev") mode="$arg" ;; "fdroid") fdroidFlag="fdroid" ;; "apk"|"bundle") packageType="$arg" ;; *) echo "Warning: Unknown argument '$arg' ignored" ;; esac done root=$(npm prefix) if [ -n "$TMPDIR" ] && [ -r "$TMPDIR" ] && [ -w "$TMPDIR" ]; then tmpdir="$TMPDIR" elif [ -r "/tmp" ] && [ -w "/tmp" ]; then tmpdir="/tmp" else echo "Warning: No usable temporary directory found (TMPDIR or /tmp not accessible). Skipping fdroid.bool file." >&2 tmpdir="" fi if [ "$fdroidFlag" = "fdroid" ]; then if [ -n "$tmpdir" ]; then echo "true" > "$tmpdir/fdroid.bool" fi # Remove only if installed if [ -d "plugins/com.foxdebug.acode.rk.exec.proot" ]; then cordova plugin remove com.foxdebug.acode.rk.exec.proot fi else if [ -n "$tmpdir" ]; then echo "false" > "$tmpdir/fdroid.bool" fi # Add only if the src exists and not already installed if [ -d "src/plugins/proot" ] && [ ! -d "plugins/com.foxdebug.acode.rk.exec.proot" ]; then cordova plugin add src/plugins/proot/ fi fi # Normalize mode values if [ "$mode" = "p" ] || [ "$mode" = "prod" ] then mode="p" webpackmode="production" cordovamode="--release" fi # Set build target based on packageType if [ "$packageType" = "bundle" ]; then echo "Building AAR library file..." else echo "Building APK file..." fi RED='' NC='' script1="node ./utils/config.js $mode $app" script2="rspack --mode $webpackmode" # script3="node ./utils/loadStyles.js" echo "type : $packageType" script4="cordova build android $cordovamode -- --packageType=$packageType" eval " echo \"${RED}$script1${NC}\"; $script1; echo \"${RED}$script2${NC}\"; $script2&& # echo \"${RED}$script3${NC}\"; # $script3; echo \"${RED}$script4${NC}\"; $script4; " ================================================ FILE: utils/scripts/clean.sh ================================================ #! /bin/bash platform_rm=$1 platform_add=$2 if [ -z "$platform_rm" ] then platform_rm="android" fi if [ -z "$platform_add" ] then platform_add="$platform_rm" fi eval " cordova platform rm $platform_rm; cordova platform add $platform_add " ================================================ FILE: utils/scripts/generate-release-notes.js ================================================ #!/usr/bin/env node /** * ✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025. * * GitHub Release Notes Generator * * Features: * - Auto categorizes commits by type * - Optional compact "plain" output to save space * - Option to include only important tags (feat, fix, refactor, perf) * - Option to use only merge commits * * Usage: * GITHUB_TOKEN= node generate-release-notes.js [options] * * Options: * --plain Output minimal Markdown (no emojis, compact) * --important-only Include only features, fixes, refactors, and perf * --merge-only Include only merge commits * --help Show usage * --format [md/json] Output Format * --fromTag v1.11.0 The From/Previous Tag * --quiet Suppress output to stdout */ const args = process.argv.slice(2); function getArgValue(flag) { const idx = args.indexOf(flag); return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("-") ? args[idx + 1] : null; } if (args.includes("--help") || args.length < 3) { console.log(` Usage: GITHUB_TOKEN= node generate-release-notes.js [options] ✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025 Options: --plain Compact, no emojis (saves space) --important-only Only include Features, Fixes, Refactors, Perf --merge-only Include only merge commits --help Show this help message --format [md/json] Output Format --from-tag v1.11.0 The From/Previous Tag --quiet Suppress output to stdout --stdout-only Output to stdout only --changelog-only Output changelog only `); process.exit(0); } const [owner, repo, currentTag, previousTagArg] = args; const token = process.env.GITHUB_TOKEN; if (!token) { console.error("❌ Missing GITHUB_TOKEN environment variable."); process.exit(1); } const flags = { plain: args.includes("--plain"), importantOnly: args.includes("--important-only"), mergeOnly: args.includes("--merge-only"), quiet: args.includes("--quiet") || args.includes("--stdout-only"), format: getArgValue("--format") || "md", fromTag: getArgValue("--from-tag"), changelogOnly: args.includes("--changelog-only"), }; function log(...msg) { if (!flags.quiet) console.error(...msg); } const headers = { Authorization: `token ${token}`, Accept: "application/vnd.github+json", "User-Agent": "release-notes-script", }; async function getPreviousTag() { const res = await fetch( `https://api.github.com/repos/${owner}/${repo}/tags`, { headers }, ); const tags = await res.json(); if (!Array.isArray(tags) || tags.length < 2) return null; return tags[1].name; } async function getCommits(previousTag, currentTag) { const url = `https://api.github.com/repos/${owner}/${repo}/compare/${previousTag}...${currentTag}`; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed to fetch commits: ${res.status}`); const data = await res.json(); return data.commits || []; } function categorizeCommits(commits, { mergeOnly, importantOnly }) { const sections = { feat: [], fix: [], perf: [], refactor: [], docs: [], chore: [], test: [], add: [], revert: [], update: [], other: [], }; for (const c of commits) { const msg = c.commit.message.split("\n")[0]; const isMerge = msg.startsWith("Merge pull request") || msg.startsWith("Merge branch"); if (mergeOnly && !isMerge) continue; const type = Object.keys(sections).find((k) => { const lowerMsg = msg.toLowerCase(); return ( lowerMsg.startsWith(`${k}:`) || lowerMsg.startsWith(`${k} `) || lowerMsg.startsWith(`${k}: `) || lowerMsg.startsWith(`${k}(`) // handles e.g. 'feat(plugin-api): ...' ); }) || "other"; if ( importantOnly && !["feat", "fix", "refactor", "perf", "add", "revert", "update"].includes( type, ) ) continue; const author = c.author?.login ? `[${c.author.login}](https://github.com/${c.author.login})` : "unknown"; const entry = `- ${msg} (${c.sha.slice(0, 7)}) by ${author}`; sections[type].push(entry); } return sections; } const emojis = { feat: flags.plain ? "" : "✨ ", fix: flags.plain ? "" : "🐞 ", perf: flags.plain ? "" : "⚡ ", refactor: flags.plain ? "" : "🔧 ", docs: flags.plain ? "" : "📝 ", chore: flags.plain ? "" : "🧹 ", test: flags.plain ? "" : "🧪 ", other: flags.plain ? "" : "📦 ", revert: flags.plain ? "" : "⏪ ", add: flags.plain ? "" : "➕ ", update: flags.plain ? "" : "🔄 ", }; function formatMarkdown(tag, prevTag, sections, { plain }) { const lines = [ flags.changelogOnly ? "" : `Changes since [${prevTag}](https://github.com/${owner}/${repo}/releases/tag/${prevTag})`, "", ]; for (const [type, list] of Object.entries(sections)) { if (list.length === 0) continue; const header = plain ? `## ${type}` : `## ${emojis[type]}${type[0].toUpperCase() + type.slice(1)}`; lines.push(header, "", list.join("\n"), ""); } // Compact single-line mode for super small output // if (plain) { // const compact = Object.entries(sections) // .filter(([_, list]) => list.length) // .map(([type, list]) => `${type.toUpperCase()}: ${list.length} commits`) // .join(" | "); // lines.push(`\n_Summary: ${compact}_`); // } return lines.join("\n"); } function formatJSON(tag, prevTag, sections, plain = true) { const lines = [ "", flags.changelogOnly ? "" : `Changes since [${prevTag}](https://github.com/${owner}/${repo}/releases/tag/${prevTag})`, "", ]; // todo: split into function for (const [type, list] of Object.entries(sections)) { if (list.length === 0) continue; const header = plain ? `## ${type}` : `## ${emojis[type]}${type[0].toUpperCase() + type.slice(1)}`; lines.push(header, "", list.join("\n"), ""); } return JSON.stringify( { release: tag, previous: prevTag, sections: Object.fromEntries( Object.entries(sections).filter(([_, v]) => v.length), ), notes: lines.join("\n"), }, null, 2, ); } async function main() { log(`🔍 Generating release notes for ${owner}/${repo} @ ${currentTag}...`); const prevTag = flags.fromTag || (await getPreviousTag()); if (!prevTag) { console.error("No previous tag found. Use --from-tag to specify one."); process.exit(1); } const commits = await getCommits(prevTag, currentTag); if (!commits.length) { console.error("No commits found."); process.exit(1); } const categorized = categorizeCommits(commits, flags); let output; if (flags.format === "json") { output = formatJSON(currentTag, prevTag, categorized); } else { output = formatMarkdown(currentTag, prevTag, categorized, flags); } process.stdout.write(output + "\n"); } main().catch((err) => console.error(err)); ================================================ FILE: utils/scripts/plugin.sh ================================================ #! /bin/bash if [ -z "$2" ] then eval "cordova plugin rm $1; cordova plugin add $1" else eval "cordova plugin rm $1; cordova plugin add $2" fi ================================================ FILE: utils/scripts/setup.sh ================================================ echo "Setting up the project..." npm install cordova platform add android@10 cordova prepare mkdir -p www/css/build www/js/build ================================================ FILE: utils/scripts/start.sh ================================================ #! /bin/bash platform="$1" app="$2" mode="$3" webpackmode="development" cordovamode="" if [ -z "$platform" ] then platform="android" fi if [ -z "$mode" ] then mode="d" fi if [ -z "$app" ] then app="paid" fi if [ "$mode" = "p" ] then webpackmode="production" cordovamode="--release" fi RED='' NC='' script1="node ./utils/config.js $mode $app" script2="rspack --mode $webpackmode" # script3="node ./utils/loadStyles.js" script4="cordova run $platform $cordovamode" eval " echo \"${RED}$script1${NC}\"; $script1; echo \"${RED}$script2${NC}\"; $script2&& # echo \"${RED}$script3${NC}\"; # $script3; echo \"${RED}$script4${NC}\"; $script4 " ================================================ FILE: utils/setup.js ================================================ // setup acode for the first time // 1. install dependencies // 2. add cordova platform android@10.2 // 3. install cordova plugins // cordova-plugin-buildinfo // cordova-plugin-device // cordova-plugin-file // all the plugins in ./src/plugins const { execSync } = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); const PLATFORM_FILES = [".DS_Store"]; execSync("npm install", { stdio: "inherit" }); try { execSync("cordova platform add android", { stdio: "inherit" }); } catch (error) { // ignore } try { execSync("mkdir -p www/css/build www/js/build", { stdio: "inherit" }); } catch (error) { console.log( "Failed to create www/css/build & www/js/build directories (You may Try after reading The Error)", error, ); } execSync("cordova plugin add cordova-plugin-buildinfo", { stdio: "inherit" }); execSync("cordova plugin add cordova-plugin-device", { stdio: "inherit" }); execSync("cordova plugin add cordova-plugin-file", { stdio: "inherit" }); const plugins = fs.readdirSync(path.join(__dirname, "../src/plugins")); plugins.forEach((plugin) => { if (PLATFORM_FILES.includes(plugin) || plugin.startsWith(".")) return; execSync(`cordova plugin add ./src/plugins/${plugin}`, { stdio: "inherit" }); }); ================================================ FILE: utils/storage_manager.mjs ================================================ #!/usr/bin/env node import readline from 'node:readline'; import { stdin as input, stdout as output } from 'node:process'; import { readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { execSync } from 'node:child_process'; const npmPrefix = execSync('npm prefix').toString().trim(); const pluginXmlPath = join(npmPrefix, 'src/plugins/terminal/plugin.xml'); const permissionLine = ` `; const permissionRegex = /^\s*\s*$/gm; async function addPermission() { try { let xml = await readFile(pluginXmlPath, 'utf-8'); if (xml.includes('android.permission.MANAGE_EXTERNAL_STORAGE')) { console.log('Permission already exists in plugin.xml'); return false; } const pattern = /\s*<\/config-file>/; if (pattern.test(xml)) { xml = xml.replace(pattern, match => { return match.replace( '', `\n${permissionLine}\n ` ); }); await writeFile(pluginXmlPath, xml, 'utf-8'); console.log('Permission added inside existing block.'); return true } else { console.error('Could not find block.'); return false } } catch (err) { console.error('Failed to add permission:', err); return false } } async function removePermission() { try { let xml = await readFile(pluginXmlPath, 'utf-8'); if (!xml.includes('android.permission.MANAGE_EXTERNAL_STORAGE')) { console.log('Permission not found — nothing to remove.'); return false; } const cleanedXml = xml.replace(permissionRegex, ''); await writeFile(pluginXmlPath, cleanedXml, 'utf-8'); console.log('Permission removed from plugin.xml'); return true } catch (err) { console.error('Failed to remove permission:', err); return false } } function updatePlugin() { try { const prefix = execSync('npm prefix').toString().trim(); const pluginPath = join(prefix, 'src/plugins/terminal'); execSync('cordova plugin remove com.foxdebug.acode.rk.exec.terminal', { stdio: 'inherit' }); execSync(`cordova plugin add "${pluginPath}"`, { stdio: 'inherit' }); console.log('✅ Plugin updated successfully.'); } catch (err) { console.error(err.message); process.exit(1); } } async function handleAnswer(answer) { answer = answer.trim().toLowerCase(); if (answer === 'yes' || answer === 'y') { if(await addPermission()){ updatePlugin() } } else if (answer === 'no' || answer === 'n') { if(await removePermission()){ updatePlugin() } } else { console.error("Invalid input. Please type 'yes' or 'no'."); process.exit(1); } } function prompt() { const rl = readline.createInterface({ input, output }); rl.question("Enable 'MANAGE_EXTERNAL_STORAGE' permission? Y/n: ", async (answer) => { rl.close(); await handleAnswer(answer); }); } const args = process.argv.slice(2); let answer = null; if (args.includes('--yes') || args.includes('-y')) { answer = 'yes'; } else if (args.includes('--no') || args.includes('-n')) { answer = 'no'; } else if (args[0]) { answer = args[0]; } if (answer) { await handleAnswer(answer); } else { prompt(); } ================================================ FILE: utils/updateAce.js ================================================ const fs = require("node:fs"); const path = require("node:path"); const sourceDir = "./ace-builds/src-min"; const destDir = "./www/js/ace"; function updateAce() { try { // Remove existing destination directory if it exists if (fs.existsSync(destDir)) { fs.rmSync(destDir, { recursive: true }); console.log("Removed existing destination directory"); } // Create destination directory fs.mkdirSync(destDir, { recursive: true }); console.log("Created new destination directory"); // Read all files from source directory const files = fs.readdirSync(sourceDir); files.forEach((file) => { const sourcePath = path.join(sourceDir, file); const destPath = path.join(destDir, file); // Skip snippets directory and worker files if (file === "snippets" || file.startsWith("worker-")) { console.log(`Skipping: ${file}`); return; } // Copy if it's a file if (fs.statSync(sourcePath).isFile()) { fs.copyFileSync(sourcePath, destPath); console.log(`Copied: ${file}`); } }); console.log("Ace editor files updated successfully!"); } catch (error) { console.error("Error updating Ace editor files:", error); } } updateAce(); ================================================ FILE: webpack.config.js ================================================ const path = require('path'); const fs = require('fs'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const WWW = path.resolve(__dirname, 'www'); module.exports = (env, options) => { const { mode = 'development' } = options; const rules = [ { test: /\.tsx?$/, exclude: /node_modules/, use: [ 'html-tag-js/jsx/tag-loader.js', { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-typescript'], }, }, { loader: 'ts-loader', options: { transpileOnly: true, // Skip type checking for faster builds }, }, ], }, { test: /\.(hbs|md)$/, use: ['raw-loader'], }, { test: /\.m.(sa|sc|c)ss$/, use: [ 'raw-loader', 'postcss-loader', 'sass-loader', ], }, { test: /\.(png|svg|jpg|jpeg|ico|ttf|webp|eot|woff|webm|mp4|webp|wav)(\?.*)?$/, type: "asset/resource", }, { test: /(? Acode