diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dfe5310..62bb6f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,13 +11,14 @@ on: - beta - rc - release + - extensions tags: - "*" workflow_dispatch: env: - # default value, will be overwritten later if tag exists - APP_VERSION: "0.1.0" + # A default value, will be overwritten later if tag exists + APP_VERSION: "0.0.1" jobs: tauri: @@ -27,18 +28,46 @@ jobs: fail-fast: false matrix: include: - - name: "macOS-ARM" - platform: "macos-latest" # for Arm based macs (M1 and above). + - name: "macOS ARM" + identifier: "macos-arm" + platform: "macos-latest" # For Arm based macs (M1 and above). args: "--target aarch64-apple-darwin" - - name: "macOS-x86_64" - platform: "macos-latest" # for Intel based macs. + type: "non-portable" + - name: "macOS x86_64" + identifier: "macos-x86" + platform: "macos-latest" # For Intel based macs. args: "--target x86_64-apple-darwin" + type: "non-portable" - name: "Linux" + identifier: "linux" platform: "ubuntu-22.04" args: "" + type: "non-portable" - name: "Windows" + identifier: "windows" platform: "windows-latest" args: "" + type: "non-portable" + - name: "macOS ARM (Portable)" + identifier: "macos-arm" + platform: "macos-latest" # For Arm based macs (M1 and above). + args: "--target aarch64-apple-darwin" + type: "portable" + - name: "macOS x86_64 (Portable)" + identifier: "macos-x86" + platform: "macos-latest" # For Intel based macs. + args: "--target x86_64-apple-darwin" + type: "portable" + - name: "Linux (Portable)" + identifier: "linux" + platform: "ubuntu-22.04" + args: "" + type: "portable" + - name: "Windows (Portable)" + identifier: "windows" + platform: "windows-latest" + args: "" + type: "portable" runs-on: ${{ matrix.platform }} steps: @@ -67,22 +96,38 @@ jobs: sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - name: Use version from Github Tag (Non-Windows) - if: startsWith(github.ref, 'refs/tags/') && matrix.name != 'Windows' + if: startsWith(github.ref, 'refs/tags/') && matrix.identifier != 'windows' run: | TAG_NAME="${GITHUB_REF#refs/tags/}" echo "APP_VERSION=${TAG_NAME}" >> $GITHUB_ENV - name: Use version from Github Tag (Windows) - if: startsWith(github.ref, 'refs/tags/') && matrix.name == 'Windows' + if: startsWith(github.ref, 'refs/tags/') && matrix.identifier == 'windows' run: | $TAG_NAME = $env:GITHUB_REF -replace 'refs/tags/', '' echo "APP_VERSION=$TAG_NAME" >> $env:GITHUB_ENV + - name: Remove File System restrictions (Portable-only) + if: matrix.type == 'portable' + uses: restackio/update-json-file-action@6f50afee9a03a456a30cd574123db793319f7544 + with: + file: "./src-tauri/capabilities/plugin-fs.json" + fields: "{ + \"permissions[0].allow\": [{\"path\":\"**/*\"}] + }" + + - name: Mark window title as Portable (Portable-only) + if: matrix.type == 'portable' + uses: restackio/update-json-file-action@6f50afee9a03a456a30cd574123db793319f7544 + with: + file: "./src-tauri/tauri.conf.json" + fields: "{\"app.windows[0].title\": \"Kaede Portable\"}" + - name: Bump tauri.conf.json version uses: restackio/update-json-file-action@6f50afee9a03a456a30cd574123db793319f7544 with: file: "./src-tauri/tauri.conf.json" - fields: '{"version": "${{ env.APP_VERSION }}"}' + fields: "{\"version\": \"${{ env.APP_VERSION }}\"}" - name: Bump Cargo.toml version uses: colt-1/toml-editor@da6b46ee7779ed730d2160393ed95fb20e82696d @@ -100,6 +145,15 @@ jobs: - name: Install frontend dependencies run: bun install + - name: Run TypeScript checks + run: bun run typecheck + + - name: Run ESLint + run: bun run lint + + - name: Run Vitest + run: bun run test + - name: Build a Tauri app (Non-Release) if: (startsWith(github.ref, 'refs/tags/')) != true uses: tauri-apps/tauri-action@564aea5a8075c7a54c167bb0cf5b3255314a7f9d @@ -121,81 +175,81 @@ jobs: args: ${{ matrix.args }} - name: Upload binary (Windows, NSIS) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'Windows' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'windows' && matrix.type != 'portable' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-windows-nsis-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-windows-nsis-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/release/bundle/nsis/${{ steps.json_properties.outputs.productName }}_${{steps.json_properties.outputs.version}}_x64-setup.exe" - name: Upload binary (Windows, MSI) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'Windows' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'windows' && matrix.type != 'portable' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-windows-msi-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-windows-msi-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/release/bundle/msi/${{ steps.json_properties.outputs.productName }}_${{steps.json_properties.outputs.version}}_x64_en-US.msi" - - name: Upload binary (Windows, portable) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'Windows' + - name: Upload binary (Windows, non-setup) + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'windows' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-windows-portable-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-windows-non-setup-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/release/${{ steps.json_properties.outputs.productName }}.exe" - name: Upload binary (Linux, DEB) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'Linux' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'linux' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-linux-deb-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-linux-deb-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/release/bundle/deb/${{ steps.json_properties.outputs.productName }}_${{steps.json_properties.outputs.version}}_amd64.deb" - name: Upload binary (Linux, RPM) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'Linux' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'linux' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-linux-rpm-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-linux-rpm-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/release/bundle/rpm/${{ steps.json_properties.outputs.productName }}-${{steps.json_properties.outputs.version}}-1.x86_64.rpm" - name: Upload binary (Linux, AppImage) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'Linux' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'linux' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-linux-app-image-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-linux-app-image-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/release/bundle/appimage/${{ steps.json_properties.outputs.productName }}_${{steps.json_properties.outputs.version}}_amd64.AppImage" - name: Upload binary (macOS, x86_64, DMG) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'macOS-x86_64' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'macos-x86' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-macos-x86_64-dmg-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-macos-x86_64-dmg-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/${{ steps.json_properties.outputs.productName }}_${{steps.json_properties.outputs.version}}_x64.dmg" - name: Upload binary (macOS, x86_64, tarball) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'macOS-x86_64' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'macos-x86' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-macos-x86_64-tarball-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-macos-x86_64-tarball-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/x86_64-apple-darwin/release/bundle/macos/${{ steps.json_properties.outputs.productName }}.app.tar.gz" - name: Upload binary (macOS, ARM, DMG) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'macOS-ARM' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'macos-arm' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-macos-arm-dmg-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-macos-arm-dmg-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/${{ steps.json_properties.outputs.productName }}_${{steps.json_properties.outputs.version}}_aarch64.dmg" - name: Upload binary (macOS, ARM, tarball) - if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.name == 'macOS-ARM' + if: (startsWith(github.ref, 'refs/tags/') != true) && matrix.identifier == 'macos-arm' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: if-no-files-found: "warn" - name: "${{ steps.json_properties.outputs.productName }}-nightly-macos-arm-tarball-${{steps.json_properties.outputs.version}}-${{ github.ref_name }}" + name: "${{ steps.json_properties.outputs.productName }}-dev-macos-arm-tarball-${{steps.json_properties.outputs.version}}-${{ matrix.type }}-${{ github.ref_name }}" path: "./src-tauri/target/aarch64-apple-darwin/release/bundle/macos/${{ steps.json_properties.outputs.productName }}.app.tar.gz" diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml deleted file mode 100644 index 5e73a80..0000000 --- a/.github/workflows/eslint.yml +++ /dev/null @@ -1,47 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# ESLint is a tool for identifying and reporting on patterns -# found in ECMAScript/JavaScript code. -# More details at https://github.com/eslint/eslint -# and https://eslint.org - -name: ESLint - -on: - push: - branches: ["nightly"] - pull_request: - # The branches below must be a subset of the branches above - branches: ["nightly"] - schedule: - - cron: "0 0 * * 0" - -jobs: - eslint: - name: Run eslint scanning - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: lts/* - - - name: Setup Bun - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 - with: - bun-version: latest - - - name: Install Dependencies - run: bun install - - - name: Run ESLint - run: bun lint diff --git a/.gitignore b/.gitignore index 5d3f2ce..a80afc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Development +public/ + # Logs logs *.log diff --git a/bun.lockb b/bun.lockb index 310cdd3..f8bf63d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 8b3953e..34e8e89 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,3 +1,5 @@ +[README for JavaScript-related code](../src/README.md) | [README for Rust-related code](../src-tauri/README.md) | Contributing Guidelines + # Contributions Guidelines ## Note @@ -10,7 +12,25 @@ Translations are done externally via a [Kaede Translations repository](https://g ## Extensions -tbd +You need this section only if you want to develop an extension. + +### Repositories + +Kaede has two built-in plugin repositories. + +The first one is a [Kaede Add-ons User Repository (KAUR)](https://github.com/kaede-basement/kaur), similar to [AUR](https://aur.archlinux.org/) and [nixpkgs](https://github.com/NixOS/nixpkgs). This repository contains user published extensions. + +The second one is a [trusted-extensions repository](https://github.com/kaede-basement/trusted-extensions). I publish my extensions there. Others may publish there too, but only by contacting me. A plugin publisher must provide me the plugin source code and build manuals. I will manually review the provided code and provide the feedback if something is not good. The reviewing procedure will happen each time a plugin publisher wants to update their extension in the repository. + +### Safety + +Extensions can be loaded in two environments. + +The first one is a restricted environment (sandbox) that uses a permission-based system. When enabling the plugin for the first time, users are prompted with the permissions window. That window has permission toggles that the plugin requested. KAUR extensions are executed in this environment. + +Restricted environment is achieved by using a [Secure ECMAScript](https://github.com/endojs/endo) framework. Each permission has its own list of globals that are passed to the plugin. Unfortunately, almost every DOM operation is prohibited since it leads to the sandbox escape. + +The second one is an unrestricted environment that allows plugins to do everything that the Kaede can do itself. Trusted extensions are executed in this environment. Settings also have an option to enable the execution of KAUR extensions that require an unrestricted environment. ## Code Formatting @@ -18,11 +38,13 @@ All files are formatted with [ESLint](https://eslint.org/) using the configurati Please also follow the project's conventions for frontend: +- No AI slops in the launcher code (plugins don't count). - TypeScript is highly recommended. - `.vue` file names should be formatted as `PascalCase`. All other files should use `kebab-case`. - Exported constants should be formatted as `PascalCase`. - Functions, variables, non-exported constants should be formatted as `camelCase`. -- Elements styling is preferred by using `Tailwind v3` classes. If there is no utility class for some case, make your own, or use CSS/JS. +- Element styling is preferred by using `Tailwind v3` classes. If there is no utility class for some cases, then make your own with CSS. +- [BEM](https://en.bem.info/methodology/) methodology is the preferred way to name element IDs and classes to simplify styling by extensions. All elements should have unique IDs. ## Commit Messages @@ -33,18 +55,18 @@ Please also follow the project's conventions for frontend: ### Elements - **Type**: Choose from the following list. If none of the types match, use `chore`. - - `feat`: A new feature - - `fix`: A bug fix - - `docs`: Documentation only changes - - `style`: Changes that do not affect the meaning of the code - - `refactor`: Improving code structure - - `perf`: A code change that improves performance - - `test`: Adding missing tests or correcting existing tests - - `build`: Changes that affect the build system or external dependencies - - `chore`: Other changes that don't modify src or test files - - `revert`: Reverts a previous commit - - `release`: Releasing a new version - - `ci`: Changes to our CI configuration + - `feat`: A new feature. + - `fix`: A bug fix. + - `docs`: Documentation only changes. + - `style`: Changes that do not affect the meaning of the code. + - `refactor`: Improving code structure. + - `perf`: A code change that improves performance. + - `test`: Adding missing tests or correcting existing tests. + - `build`: Changes that affect the build system or external dependencies. + - `chore`: Other changes that don't modify src or test files. + - `revert`: Reverts a previous commit. + - `release`: Releasing a new version. + - `ci`: Changes to our CI configuration. - **Scope**: As described in Conventional Commits. - **Breaking Change**: If you're introducing a breaking change, append `!` to the type or scope, e.g., `feat(ui)!: Breaking change`. @@ -52,5 +74,5 @@ Please also follow the project's conventions for frontend: ### Guidelines -- Use imperative mood, e.g., "add feature" instead of "adding feature" or "added feature". +- Use imperative mood, e.g. "add feature" instead of "adding feature" or "added feature". - Avoid ending with a period. diff --git a/docs/PLAN.md b/docs/PLAN.md index a4790ea..6d6f3fb 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -124,10 +124,10 @@ Launcher should expose everything that it can through the `window.__KAEDE__`. Pr For example, see the next code: ```ts -/** Launcher */ +/* Launcher */ window.postMessage("kaede-route-changed", /* some data */) -/** Extension */ +/* Extension */ const handleRouteChange = (pathname: string) => { if (pathname === "/home") { // do something @@ -151,17 +151,17 @@ While it works, it doesn't look good to me. Adding a lot of window listeners can Now I'm suggesting the next structure: ```ts -/** Launcher */ +/* Launcher */ window.__KAEDE__.hooks.onRouteChange.before = []; -/** Extension */ +/* Extension */ const currentRoute = ref("home"); window.__KAEDE__.hooks.onRouteChange.before.push(({ pathname }: { pathname: RouteType }) => { currentRoute.value = pathname; }); -/** Launcher */ +/* Launcher */ // example with the '@kitbag/router' onBeforeRouteChange(() => { for (const hook of window.__KAEDE__.hooks.onRouteChange.before) { diff --git a/docs/README.md b/docs/README.md index 78bb21d..ed8b2e5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,18 +1,20 @@
-Favicon + +My re-creation of Tendou Arisu's halo +

-Kaede +Kaede

-[WIP] An extensible Tauri-based Minecraft launcher written in Vue and Rust +An extensible Tauri-based Minecraft launcher written in Typescript with Vue

-English | Русский +English | Русский

-[![star-count]](https://github.com/freesmteam/kaede/stargazers) +[![star-count]](https://github.com/kaede-basement/kaede/stargazers) [![vue-badge]](https://vuejs.org/) [![tauri-badge]](https://v2.tauri.app/) [![unocss-badge]](https://unocss.dev/) @@ -21,15 +23,21 @@ ## Plans -Kaede is not even in a development stage yet. Check the [plan](./PLAN.md) to see more about this launcher +Kaede is in really early stages of development. Check the [plan](./PLAN.md) to see more about this launcher ## Contributing -[Contributing Guide](./CONTRIBUTING.md) +You don't need a Rust knowledge to contribute to this project. Almost everything was written in TypeScript using Tauri API. These files will help you: + +- [README for JavaScript-related code](../src/README.md) (the most important one) +- [README for Rust-related code](../src-tauri/README.md) +- [Contributing Guidelines](./CONTRIBUTING.md) + +I also leave a lot of comments in the code. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. -## Screenshots +## Demonstration
@@ -43,19 +51,85 @@ nothing here yet ## Features -tbd - -## Comparison +- Plugin system +- Cross-platform +- Available as Non-Portable/Portable +- Open Source, GPL-3.0 +- Written in TypeScript + +Not implemented yet: + +- [ ] Plugin system + - [ ] Custom CSS themes + - [ ] Permission-system + - [ ] Dependencies handling (?) + - [x] Application hooks + - [ ] Sandboxed environment using Secure ECMAScript + - [ ] Unrestricted environment for microfrontends with shared dependencies + - [ ] Unrestricted environment with `new Function` +- [ ] Authentication + - [ ] Microsoft authentication + - [ ] Offline accounts if user has a Microsoft account with the game + - [ ] Profile systems (?) (basically different launcher settings for different users) +- [ ] Instance management + - [ ] All Minecraft versions launch + - [ ] Instance import (from Prism Launcher, Modrinth, etc.) + - [ ] Instance export + - [ ] Sandboxed minecraft instances (?) +- [ ] Modpack providers support + - [ ] CurseForge + - [ ] Modrinth + - [ ] ATLauncher + - [ ] FTB + - [ ] Legacy FTB + - [ ] Technic +- [ ] Mod loaders + - [ ] Fabric + - [ ] Forge + - [ ] NeoForge + - [ ] Quilt + - [ ] Legacy Fabric + - [ ] LiteLoader + - [ ] Kaolin +- [ ] Resource management + - [ ] Mods + - [ ] CurseForge blocked download handling via spawning a webview window (?) + - [ ] Symlinks for identical mods (?) + - [ ] Resourcepacks + - [ ] Shaderpacks + - [ ] Worlds + - [ ] Datapacks +- [ ] Java management + - [ ] Already installed JDKs detection + - [ ] Different version selection for supported Minecraft versions + - [ ] Bundled GraalVM Community Edition JDK (?) +- [ ] Server management (via plugin) + - [ ] Various server cores (Bukkit-based, Sponge-based, Forge, Fabric, Minestom, etc.) + - [ ] Plugins management + - [ ] Mods management tbd (https://mc-launcher.tayou.org/) ## Installation -tbd +### Stable Releases + +Download Kaede from the [GitHub Releases](https://github.com/kaede-basement/kaede/releases) page. Packages are available for Linux, Windows, and macOS. + +### Development builds + +Please understand that these builds are not intended for most users. There may be bugs and other instabilities. You have been warned. + +There are development builds available through: + +- [GitHub Actions](https://github.com/kaede-basement/kaede/actions) (includes builds from pull requests opened by contributors). +- [nightly.link](https://nightly.link/kaede-basement/kaede/workflows/build/nightly) (this will always point only to the latest version of the `nightly` branch). + +Prebuilt Development builds are provided for Linux, Windows, and macOS. ## Community & Support -If you found a bug or want to suggest a feature, please open an issue in [GitHub Issues](https://github.com/FreesmTeam/FreesmLauncher/issues). Pull requests and contributions (code, docs, translations) are welcome! +If you found a bug or want to suggest a feature, please open an issue in [GitHub Issues](https://github.com/kaede-basement/kaede/issues). Pull requests and contributions (code, docs, translations) are welcome! ### Discord @@ -69,16 +143,16 @@ If you found a bug or want to suggest a feature, please open an issue in [GitHub See [Tauri v2 Prerequisites](https://v2.tauri.app/start/prerequisites/). -We also recommend installing [bun](https://bun.sh/). +I also recommend installing [bun](https://bun.sh/). Once you are ready, clone this repository: ```bash -git clone https://github.com/freesmteam/kaede +git clone https://github.com/kaede-basement/kaede ``` -Navigate to cloned directory and install project dependencies: +Navigate to the cloned directory and install project dependencies: ```bash bun install @@ -104,13 +178,13 @@ bun run build ## License -[![license-badge]](https://github.com/freesmteam/kaede/blob/main/LICENSE) +[![license-badge]](https://github.com/kaede-basement/kaede/blob/main/LICENSE) -[star-count]: https://img.shields.io/github/stars/freesmteam/kaede?label=Stars&style=for-the-badge&color=%23ff637e&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIC05NjAgOTYwIDk2MCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Im0zNTQtMjQ3IDEyNi03NiAxMjYgNzctMzMtMTQ0IDExMS05Ni0xNDYtMTMtNTgtMTM2LTU4IDEzNS0xNDYgMTMgMTExIDk3LTMzIDE0M1pNMjMzLTgwbDY1LTI4MUw4MC01NTBsMjg4LTI1IDExMi0yNjUgMTEyIDI2NSAyODggMjUtMjE4IDE4OSA2NSAyODEtMjQ3LTE0OUwyMzMtODBabTI0Ny0zNTBaIiBzdHlsZT0iZmlsbDogcmdiKDI1NSwgOTksIDEyNik7Ii8%2BCjwvc3ZnPg%3D%3D%0A +[star-count]: https://img.shields.io/github/stars/kaede-basement/kaede?label=Stars&style=for-the-badge&color=%23a1fee4&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIC05NjAgOTYwIDk2MCIgd2lkdGg9IjI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Im0zNTQtMjQ3IDEyNi03NiAxMjYgNzctMzMtMTQ0IDExMS05Ni0xNDYtMTMtNTgtMTM2LTU4IDEzNS0xNDYgMTMgMTExIDk3LTMzIDE0M1pNMjMzLTgwbDY1LTI4MUw4MC01NTBsMjg4LTI1IDExMi0yNjUgMTEyIDI2NSAyODggMjUtMjE4IDE4OSA2NSAyODEtMjQ3LTE0OUwyMzMtODBabTI0Ny0zNTBaIiBzdHlsZT0iZmlsbDogcmdiKDE2MSwgMjU0LCAyMjgpOyIvPgo8L3N2Zz4%3D%0A [vue-badge]: https://img.shields.io/badge/vuejs-%2335495e.svg?style=for-the-badge&logo=vuedotjs&logoColor=%234FC08D [tauri-badge]: https://img.shields.io/badge/tauri-%2324C8DB.svg?style=for-the-badge&logo=tauri&logoColor=%23FFFFFF [unocss-badge]: https://img.shields.io/badge/unocss-333333.svg?style=for-the-badge&logo=unocss&logoColor=white [discord-banner]: https://discordapp.com/api/guilds/1422266074908594199/widget.png?style=banner3 -[license-badge]: https://img.shields.io/github/license/freesmteam/kaede?style=for-the-badge +[license-badge]: https://img.shields.io/github/license/kaede-basement/kaede?style=for-the-badge diff --git a/docs/README.ru.md b/docs/README.ru.md index e69de29..9daaeff 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -0,0 +1,415 @@ +
+ +```ts +function handleNavigation(path: RouteType): void { + const webviewId = path === "home" ? "main" : `navigation_window_${path}`; + + new WebviewWindow(webviewId, { + "url" : `/?route=${path}`, + "visible": false, + "title" : "Kaede - " + General.capitalize(path), + }); +} + +export async function temporaryShit(): Promise { + window.__KAEDE__.libs.GlobalStateHelpers.Pages.navigate = handleNavigation; + + const currentWebviewWindow = getCurrentWebviewWindow(); + const currentId = currentWebviewWindow.label; + + if (currentId !== "navigationWindow") { + return; + } + + document.head.insertAdjacentHTML( + "beforeend", + "", + ); +} +``` + +
+ +A video demonstration of the isolated plugin that renders an interactive Live2D of [Misono Mika](https://bluearchive.wiki/wiki/Mika). Isolated plugin has the access to DOM (`document`); also can use `window.log` (a custom logger function that wraps tauri-plugin-log), `Math`, `Date`, `URL`, `Buffer`, `window["__core-js_shared__"]`, `console`, etc. This list is not full because it is really long, and the key thing here is that we can manually pass down global variables, so important and dangerous `window.__TAURI__.*` functions are not accessible. + +Isolation was done using the [Secure ECMAScript](https://github.com/endojs/endo). All globals were frozen using the `lockdown` function that `ses` library provides. So plugins can't rewrite any globals, prototypes, etc. + +Interactive Live2Ds were taken from [Z_DK's Steam Workshop](https://steamcommunity.com/id/xingsuileixi/myworkshopfiles/?appid=431960) + +
+I need to take care of DOM script tags that plugins can add when have the DOM access. Otherwise, this sandbox can be escaped. Maybe I can overwrite the `document.createElement` before freezing it? And just to be sure, listen for `head` element changes for possible script tag additions? + +Update: Seems like giving DOM access to the compartment basically ruins the whole purpose of plugin isolation, since one can add a JS code to any DOM element that will be executed in the global scope. + +Well, let's make these permissions then? + +- Full DOM access +- File System access (window.__TAURI__.fs) +- System shell access (window.__TAURI__.shell) +- ...other Tauri-specific plugins access + +How to adapt to these rules? Do I really need compartments here? Isn't it better to just use `new Function` at this point? What to do about script tags and the above DOM element JS executions? Should I expose Tauri API through `window` object? How to import and share Tauri API then? + +--- + +After some thoughts, I decided to go with this approach: + +Tauri API **will not** be exposed through `window` + +Account sign in is made through another webview window with no plugin access to prevent any possible security vulnerabilities. + +- Kaede Add-ons User Repository (KAUR) - plugins that are free to publish for anyone. Will use Secure ECMAScript Compartments for not full DOM access, otherwise they are executed with `new Function` +- Moderated plugins - every plugin publish/update goes through my checks. I only need plugin's source code and build manual. Executed with `new Function` or `Module Federation Runtime API`, have full DOM access by default, only require Tauri API scope permissions (fs, shell, global shortcuts, network, os info, etc.) from user + +Permissions: + +Runs in compartment: + +- CSS: Apply CSS stylesheet - Custom +- | Edit/replace element class list (by id) +- | Edit element styles (by id) + +Runs in `new Function` or Module Federation Runtime API (`eval`): + +Note: I need to generalize these, because seeing 30 permissions that vary from doing 1 simple ass thing (tauri-core-app) to doing 50+ dangerous things (tauri-plugin-fs/tauri-plugin-shell) is not ok + +- Website capabilities: everything that HTML & JS & CSS offers (no access to Tauri API) +- Tauri Core: Allow to change application theme (not the UI) - App +- | Emit/listen events to/from the backend - Event +- | Create images from paths - Image +- | Menu +- | Path +- | Resources +- | Tray +- | WebViews & windows management +- Tauri Plugins: +- plugin-drpc (i will format these a bit later) +- plugin-fs +- plugin-http +- plugin-log +- plugin-notification +- plugin-opener +- plugin-os +- plugin-process +- plugin-shell +- plugin-upload + +not related, but need to store it somewhere: + +context menu has 50000 z-index +log menu has 40000 z-index +sidebar has 3000 z-index + +Update: apparently, this shit is not going to work because tauri exposes __TAURI_INTERNALS__ to the `window` object. Nothing I can do about it, right? + +Update: yeah, `window.__TAURI_INTERNALS__` are frozen. But even if I somehow disable freezing, clone the `window.__TAURI_INTERNALS__.invoke` function, overwrite/delete it, and freeze the whole `window.__TAURI_INTERNALS__`, Tauri will just break since it accesses invokes from `window` (even in rust). + +Now I need to make some safe fine-grained permission system that manually exposes some JS capabilities, safe DOM wrappers, app functions and variables, Tauri-specific scopes, and only then the whole unrestricted environment (for DOM and full Tauri API). (Also need to take care of `plugin-shell` that allows to execute `java` with any arguments. Maybe not allow it unless unrestricted environment?) + +restricted environment will run in compartments, unrestricted environment is basically a `new Function` or `eval` in case of Module Federation Runtime API. + +Also, not related to extensions, but I can implement a `temporary launcher version switcher` that will fetch the JS code, save it in the directory somewhere that JS bindings can't access, and execute it instead of the bundled into the launcher JS assets. +
+ +If there is no video, [click here](https://github.com/user-attachments/assets/a1ccc9f2-0244-437b-8883-a68a26953e2a) + + + +
+ +```typescript +/* App.vue */ + +(async () => { + if (!localStorage.getItem("CH0304_home.skel")) { + localStorage.setItem("CH0304_home.skel", String.raw`{"SettingModel":{"version":5,"skel":"CH0304_home.skel","atlas":"CH0304_home.atlas","eyeTouch":"Touch_Me","idol":"Idle_01","x":459,"y":693,"scale":0.34,"angle":0,"bgmVolume":0.1,"talkVolume":0.45,"showTouch":false,"panelDisplay":false,"showTalkDialog":false,"textX":767,"textY":382,"textSize":16,"autoTalk":0,"resolution":"2k","startAnimation":"Start_Idle_01","startScene":true,"language":"中文"},"EventInfos":[{"BoneName":"Spine_03","Width":1500,"Height":800,"Animations":["Talk_01_M","Talk_02_M","Talk_03_M","Talk_04_M","Talk_05_M"],"AnimationIndex":10,"TextIndex":0,"ClickType":"click","EmptyAnimation":true},{"BoneName":"front hat_03b","Width":800,"Height":500,"Animations":["Pat_01_M"],"AnimationIndex":20,"TextIndex":0,"ClickType":"pointerdown","EmptyAnimation":false}],"AnimationsTexts":{"中文":{"CH0304_MemorialLobby_1_1":"……我想起了第一次拿起画笔的那天。","CH0304_MemorialLobby_1_2":"那时……我单纯地、笔直地面对着画布。","CH0304_MemorialLobby_2_1":"现在的我……已经不一样了。","CH0304_MemorialLobby_2_2":"不知道该画什么,失去了自信……","CH0304_MemorialLobby_3_1":"就在那时,老师告诉了我。","CH0304_MemorialLobby_3_2":"「名为『梦想』的魔法,早已寄宿在你的心里。」","CH0304_MemorialLobby_4_1":"只要还有梦想……接下来就不会在道路上迷失了吧。","CH0304_MemorialLobby_4_2":"老师……如果可以的话,愿意和我一起走这条路吗?","CH0304_MemorialLobby_5":"一条用五彩缤纷的梦想铺成的魔法之路!"},"日本語":{"CH0304_MemorialLobby_1_1":"……はじめて絵を描いた日を、思い出します。","CH0304_MemorialLobby_1_2":"あのときは……まっすぐ、純粋に、キャンバスと向き合っていました。","CH0304_MemorialLobby_2_1":"今の私は……違います。","CH0304_MemorialLobby_2_2":"何を描けばいいのか分からなくなって、自信を失ってしまって……。","CH0304_MemorialLobby_3_1":"そんなとき、マスターが教えてくれたんです。","CH0304_MemorialLobby_3_2":"「夢」という魔法は、もう心の中に宿ってるって。","CH0304_MemorialLobby_4_1":"夢があるなら……これからは、道に迷わずに済みそうです。","CH0304_MemorialLobby_4_2":"マスター……よければ、私と同じ道を歩みませんか?","CH0304_MemorialLobby_5":"色とりどりの夢でいっぱいの、魔法の道を!"},"English":{"CH0304_MemorialLobby_1_1":"...I remember the first day I ever picked up a brush.","CH0304_MemorialLobby_1_2":"Back then... I faced the canvas straight on, with pure heart.","CH0304_MemorialLobby_2_1":"But now... I’m not the same.","CH0304_MemorialLobby_2_2":"I no longer know what to paint, and I’ve lost my confidence...","CH0304_MemorialLobby_3_1":"At that moment, Master told me:","CH0304_MemorialLobby_3_2":"“The magic called ‘dream’ has already taken root inside your heart.”","CH0304_MemorialLobby_4_1":"As long as I have that dream... I won’t lose my way anymore.","CH0304_MemorialLobby_4_2":"Master... if you’d like, would you walk this same path with me?","CH0304_MemorialLobby_5":"A magical road paved with countless, colorful dreams!"},"Tiếng Việt":{"CH0304_MemorialLobby_1_1":".....Em vẫn nhớ ngày đầu tiên, mình bắt đầu vẽ tranh.","CH0304_MemorialLobby_1_2":"Lúc đó.. Em đối diện với khuôn canvas... \nTheo cách chân thành và thuần khiết nhất.","CH0304_MemorialLobby_2_1":"Còn em bây giờ... khác nhiều rồi.","CH0304_MemorialLobby_2_2":"Khi cầm cọ lên, em không biết nên vẽ gì nữa. \nEm mất tự tin về chính bản thân mình.","CH0304_MemorialLobby_3_1":"Và chính khi ấy, \nMaster lại dạy cho em một bài học","CH0304_MemorialLobby_3_2":"“Về phép màu mang tên ''Giấc mơ''.. \nVẫn luôn trú ngụ trong trái tim chúng ta ấy.”","CH0304_MemorialLobby_4_1":"Miễn còn có Giấc mơ... Thì từ giờ trở đi... \nEm không phải lo mình lạc l4_MemorialLobby_4_2":"Master... Nếu người không ngại thì... Hãy cùng em đi chung một lối nhé?","Cối nữa.","CH030H0304_MemorialLobby_5":"Trên con đường ma thuật, Ngập tràn những giấc mơ muôn màu!"}}}`); + } + + const url = "./assets/index-CQUR_vLf.js"; + const response = await fetch(url); + const _code = await response.text(); + const _cleaned = _code; + //.replace("", ">") + //.replace("-->", ">"); + const result = await Command.create("java", [ + "--version", + ]).execute(); + + // console.log(result, /* () => _cleaned */); + + // const realm = new ShadowRealm; + + // await realm.importValue("document", "document"); + + // realm.evaluate(_cleaned); + + console.log("Hardened Javascript test using SES"); + + const t1 = performance.now(); + + try { + console.log("Action: Trying to lockdown every JS globals"); + // lockdown({ + // "evalTaming": "unsafeEval", + // }); + } catch { + console.log("Already locked down"); + } + + // window.Buffer = Buffer; + + const _temporary: object = {}; + + for (const _name of Object.getOwnPropertyNames(window)) { + if (["Infinity", "undefined", "NaN"].includes(_name)) { + continue; + } + + if (_name[0] === _name[0].toLowerCase() && typeof window[_name] === "function") { + _temporary[_name] = window[_name].bind(window); + + continue; + } + + _temporary[_name] = window[_name]; + } + + console.log("Action: Creating a safe compartment"); + const c = new Compartment({ + "globals": /* _temporary */ { + ...window, + "console": { + ...console, + "log": log.debug, + ...log, + }, + "log" : log, + "document": window.document, + "globalThis": window, + "location": window.location, + "Symbol": window.Symbol, + "Buffer": window.Buffer, + "MathMLElement": window.MathMLElement, + "HTMLElement": window.HTMLElement, + "HTMLDivElement": window.HTMLDivElement, + "Response": window.Response, + "TouchEvent": window.TouchEvent, + "MouseEvent": window.MouseEvent, + "PointerEvent": window.PointerEvent, + "global": window, + "window": window, + "Date": window.Date, + "requestAnimationFrame": window.requestAnimationFrame.bind(window), + "cancelAnimationFrame": window.cancelAnimationFrame.bind(window), + "XMLHttpRequest": window.XMLHttpRequest, + "ResizeObserver": window.ResizeObserver, + "performance": window.performance, + "ElementPrototype": Element.prototype, + "HTMLElementPrototype": HTMLElement.prototype, + "NodePrototype": Node.prototype, + "EventTargetPrototype": EventTarget.prototype, + "SVGElement": window.SVGElement, + "Element": window.Element, + "WebGLRenderingContext": window.WebGLRenderingContext, + "WebGL2RenderingContext": window.WebGL2RenderingContext, + "navigator": window.navigator, + "Image": window.Image, + "OffscreenCanvas": window.OffscreenCanvas, + "Node": window.Node, + "EventTarget": window.EventTarget, + "HTMLCanvasElement": window.HTMLCanvasElement, + "screen": window.screen, + "fonts": window.document.fonts, + "dispatchEvent" : window.dispatchEvent.bind(window), + "addEventListener" : window.addEventListener.bind(window), + "removeEventListener": window.removeEventListener.bind(window), + "HTMLInputElement": window.HTMLInputElement, + "CanvasRenderingContext2D": window.CanvasRenderingContext2D, + "CanvasGradient": window.CanvasGradient, + "CanvasPattern": window.CanvasPattern, + "ImageData": window.ImageData, + "TextMetrics": window.TextMetrics, + "Path2D": window.Path2D, + "ImageBitmapRenderingContext": window.ImageBitmapRenderingContext, + "__core-js_shared__": window["__core-js_shared__"], + "fetch" : (...args: [RequestInfo]) => fetch(...args), + "ImageBitmap": window.ImageBitmap, + HTMLImageElement: window.HTMLImageElement, + HTMLVideoElement: window.HTMLVideoElement, + Blob: window.Blob, + File: window.File, + ArrayBuffer: window.ArrayBuffer, + Uint8Array: window.Uint8Array, + Int8Array: window.Int8Array, + Uint8ClampedArray: window.Uint8ClampedArray, + Uint16Array: window.Uint16Array, + "Int16Array": window.Int16Array, + Uint32Array: window.Uint32Array, + Int32Array: window.Int32Array, + Float32Array: window.Float32Array, + DataView: window.DataView, + XMLDocument: window.XMLDocument, + "Math": window.Math, + URL: { + ...window.URL, + createObjectURL: URL.createObjectURL.bind(URL), + revokeObjectURL: URL.revokeObjectURL.bind(URL), + }, + createImageBitmap: window.createImageBitmap.bind(window), + setInterval: window.setInterval.bind(window), + setTimeout: window.setTimeout.bind(window), + clearInterval: window.clearInterval.bind(window), + clearTimeout: window.clearTimeout.bind(window), + FontFace: window.FontFace, + }, + "__options__": true, + }); + + const t2 = performance.now(); + + console.log("Action: Running the contained code"); + try { + // await c.evaluate(_cleaned); + eval(_cleaned); + } catch (error) { + console.error("Error in the container:", error); + } + + log.debug("Initializing launcher"); + await initializeLauncher().catch((error: unknown) => { + log.error("Failed to initialize launcher:", JSON.stringify(error)); + }); + + const t3 = performance.now(); + console.log("Locking down & hardening done in", t2 - t1, "ms\n", "Secure ECMAScript evaluation done in", t3 - t2, "ms"); + + // const __invoke = window.__TAURI_INTERNALS__.invoke.bind(window.__TAURI_INTERNALS__); // or `{}`? + // console.log(window.__TAURI_INTERNALS__); + + // window.__TAURI_INTERNALS__.invoke = (): void => {}; + + console.log("Evaluating..."); + log.debug("Evaluating..."); + (new Function(` + console.log(window.__TAURI_INTERNALS__); + window.__TAURI_INTERNALS__.invoke("plugin:log|log", { + level: 4, + message: 'Wow!', + location: '', + file: '', + line: 1, + keyValues: { "": "" } + }) + `))(); +})(); +``` + +```typescript +/* globals.css */ + +/* + * TODO: these are live2d plugin-related styles + * +.loading-container, .loading-image, .el-scrollbar { + display: none !important; +} + */ + +/* +(async () => { + if (HookMappings.page !== "onRouteChange") { + return; + } + + try { + const wrapper = document.createElement("div"); + + wrapper.setAttribute("id", "blue_archive"); + wrapper.setAttribute("style", "position:absolute;top:0;left:0;right:0;bottom:0"); + + document.body.append(wrapper); + + const response = await fetch("./assets/index-CQUR_vLf.js"); + const code = await response.text(); + const plugin = new Function(code); + + const __globalStates = window[ApplicationNamespace].functions.getGlobalStates(); + + window[ApplicationNamespace].functions.changeGlobalStates("sidebarItems", [ + ...__globalStates.sidebarItems, + { + "path" : "none", + "icon" : "i-lucide-rss", + "name" : "via plugin!", + "action": () => window[ApplicationNamespace].functions.changeGlobalStates("page", "none"), + }, + ]); + + plugin(); + + setTimeout(() => { + const __appWrapper = document.getElementById("app_wrapper"); + const __sidebar = document.getElementById("sidebar"); + const __sidebarPlaceholder = document.getElementById("sidebar__placeholder"); + const __sidebarItems = document.getElementsByName("sidebar__item"); + const __sidebarTexts = document.getElementsByName("sidebar__item_text"); + + if (__sidebar !== null && __appWrapper !== null && __sidebarPlaceholder !== null) { + __sidebar.className = "transition-all h-58 w-16 absolute top-4 left-4 rounded-md overflow-hidden gap-2 flex flex-col bg-[theme(colors.black/.5)] p-2"; + __sidebarPlaceholder.className = "h-vh w-24 transition-[width]"; + + for (const __item of __sidebarItems) { + __item.className = "relative size-12 flex flex-col rounded-md select-none items-center justify-center gap-1 text-white transition-[width,height,background-color] duration-150 disabled:bg-[theme(colors.black/.3)]"; + } + + for (const __item of __sidebarTexts) { + __item.className = "hidden"; + } + } + }, 300); + } catch (error) { + console.error(error); + } +})(); + + */ + +/* +(async () => { + console.log("trying"); + + try { + const { discord } = window.__TAURI_PLUGINS_COMMUNITY__; + + await discord.stop(); + await discord.start("1432010035034325105"); + + const assets = new discord.Assets() + .setLargeImage("misono_mika") + .setLargeText("Misono Mika"); + + const activity = new discord.Activity() + .setState("Tauri & Vue <3") + .setAssets(assets) + .setTimestamps(new discord.Timestamps(Date.now())) + .setButton([ + new discord.Button("Minecraft Launcher w/ plugins", "https://github.com/kaede-basement/kaede"), + ]); + + await discord.setActivity(activity); + } catch (error) { + console.error("setting", error); + } +})(); + */ +``` + +
diff --git a/docs/demos/misono_mika_l2d_as_a_plugin.mp4 b/docs/demos/misono_mika_l2d_as_a_plugin.mp4 new file mode 100644 index 0000000..f063456 Binary files /dev/null and b/docs/demos/misono_mika_l2d_as_a_plugin.mp4 differ diff --git a/eslint.config.js b/eslint.config.js index 79b7056..9b1f242 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,14 +1,17 @@ +import { fileURLToPath } from "node:url"; + import { includeIgnoreFile } from "@eslint/compat"; -import { globalIgnores } from "eslint/config"; -import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript"; import stylistic from "@stylistic/eslint-plugin"; +import unocss from "@unocss/eslint-config/flat"; +import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript"; +import vueRequireID from "@vue-require-id/eslint-plugin"; +import { globalIgnores } from "eslint/config"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; import eslintPluginUnicorn from "eslint-plugin-unicorn"; import pluginVue from "eslint-plugin-vue"; -import unocss from "@unocss/eslint-config/flat"; import globals from "globals"; -import { fileURLToPath } from "node:url"; -// Get a '.gitignore' absolute path +// Get the absolute path of a '.gitignore' file const gitIgnorePath = fileURLToPath( new URL(".gitignore", import.meta.url), ); @@ -16,8 +19,14 @@ const gitIgnorePath = fileURLToPath( export default defineConfigWithVueTs( // Ignore linting for every file or directory that '.gitignore' contains includeIgnoreFile(gitIgnorePath), - // Also ignore './src-tauri', because it contains Rust code - globalIgnores(["./src-tauri"]), + globalIgnores([ + // Ignore './src-tauri' because it contains Rust code + "./src-tauri", + // Ignore '__mocks__' because it contains CommonJS files + "./src/__mocks__", + // Ignore 'vite-env.d.ts' because it is generated by vite + "./src/vite-env.d.ts", + ]), vueTsConfigs.recommended, pluginVue.configs["flat/essential"], // Unicorn's eslint plugin configuration @@ -34,57 +43,155 @@ export default defineConfigWithVueTs( }, }, "plugins": { - "@stylistic": stylistic, + "@stylistic" : stylistic, + "@vue-require-id" : vueRequireID, + "simple-import-sort": simpleImportSort, }, "rules": { /* Disabled rules */ - "vue/multi-word-component-names" : ["off"], // why do I need to use multiple words for a 'Layout' component, for example? - "vue/no-multiple-template-root" : ["off"], // no need for this rule since Vue 3.x - "unicorn/no-null" : ["off"], // 'JSON.stringify' second argument doesn't accept 'undefined' to save formatting - "unicorn/prefer-global-this" : ["off"], // no need for this rule because app is CSR and Web Workers will not be used - "unicorn/prefer-top-level-await" : ["off"], // broken - "@stylistic/no-multi-spaces" : ["off"], // conflict with eslint@stylistic/key-spacing - "@stylistic/line-comment-position" : ["off"], - "@stylistic/linebreak-style" : ["off"], - "@stylistic/eol-last" : ["off"], - "@stylistic/object-property-newline": ["off"], + // Why do I need to use multiple words for a 'Layout' component, for example? + "vue/multi-word-component-names": ["off"], + // No need for this rule since Vue 3.x + "vue/no-multiple-template-root" : ["off"], + // The second argument of 'JSON#stringify' doesn't accept 'undefined' to save formatting + "unicorn/no-null" : ["off"], + // No need for this rule since this app is fully CSR, and Web Workers are not going to be used + "unicorn/prefer-global-this" : ["off"], + // Top level await is broken somehow + "unicorn/prefer-top-level-await": ["off"], + // 'getElementById' is a lot easier to use + "unicorn/prefer-query-selector" : ["off"], + // Requires a different lib version + "unicorn/prefer-at" : ["off"], + // Conflicts with 'eslint@stylistic/key-spacing' + "@stylistic/no-multi-spaces" : ["off"], + // Conflicts with git + "@stylistic/linebreak-style" : ["off"], + // Conflicts with git + "@stylistic/eol-last" : ["off"], + + /* Important */ + "@stylistic/semi" : ["error", "always"], + "@stylistic/no-confusing-arrow": ["error", { + "allowParens" : true, + "onlyOneSimpleParam": false, + }], + "@stylistic/no-floating-decimal" : ["error"], + "@stylistic/max-statements-per-line" : ["error", { "max": 1 }], + "@stylistic/one-var-declaration-per-line": ["error", "always"], + "vue/block-lang" : ["error", { + "script": { + "lang": "ts", + }, + }], + "vue/component-api-style": ["error", ["script-setup"]], + + /* ESLint */ + "capitalized-comments": ["warn", "always"], + "no-console" : "warn", + + /* Simple Import Sort */ + "simple-import-sort/imports": "warn", + + // Element IDs simplify styling for plugins + "@vue-require-id/require-id": ["warn", { + "elements": [ + + /* Layout elements */ + "header", + "footer", + "aside", + "main", + "span", + "div", + "nav", + + /* Text */ + "label", + "br", + "hr", + "ul", + "ol", + "li", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "p", + + /* Interactive */ + "button", + "input", + "a", + + /* Graphics/Embeds */ + "canvas", + "iframe", + "img", + "svg", + ], + }], + + /* TypeScript */ + "@typescript-eslint/explicit-function-return-type": ["warn"], + "@typescript-eslint/no-unused-vars" : ["warn"], /* Vue */ + "vue/attribute-hyphenation": ["warn", "always", { + "ignore" : [], + "ignoreTags": [], + }], + "vue/component-name-in-template-casing": ["warn", "PascalCase", { + "registeredComponentsOnly": true, + "ignores" : [], + }], + "vue/no-duplicate-attributes": ["warn", { + "allowCoexistClass": true, + "allowCoexistStyle": true, + }], + "vue/no-unused-vars" : ["warn"], "vue/no-extra-parens": ["warn", "all", { "nestedBinaryExpressions": true }], "vue/max-len" : ["warn", { - "code" : 110, - "ignoreComments" : true, - "ignoreTrailingComments" : true, + "code" : 100, + "ignoreComments" : false, + "ignoreTrailingComments" : false, "ignoreHTMLAttributeValues": true, "ignoreHTMLTextContents" : true, "ignoreUrls" : true, }], /* Unicorn */ - "unicorn/filename-case" : ["warn", { "cases": { "kebabCase": true, "pascalCase": true } }], + "unicorn/filename-case": ["warn", { + "cases": { + "kebabCase" : true, + "pascalCase": true, + }, + }], "unicorn/prevent-abbreviations": ["warn"], /* Stylistic */ - "@stylistic/array-bracket-newline" : ["error", "consistent"], - "@stylistic/array-bracket-spacing" : ["error", "never"], - "@stylistic/array-element-newline" : ["error", "consistent"], + "@stylistic/array-bracket-newline" : ["warn", "consistent"], + "@stylistic/array-bracket-spacing" : ["warn", "never"], + "@stylistic/array-element-newline" : ["warn", "consistent"], "@stylistic/arrow-parens" : ["warn", "as-needed"], - "@stylistic/arrow-spacing" : ["error", { "before": true, "after": true }], - "@stylistic/block-spacing" : ["error", "always"], - "@stylistic/brace-style" : ["error", "1tbs"], + "@stylistic/arrow-spacing" : ["warn", { "before": true, "after": true }], + "@stylistic/block-spacing" : ["warn", "always"], + "@stylistic/brace-style" : ["warn", "1tbs"], "@stylistic/comma-dangle" : ["warn", "always-multiline"], - "@stylistic/comma-spacing" : ["error", { "before": false, "after": true }], - "@stylistic/comma-style" : ["error", "last"], - "@stylistic/computed-property-spacing" : ["error", "never"], - "@stylistic/curly-newline" : ["error", { "consistent": true }], - "@stylistic/function-call-argument-newline": ["error", "consistent"], - "@stylistic/function-call-spacing" : ["error", "never"], - "@stylistic/generator-star-spacing" : ["error", "before"], - "@stylistic/implicit-arrow-linebreak" : ["error", "beside"], - "@stylistic/indent" : ["error", 2, { "SwitchCase": 1 }], - "@stylistic/indent-binary-ops" : ["error", 2], - "@stylistic/key-spacing" : ["error", { + "@stylistic/comma-spacing" : ["warn", { "before": false, "after": true }], + "@stylistic/comma-style" : ["warn", "last"], + "@stylistic/computed-property-spacing" : ["warn", "never"], + "@stylistic/curly-newline" : ["warn", { "consistent": true }], + "@stylistic/function-call-argument-newline": ["warn", "consistent"], + "@stylistic/function-call-spacing" : ["warn", "never"], + "@stylistic/generator-star-spacing" : ["warn", "before"], + "@stylistic/implicit-arrow-linebreak" : ["warn", "beside"], + "@stylistic/indent" : ["warn", 2, { "SwitchCase": 1 }], + "@stylistic/indent-binary-ops" : ["warn", 2], + "@stylistic/key-spacing" : ["warn", { "beforeColon": false, "afterColon" : true, "mode" : "strict", @@ -95,17 +202,17 @@ export default defineConfigWithVueTs( "mode" : "strict", }, }], - "@stylistic/keyword-spacing" : ["error", { "before": true, "after": true }], - "@stylistic/lines-around-comment": ["error", { + "@stylistic/keyword-spacing" : ["warn", { "before": true, "after": true }], + "@stylistic/line-comment-position": ["warn", { "position": "above" }], + "@stylistic/lines-around-comment" : ["warn", { "beforeBlockComment": true, "allowBlockStart" : true, }], - "@stylistic/lines-between-class-members": ["error", "always", { + "@stylistic/lines-between-class-members": ["warn", "always", { "exceptAfterOverload" : true, "exceptAfterSingleLine": true, }], - "@stylistic/max-statements-per-line": ["error", { "max": 1 }], - "@stylistic/member-delimiter-style" : ["error", { + "@stylistic/member-delimiter-style": ["warn", { "multiline": { "delimiter" : "semi", "requireLast": true, @@ -116,13 +223,10 @@ export default defineConfigWithVueTs( }, "multilineDetection": "brackets", }], - "@stylistic/multiline-comment-style" : ["error", "starred-block"], - "@stylistic/multiline-ternary" : ["error", "never"], + "@stylistic/multiline-comment-style" : ["warn", "starred-block"], "@stylistic/new-parens" : ["warn", "never"], "@stylistic/newline-per-chained-call": ["warn", { "ignoreChainWithDepth": 2 }], - "@stylistic/no-confusing-arrow" : ["error", { "allowParens": true, "onlyOneSimpleParam": false }], - "@stylistic/no-extra-semi" : ["error"], - "@stylistic/no-floating-decimal" : ["error"], + "@stylistic/no-extra-semi" : ["warn"], "@stylistic/no-mixed-operators" : ["warn", { "groups": [ ["+", "-", "*", "/", "%", "**"], @@ -133,18 +237,17 @@ export default defineConfigWithVueTs( ], "allowSamePrecedence": true, }], - "@stylistic/no-mixed-spaces-and-tabs" : ["error"], - "@stylistic/no-multiple-empty-lines" : ["error", { "max": 3, "maxEOF": 1 }], - "@stylistic/no-tabs" : ["error"], - "@stylistic/no-trailing-spaces" : ["error"], - "@stylistic/no-whitespace-before-property" : ["error"], - "@stylistic/nonblock-statement-body-position": ["error", "beside"], - "@stylistic/object-curly-newline" : ["error", { "consistent": true }], - "@stylistic/object-curly-spacing" : ["error", "always"], - "@stylistic/one-var-declaration-per-line" : ["error", "always"], - "@stylistic/padded-blocks" : ["error", "never"], + "@stylistic/no-mixed-spaces-and-tabs" : ["warn"], + "@stylistic/no-multiple-empty-lines" : ["warn", { "max": 3, "maxEOF": 1 }], + "@stylistic/no-tabs" : ["warn"], + "@stylistic/no-trailing-spaces" : ["warn"], + "@stylistic/no-whitespace-before-property" : ["warn"], + "@stylistic/nonblock-statement-body-position": ["warn", "beside"], + "@stylistic/object-curly-newline" : ["warn", { "consistent": true }], + "@stylistic/object-curly-spacing" : ["warn", "always"], + "@stylistic/padded-blocks" : ["warn", "never"], "@stylistic/padding-line-between-statements" : [ - "error", + "warn", { "blankLine": "always", "prev": "*", "next": "return" }, { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, { "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] }, @@ -152,29 +255,28 @@ export default defineConfigWithVueTs( { "blankLine": "any", "prev": "directive", "next": "directive" }, ], "@stylistic/quote-props" : ["warn", "always"], - "@stylistic/quotes" : ["error", "double"], - "@stylistic/rest-spread-spacing" : ["error"], - "@stylistic/semi" : ["error", "always"], - "@stylistic/semi-spacing" : ["error", { "before": false, "after": true }], - "@stylistic/semi-style" : ["error", "last"], - "@stylistic/space-before-blocks" : ["error", "always"], - "@stylistic/space-before-function-paren": ["error", { + "@stylistic/quotes" : ["warn", "double"], + "@stylistic/rest-spread-spacing" : ["warn"], + "@stylistic/semi-spacing" : ["warn", { "before": false, "after": true }], + "@stylistic/semi-style" : ["warn", "last"], + "@stylistic/space-before-blocks" : ["warn", "always"], + "@stylistic/space-before-function-paren": ["warn", { "anonymous" : "always", "named" : "never", "asyncArrow": "always", "catch" : "always", }], - "@stylistic/space-in-parens" : ["error", "never"], - "@stylistic/space-unary-ops" : ["error", { "words": true, "nonwords": false }], - "@stylistic/spaced-comment" : ["error", "always"], - "@stylistic/switch-colon-spacing" : ["error", { "after": true, "before": false }], - "@stylistic/template-curly-spacing" : ["error", "never"], - "@stylistic/template-tag-spacing" : ["error", "never"], - "@stylistic/type-generic-spacing" : ["error"], - "@stylistic/type-named-tuple-spacing": ["error"], - "@stylistic/wrap-iife" : ["error", "inside"], - "@stylistic/wrap-regex" : ["error"], - "@stylistic/yield-star-spacing" : ["error", { "before": false, "after": true }], + "@stylistic/space-in-parens" : ["warn", "never"], + "@stylistic/space-unary-ops" : ["warn", { "words": true, "nonwords": false }], + "@stylistic/spaced-comment" : ["warn", "always"], + "@stylistic/switch-colon-spacing" : ["warn", { "after": true, "before": false }], + "@stylistic/template-curly-spacing" : ["warn", "never"], + "@stylistic/template-tag-spacing" : ["warn", "never"], + "@stylistic/type-generic-spacing" : ["warn"], + "@stylistic/type-named-tuple-spacing": ["warn"], + "@stylistic/wrap-iife" : ["warn", "inside"], + "@stylistic/wrap-regex" : ["warn"], + "@stylistic/yield-star-spacing" : ["warn", { "before": false, "after": true }], }, }, ); diff --git a/index.html b/index.html index 0a507bb..a34b4e2 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Kaede - +
diff --git a/package.json b/package.json index 6c8470f..8a136ff 100644 --- a/package.json +++ b/package.json @@ -9,32 +9,48 @@ "build:frontend": "vue-tsc -b && vite build", "preview:frontend": "vite preview", "test": "vitest run ./src", - "lint": "eslint ." + "lint": "eslint .", + "typecheck": "vue-tsc -b" }, "dependencies": { - "@kitbag/router": "^0.20.6", + "@fabianlars/tauri-plugin-oauth": "2", "@module-federation/enhanced": "^0.19.1", - "@tanstack/vue-query": "^5.90.2", + "@tanstack/vue-query": "^5.90.7", "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-fs": "~2", + "@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-log": "~2", + "@tauri-apps/plugin-notification": "~2", + "@tauri-apps/plugin-opener": "~2", "@tauri-apps/plugin-os": "~2", - "@types/unzipper": "^0.10.11", - "typebox": "^1.0.20", + "@tauri-apps/plugin-process": "~2", + "@tauri-apps/plugin-shell": "~2", + "@tauri-apps/plugin-upload": "~2", + "@vueuse/core": "^14.0.0", + "m3ripple-vue": "^0.1.0", + "ses": "^1.14.0", + "tauri-plugin-drpc": "^1.0.3", + "typebox": "^1.0.8", "unzipper": "^0.12.3", - "vue": "^3.5.17" + "vue": "^3.5.17", + "vue-virtualised": "^0.1.8" }, "devDependencies": { "@eslint/compat": "^1.3.2", "@eslint/js": "^9.34.0", + "@iconify-json/lucide": "^1.2.70", "@stylistic/eslint-plugin": "^5.2.3", "@tauri-apps/cli": "^2.8.3", "@types/node": "^24.3.0", + "@types/unzipper": "^0.10.11", "@unocss/eslint-config": "^66.4.2", "@vitejs/plugin-vue": "^6.0.0", + "@vue-require-id/eslint-plugin": "^1.0.4", "@vue/eslint-config-typescript": "^14.6.0", "@vue/tsconfig": "^0.7.0", "eslint": "^9.34.0", + "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unicorn": "^60.0.0", "eslint-plugin-vue": "^10.4.0", "globals": "^16.3.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c1b4bcd..3074144 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -96,6 +96,158 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "atk" version = "0.18.2" @@ -212,6 +364,19 @@ dependencies = [ "objc2 0.6.2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.3.0" @@ -407,7 +572,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid", + "uuid 1.18.0", ] [[package]] @@ -450,7 +615,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -463,6 +628,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -475,10 +649,39 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "percent-encoding", "time", "version_check", ] +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -502,7 +705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.3", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -515,7 +718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.9.3", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -634,6 +837,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "deranged" version = "0.4.0" @@ -685,7 +894,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -701,6 +910,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.3", + "block2 0.6.1", + "libc", "objc2 0.6.2", ] @@ -715,6 +926,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.0" @@ -738,6 +958,21 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -794,6 +1029,42 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -827,9 +1098,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -926,6 +1224,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -933,6 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -958,6 +1272,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -987,6 +1314,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1118,12 +1446,12 @@ dependencies = [ [[package]] name = "gethostname" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -1144,8 +1472,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1155,9 +1485,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.3+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1314,6 +1646,25 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.11.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1341,6 +1692,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1393,6 +1750,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -1409,6 +1772,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1420,6 +1784,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.16" @@ -1439,9 +1820,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1650,7 +2033,26 @@ dependencies = [ ] [[package]] -name = "itoa" +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" @@ -1740,11 +2142,22 @@ dependencies = [ "log", "serde", "serde_json", + "sysinfo", "tauri", "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-drpc", "tauri-plugin-fs", + "tauri-plugin-http", "tauri-plugin-log", + "tauri-plugin-notification", + "tauri-plugin-oauth", + "tauri-plugin-opener", "tauri-plugin-os", + "tauri-plugin-process", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-upload", ] [[package]] @@ -1838,6 +2251,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.13" @@ -1857,12 +2276,30 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee70bb2bba058d58e252d2944582d634fc884fc9c489a966d428dedcf653e97" +dependencies = [ + "cc", + "objc2 0.6.2", + "objc2-foundation 0.3.1", + "time", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -1993,12 +2430,48 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify-rust" +version = "4.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2030,7 +2503,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.106", @@ -2186,6 +2659,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "objc2-io-surface" version = "0.3.1" @@ -2297,12 +2780,34 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_info" version = "3.12.0" @@ -2315,6 +2820,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -2340,6 +2855,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.4" @@ -2363,6 +2884,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2515,6 +3042,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2529,7 +3067,7 @@ checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", "indexmap 2.11.0", - "quick-xml", + "quick-xml 0.38.3", "serde", "time", ] @@ -2547,6 +3085,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -2597,6 +3149,15 @@ dependencies = [ "toml_edit 0.20.2", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.4", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2636,6 +3197,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2656,6 +3223,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.3" @@ -2665,6 +3251,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.16", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -2711,6 +3352,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2731,6 +3382,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2749,6 +3410,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -2773,6 +3443,17 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "read-progress-stream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6435842fc2fea44b528719eb8c32203bbc1bb2f5b619fbe0c0a3d8350fd8d2a8" +dependencies = [ + "bytes", + "futures", + "pin-project-lite", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -2859,22 +3540,32 @@ checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2884,6 +3575,46 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] @@ -2901,7 +3632,7 @@ dependencies = [ "rkyv_derive", "seahash", "tinyvec", - "uuid", + "uuid 1.18.0", ] [[package]] @@ -2915,6 +3646,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rpcdiscord" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71aa9a2097dc0176805e24debcb5d3ea5a17b796cd1d28e76b29f78fb49d7d5d" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "uuid 0.8.2", +] + [[package]] name = "rust_decimal" version = "1.37.2" @@ -2937,6 +3681,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2956,7 +3706,42 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2992,7 +3777,7 @@ dependencies = [ "serde", "serde_json", "url", - "uuid", + "uuid 1.18.0", ] [[package]] @@ -3031,6 +3816,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3240,12 +4031,53 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3346,6 +4178,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.9" @@ -3377,6 +4215,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3451,6 +4295,41 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.3", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3472,7 +4351,7 @@ checksum = "4daa814018fecdfb977b59a094df4bd43b42e8e21f88fddfc05807e6f46efaaf" dependencies = [ "bitflags 2.9.3", "block2 0.6.1", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -3544,6 +4423,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", @@ -3624,7 +4504,7 @@ dependencies = [ "thiserror 2.0.16", "time", "url", - "uuid", + "uuid 1.18.0", "walkdir", ] @@ -3643,82 +4523,261 @@ dependencies = [ ] [[package]] -name = "tauri-plugin" -version = "2.4.0" +name = "tauri-plugin" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.5", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.16", + "url", +] + +[[package]] +name = "tauri-plugin-drpc" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b291669b7dbc05471fba380eeecf31e3f733ae6013aaa5216a43ca376027e5a" +dependencies = [ + "log", + "rpcdiscord", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", + "tokio", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.16", + "toml 0.9.5", + "url", +] + +[[package]] +name = "tauri-plugin-http" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938a3d7051c9a82b431e3a0f3468f85715b3442b3c3a3913095e9fa509e2652c" +dependencies = [ + "bytes", + "cookie_store", + "data-url", + "http", + "regex", + "reqwest", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.16", + "tokio", + "url", + "urlpattern", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59139183e0907cec1499dddee4e085f5a801dc659efa0848ee224f461371426" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2 0.6.2", + "objc2-foundation 0.3.1", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", + "time", +] + +[[package]] +name = "tauri-plugin-notification" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fbc86b929b5376ab84b25c060f966d146b2fbd59b6af8264027b343c82c219" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-oauth" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda564acdb23185caf700f89dd6e5d4540225d6a991516b2cad0cbcf27e4dcd3" +dependencies = [ + "httparse", + "log", + "serde", + "tauri", + "tauri-plugin", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "toml 0.9.5", - "walkdir", + "tauri", + "tauri-plugin", ] [[package]] -name = "tauri-plugin-fs" -version = "2.4.2" +name = "tauri-plugin-shell" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +checksum = "54777d0c0d8add34eea3ced84378619ef5b97996bd967d3038c668feefd21071" dependencies = [ - "anyhow", - "dunce", - "glob", - "percent-encoding", + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", "schemars 0.8.22", "serde", "serde_json", - "serde_repr", + "shared_child", "tauri", "tauri-plugin", - "tauri-utils", "thiserror 2.0.16", - "toml 0.9.5", - "url", + "tokio", ] [[package]] -name = "tauri-plugin-log" -version = "2.6.0" +name = "tauri-plugin-single-instance" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a59139183e0907cec1499dddee4e085f5a801dc659efa0848ee224f461371426" +checksum = "fb9cac815bf11c4a80fb498666bcdad66d65b89e3ae24669e47806febb76389c" dependencies = [ - "android_logger", - "byte-unit", - "fern", - "log", - "objc2 0.6.2", - "objc2-foundation 0.3.1", "serde", "serde_json", - "serde_repr", - "swift-rs", "tauri", - "tauri-plugin", "thiserror 2.0.16", - "time", + "tracing", + "windows-sys 0.60.2", + "zbus", ] [[package]] -name = "tauri-plugin-os" +name = "tauri-plugin-upload" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba" +checksum = "ca3a3a78df0924a08d166d2d8041c6b9e752e6d94913177b52f1bfa11b5f73d3" dependencies = [ - "gethostname", + "futures-util", "log", - "os_info", + "read-progress-stream", + "reqwest", "serde", "serde_json", - "serialize-to-javascript", - "sys-locale", "tauri", "tauri-plugin", "thiserror 2.0.16", + "tokio", + "tokio-util", ] [[package]] @@ -3807,7 +4866,7 @@ dependencies = [ "toml 0.9.5", "url", "urlpattern", - "uuid", + "uuid 1.18.0", "walkdir", ] @@ -3821,6 +4880,31 @@ dependencies = [ "toml 0.9.5", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.16", + "windows", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.4.3" @@ -3942,11 +5026,35 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "slab", "socket2", + "tokio-macros", + "tracing", "windows-sys 0.59.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -4029,6 +5137,18 @@ dependencies = [ "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +dependencies = [ + "indexmap 2.11.0", + "toml_datetime 0.7.0", + "toml_parser", + "winnow 0.7.13", +] + [[package]] name = "toml_parser" version = "1.0.2" @@ -4096,9 +5216,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "tracing-core" version = "0.1.34" @@ -4148,6 +5280,17 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -4201,6 +5344,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -4243,6 +5392,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.16", +] + [[package]] name = "uuid" version = "1.18.0" @@ -4417,6 +5575,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.3", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.3", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -4427,6 +5645,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.1" @@ -4471,6 +5699,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.0" @@ -4562,7 +5799,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -4583,7 +5820,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -4595,7 +5832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -4627,6 +5864,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -4634,7 +5877,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result", + "windows-strings", ] [[package]] @@ -4643,7 +5897,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4652,7 +5906,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4691,6 +5945,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4728,7 +5991,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4745,7 +6008,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4754,7 +6017,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4909,6 +6172,9 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] [[package]] name = "winreg" @@ -5031,6 +6297,68 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid 1.18.0", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -5072,6 +6400,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.2" @@ -5104,3 +6438,44 @@ dependencies = [ "quote", "syn 2.0.106", ] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.106", + "winnow 0.7.13", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d9b8d31..d9eae62 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.0" description = "An extensible Tauri-based Minecraft launcher written in Vue and Rust" authors = ["windstone", "kaeeraa"] license = "GPL-3.0" -repository = "https://github.com/FreesmTeam/kaede" +repository = "https://github.com/kaede-basement/kaede" edition = "2021" rust-version = "1.77.2" @@ -21,10 +21,23 @@ tauri-build = { version = "2.4.0", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" -tauri = { version = "2.8.4", features = ["macos-private-api"] } +tauri = { version = "2.8.4", features = ["protocol-asset", "macos-private-api"] } tauri-plugin-log = "2" +chrono = "0.4.41" +sysinfo = "0.36.1" # Non-default installed plugins tauri-plugin-fs = "2" -chrono = "0.4.41" +tauri-plugin-dialog = "2" +tauri-plugin-http = "2" +tauri-plugin-notification = "2" tauri-plugin-os = "2" +tauri-plugin-process = "2" +tauri-plugin-shell = "2" +tauri-plugin-upload = "2" +tauri-plugin-drpc = "0.1.4" +tauri-plugin-opener = "2" +tauri-plugin-oauth = "2" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-single-instance = "2" diff --git a/src-tauri/README.md b/src-tauri/README.md index 47fd1b3..dda1d27 100644 --- a/src-tauri/README.md +++ b/src-tauri/README.md @@ -1,3 +1,5 @@ +[README for JavaScript-related code](../src/README.md) | README for Rust-related code | [Contributing Guidelines](../docs/CONTRIBUTING.md) + # Rust code This folder contains Tauri-specific code. You won't find much here, because I use Tauri API in JS instead of writing logic directly in Rust. @@ -7,3 +9,6 @@ Let me explain some properties in `tauri.conf.json`: - `macOSPrivateApi` allows to make webview window transparent on macOS. - `withGlobalTauri` adds Tauri internals to global `window` object. This is needed for extensions. - `app.windows[0].visible` makes webview window hidden by default so that user will not see blank screen while webview and frontend are loading. I manually make it visible in the code (check `/src/main.ts`) once Vue finishes mounting. +- `app.windows[0].title` is just a visible title for the window title bar, but it serves an important purpose in Kaede. If launcher build will be portable, then that `title` property must contain a `Portable` string. This is required, because in the logging file initialization (`lib.rs`) I determine whether launcher is portable or not by checking if window title contains a `Portable` string. + +Tauri API permissions are located in `./capabilities/`. I made a separate file for every permission scope. diff --git a/src-tauri/capabilities/core-app.json b/src-tauri/capabilities/core-app.json new file mode 100644 index 0000000..6937ad4 --- /dev/null +++ b/src-tauri/capabilities/core-app.json @@ -0,0 +1,17 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-app", + "description": "enables the core:app:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:app:allow-app-hide", + "core:app:allow-app-show", + "core:app:allow-default-window-icon", + "core:app:allow-name", + "core:app:allow-set-app-theme", + "core:app:allow-tauri-version", + "core:app:allow-version" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-event.json b/src-tauri/capabilities/core-event.json new file mode 100644 index 0000000..b5f4255 --- /dev/null +++ b/src-tauri/capabilities/core-event.json @@ -0,0 +1,14 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-event", + "description": "enables the core:event:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:event:allow-emit", + "core:event:allow-emit-to", + "core:event:allow-listen", + "core:event:allow-unlisten" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-image.json b/src-tauri/capabilities/core-image.json new file mode 100644 index 0000000..4a02368 --- /dev/null +++ b/src-tauri/capabilities/core-image.json @@ -0,0 +1,15 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-image", + "description": "enables the core:image:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:image:allow-from-bytes", + "core:image:allow-from-path", + "core:image:allow-new", + "core:image:allow-rgba", + "core:image:allow-size" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-menu.json b/src-tauri/capabilities/core-menu.json new file mode 100644 index 0000000..34d4c6a --- /dev/null +++ b/src-tauri/capabilities/core-menu.json @@ -0,0 +1,32 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-menu", + "description": "enables the core:menu:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:menu:allow-append", + "core:menu:allow-create-default", + "core:menu:allow-get", + "core:menu:allow-insert", + "core:menu:allow-is-checked", + "core:menu:allow-is-enabled", + "core:menu:allow-items", + "core:menu:allow-new", + "core:menu:allow-popup", + "core:menu:allow-prepend", + "core:menu:allow-remove", + "core:menu:allow-remove-at", + "core:menu:allow-set-accelerator", + "core:menu:allow-set-as-app-menu", + "core:menu:allow-set-as-help-menu-for-nsapp", + "core:menu:allow-set-as-window-menu", + "core:menu:allow-set-as-windows-menu-for-nsapp", + "core:menu:allow-set-checked", + "core:menu:allow-set-enabled", + "core:menu:allow-set-icon", + "core:menu:allow-set-text", + "core:menu:allow-text" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-path.json b/src-tauri/capabilities/core-path.json new file mode 100644 index 0000000..f3c5404 --- /dev/null +++ b/src-tauri/capabilities/core-path.json @@ -0,0 +1,18 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-path", + "description": "enables the core:path:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:path:allow-basename", + "core:path:allow-dirname", + "core:path:allow-extname", + "core:path:allow-is-absolute", + "core:path:allow-join", + "core:path:allow-normalize", + "core:path:allow-resolve", + "core:path:allow-resolve-directory" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-resources.json b/src-tauri/capabilities/core-resources.json new file mode 100644 index 0000000..bd7ec5d --- /dev/null +++ b/src-tauri/capabilities/core-resources.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-resources", + "description": "enables the core:resources:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:resources:allow-close" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-tray.json b/src-tauri/capabilities/core-tray.json new file mode 100644 index 0000000..f529ec8 --- /dev/null +++ b/src-tauri/capabilities/core-tray.json @@ -0,0 +1,21 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-tray", + "description": "enables the core:tray:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:tray:allow-get-by-id", + "core:tray:allow-new", + "core:tray:allow-remove-by-id", + "core:tray:allow-set-icon", + "core:tray:allow-set-icon-as-template", + "core:tray:allow-set-menu", + "core:tray:allow-set-show-menu-on-left-click", + "core:tray:allow-set-temp-dir-path", + "core:tray:allow-set-title", + "core:tray:allow-set-tooltip", + "core:tray:allow-set-visible" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-webview.json b/src-tauri/capabilities/core-webview.json new file mode 100644 index 0000000..6f63517 --- /dev/null +++ b/src-tauri/capabilities/core-webview.json @@ -0,0 +1,26 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-webview", + "description": "enables the core:webview:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:webview:allow-clear-all-browsing-data", + "core:webview:allow-create-webview", + "core:webview:allow-create-webview-window", + "core:webview:allow-get-all-webviews", + "core:webview:allow-internal-toggle-devtools", + "core:webview:allow-print", + "core:webview:allow-reparent", + "core:webview:allow-set-webview-focus", + "core:webview:allow-set-webview-position", + "core:webview:allow-set-webview-size", + "core:webview:allow-set-webview-zoom", + "core:webview:allow-webview-close", + "core:webview:allow-webview-hide", + "core:webview:allow-webview-position", + "core:webview:allow-webview-show", + "core:webview:allow-webview-size" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/core-window.json b/src-tauri/capabilities/core-window.json new file mode 100644 index 0000000..2fb9a94 --- /dev/null +++ b/src-tauri/capabilities/core-window.json @@ -0,0 +1,79 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "core-window", + "description": "enables the core:window:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "core:window:allow-available-monitors", + "core:window:allow-center", + "core:window:allow-close", + "core:window:allow-create", + "core:window:allow-current-monitor", + "core:window:allow-cursor-position", + "core:window:allow-destroy", + "core:window:allow-get-all-windows", + "core:window:allow-hide", + "core:window:allow-inner-position", + "core:window:allow-inner-size", + "core:window:allow-internal-toggle-maximize", + "core:window:allow-is-closable", + "core:window:allow-is-decorated", + "core:window:allow-is-enabled", + "core:window:allow-is-focused", + "core:window:allow-is-fullscreen", + "core:window:allow-is-maximizable", + "core:window:allow-is-maximized", + "core:window:allow-is-minimizable", + "core:window:allow-is-minimized", + "core:window:allow-is-resizable", + "core:window:allow-is-visible", + "core:window:allow-maximize", + "core:window:allow-minimize", + "core:window:allow-monitor-from-point", + "core:window:allow-outer-position", + "core:window:allow-outer-size", + "core:window:allow-primary-monitor", + "core:window:allow-request-user-attention", + "core:window:allow-scale-factor", + "core:window:allow-set-always-on-bottom", + "core:window:allow-set-always-on-top", + "core:window:allow-set-closable", + "core:window:allow-set-content-protected", + "core:window:allow-set-cursor-grab", + "core:window:allow-set-cursor-icon", + "core:window:allow-set-cursor-position", + "core:window:allow-set-cursor-visible", + "core:window:allow-set-decorations", + "core:window:allow-set-effects", + "core:window:allow-set-enabled", + "core:window:allow-set-focus", + "core:window:allow-set-fullscreen", + "core:window:allow-set-icon", + "core:window:allow-set-ignore-cursor-events", + "core:window:allow-set-max-size", + "core:window:allow-set-maximizable", + "core:window:allow-set-min-size", + "core:window:allow-set-minimizable", + "core:window:allow-set-position", + "core:window:allow-set-progress-bar", + "core:window:allow-set-resizable", + "core:window:allow-set-shadow", + "core:window:allow-set-size", + "core:window:allow-set-size-constraints", + "core:window:allow-set-skip-taskbar", + "core:window:allow-set-theme", + "core:window:allow-set-title", + "core:window:allow-set-title-bar-style", + "core:window:allow-set-visible-on-all-workspaces", + "core:window:allow-show", + "core:window:allow-start-dragging", + "core:window:allow-start-resize-dragging", + "core:window:allow-theme", + "core:window:allow-title", + "core:window:allow-toggle-maximize", + "core:window:allow-unmaximize", + "core:window:allow-unminimize" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index e3276df..ff24fbb 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,15 +1,9 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "enables the default permissions", + "description": "all permissions are handled in group-specific files", "windows": [ - "main" + "*" ], - "permissions": [ - "core:default", - "core:window:allow-show", - "fs:default", - "log:default", - "os:default" - ] + "permissions": [] } \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-dialog.json b/src-tauri/capabilities/plugin-dialog.json new file mode 100644 index 0000000..b72632e --- /dev/null +++ b/src-tauri/capabilities/plugin-dialog.json @@ -0,0 +1,15 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-dialog", + "description": "enables the dialog:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "dialog:allow-ask", + "dialog:allow-confirm", + "dialog:allow-message", + "dialog:allow-open", + "dialog:allow-save" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-drpc.json b/src-tauri/capabilities/plugin-drpc.json new file mode 100644 index 0000000..353ce68 --- /dev/null +++ b/src-tauri/capabilities/plugin-drpc.json @@ -0,0 +1,15 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-drpc", + "description": "enables the drpc:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "drpc:allow-spawn-thread", + "drpc:allow-destroy-thread", + "drpc:allow-set-activity", + "drpc:allow-clear-activity", + "drpc:allow-is-running" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-fs.json b/src-tauri/capabilities/plugin-fs.json new file mode 100644 index 0000000..93fb182 --- /dev/null +++ b/src-tauri/capabilities/plugin-fs.json @@ -0,0 +1,42 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-fs", + "description": "manages the fs:* permissions", + "windows": [ + "*" + ], + "permissions": [ + { + "identifier": "fs:scope", + "allow": [ + { "path": "$APPDATA" }, + { "path": "$APPDATA/**/*" } + ] + }, + "fs:allow-copy-file", + "fs:allow-create", + "fs:allow-exists", + "fs:allow-fstat", + "fs:allow-ftruncate", + "fs:allow-lstat", + "fs:allow-mkdir", + "fs:allow-open", + "fs:allow-read", + "fs:allow-read-dir", + "fs:allow-read-file", + "fs:allow-read-text-file", + "fs:allow-read-text-file-lines", + "fs:allow-read-text-file-lines-next", + "fs:allow-remove", + "fs:allow-rename", + "fs:allow-seek", + "fs:allow-size", + "fs:allow-stat", + "fs:allow-truncate", + "fs:allow-unwatch", + "fs:allow-watch", + "fs:allow-write", + "fs:allow-write-file", + "fs:allow-write-text-file" + ] +} diff --git a/src-tauri/capabilities/plugin-http.json b/src-tauri/capabilities/plugin-http.json new file mode 100644 index 0000000..02c997e --- /dev/null +++ b/src-tauri/capabilities/plugin-http.json @@ -0,0 +1,14 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-http", + "description": "enables the http:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "http:allow-fetch", + "http:allow-fetch-cancel", + "http:allow-fetch-read-body", + "http:allow-fetch-send" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-log.json b/src-tauri/capabilities/plugin-log.json new file mode 100644 index 0000000..a63ae67 --- /dev/null +++ b/src-tauri/capabilities/plugin-log.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-log", + "description": "enables the log:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "log:allow-log" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-notification.json b/src-tauri/capabilities/plugin-notification.json new file mode 100644 index 0000000..01a632d --- /dev/null +++ b/src-tauri/capabilities/plugin-notification.json @@ -0,0 +1,26 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-notification", + "description": "enables the notification:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "notification:allow-batch", + "notification:allow-cancel", + "notification:allow-check-permissions", + "notification:allow-create-channel", + "notification:allow-delete-channel", + "notification:allow-get-active", + "notification:allow-get-pending", + "notification:allow-is-permission-granted", + "notification:allow-list-channels", + "notification:allow-notify", + "notification:allow-permission-state", + "notification:allow-register-action-types", + "notification:allow-register-listener", + "notification:allow-remove-active", + "notification:allow-request-permission", + "notification:allow-show" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-opener.json b/src-tauri/capabilities/plugin-opener.json new file mode 100644 index 0000000..952c80d --- /dev/null +++ b/src-tauri/capabilities/plugin-opener.json @@ -0,0 +1,17 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-opener", + "description": "manages the opener:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "opener:allow-open-url", + "opener:allow-default-urls", + "opener:allow-reveal-item-in-dir", + { + "identifier": "opener:allow-open-path", + "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**/*" }] + } + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-os.json b/src-tauri/capabilities/plugin-os.json new file mode 100644 index 0000000..337630b --- /dev/null +++ b/src-tauri/capabilities/plugin-os.json @@ -0,0 +1,18 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-os", + "description": "enables the os:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "os:allow-arch", + "os:allow-exe-extension", + "os:allow-family", + "os:allow-hostname", + "os:allow-locale", + "os:allow-os-type", + "os:allow-platform", + "os:allow-version" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-process.json b/src-tauri/capabilities/plugin-process.json new file mode 100644 index 0000000..45e5c56 --- /dev/null +++ b/src-tauri/capabilities/plugin-process.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-process", + "description": "enables the process:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "process:allow-exit", + "process:allow-restart" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-shell.json b/src-tauri/capabilities/plugin-shell.json new file mode 100644 index 0000000..3024a6f --- /dev/null +++ b/src-tauri/capabilities/plugin-shell.json @@ -0,0 +1,45 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-shell", + "description": "manages the shell:* permissions", + "windows": [ + "*" + ], + "permissions": [ + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "java", + "cmd": "java", + "args": true, + "sidecar": false + } + ] + }, + { + "identifier": "shell:allow-spawn", + "allow": [ + { + "name": "java", + "cmd": "java", + "args": true, + "sidecar": false + } + ] + }, + { + "identifier": "shell:allow-stdin-write", + "allow": [ + { + "name": "java", + "cmd": "java", + "args": true, + "sidecar": false + } + ] + }, + "shell:allow-kill", + "shell:allow-open" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/plugin-upload.json b/src-tauri/capabilities/plugin-upload.json new file mode 100644 index 0000000..1df5803 --- /dev/null +++ b/src-tauri/capabilities/plugin-upload.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "plugin-upload", + "description": "enables the upload:* permissions", + "windows": [ + "*" + ], + "permissions": [ + "upload:allow-download", + "upload:allow-upload" + ] +} \ No newline at end of file diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 77e7d23..9b1e9e7 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index 0f7976f..78bcb91 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index 98fda06..2b39227 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..afb6129 Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index f35d84f..b192d0a 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index 1823bb2..2824875 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index dc2b22c..404dc60 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index 0ed3984..3206aa7 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 60bf0ea..f64cbd9 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index c8ca0ad..0c54b86 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index 8756459..d986ac0 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 2c8023c..84f2006 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index 2c5e603..a0b212f 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 17d142c..f1c0afb 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index a2993ad..18ff91e 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 06c23c8..3f982ee 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index d1756ce..d41fa81 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 588dcfa..074e009 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,36 +1,125 @@ -use chrono::Utc; use tauri::Manager; +use std::process; +use chrono::{DateTime, Utc}; +use sysinfo::{System, Pid}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_upload::init()) + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + // If user tries to open the launcher when it is already opened, + // then focus the already opened window + let _ = app + .get_webview_window("main") + .expect("no main window found - tauri single instance plugin") + .set_focus(); + })) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_drpc::init()) + .plugin(tauri_plugin_oauth::init()) .plugin(tauri_plugin_fs::init()) .setup(|app| { + // Since I am a complete newbie in Rust and Tauri, + // I couldn't think up of anything better than detecting + // whether the launcher is in the portable mode or not + // by checking if a window title contains a "Portable" string + // + // Reading window label took '225.60µs' on my laptop, + // so no worries about performance I guess + let window_title = app + .get_webview_window("main") + .expect("no main window found - tauri setup") + .title() + .unwrap() + .to_string(); + + let mut path; + + if window_title.contains("Portable") { + // Resolves to the launcher executable file directory + path = std::env::current_exe().unwrap().parent().unwrap().to_path_buf(); + } else { + // Resolves to '${dataDir}/${bundleIdentifier}' + path = app.path().app_data_dir()?; + } + + path.push("logs"); + + let _ = std::fs::create_dir_all(&path)?; + // Handle logging strategies differently based on build mode if cfg!(debug_assertions) { + // Debug mode app.handle().plugin( tauri_plugin_log::Builder::default() .level(log::LevelFilter::Debug) + // Make a new output target that will save logs in a log file + .target(tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::Folder { + path: path, + file_name: Some(format!("latest")), + }, + )) + // Make a custom logs format + .format(|out, message, record| { + let now_utc: DateTime = Utc::now(); + let formatted_date = now_utc.format("%d-%m-%Y").to_string(); + // Default tauri logging format doesn't include milliseconds + let formatted_time = now_utc.format("%H:%M:%S%.3f").to_string(); + + out.finish(format_args!( + "[{}][{}][{}][{}] {}", + formatted_date, + formatted_time, + record.target(), + record.level(), + message, + )) + }) + // Keep the log file size at 8 MB + .max_file_size(8_388_608) + // Use log rotation + .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll) .build(), )?; } else { - // Resolves to '${dataDir}/${bundleIdentifier}' - let path = app.path().app_data_dir()?; - let time = Utc::now().format("%Y-%m-%d_%H-%M").to_string(); - + // Basically the same, but I don't know Rust to refactor these duplicates + // Release mode app.handle().plugin( tauri_plugin_log::Builder::new() // Clear any default output targets, such as 'stdout', etc. .clear_targets() - // Make a new output target that will save logs in a file + // Make a new output target that will save logs in a log file .target(tauri_plugin_log::Target::new( tauri_plugin_log::TargetKind::Folder { path: path, - file_name: Some(format!("log_{time}")), + file_name: Some(format!("latest")), }, )) - // Keep log file size at 8 MB + // Make a custom logs format + .format(|out, message, record| { + let now_utc: DateTime = Utc::now(); + let formatted_date = now_utc.format("%d-%m-%Y").to_string(); + // Default tauri logging format doesn't include milliseconds + let formatted_time = now_utc.format("%H:%M:%S%.3f").to_string(); + + out.finish(format_args!( + "[{}][{}][{}][{}] {}", + formatted_date, + formatted_time, + record.target(), + record.level(), + message, + )) + }) + // Keep the log file size at 8 MB .max_file_size(8_388_608) // Use log rotation .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll) @@ -39,6 +128,55 @@ pub fn run() { } Ok(()) }) + .invoke_handler(tauri::generate_handler![ + get_executable_directory, + get_system_memory, + get_process_memory, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +#[tauri::command] +fn get_executable_directory() -> Result { + match std::env::current_exe() { + Ok(path) => { + if let Some(parent) = path.parent() { + Ok(parent.to_string_lossy().to_string()) + } else { + Err("Failed to get parent directory".to_string()) + } + } + Err(error) => Err(format!("Error getting executable path: {}", error)), + } +} + +#[tauri::command] +fn get_system_memory() -> (u64, u64) { + let mut sys = System::new_all(); + + sys.refresh_memory(); + + let used_memory = sys.used_memory(); + let total_memory = sys.total_memory(); + + return (used_memory, total_memory); +} + +#[tauri::command] +fn get_process_memory() -> (u64, u64) { + let mut sys = System::new_all(); + + sys.refresh_memory(); + + let pid = process::id(); + + if let Some(process) = sys.process(Pid::from_u32(pid)) { + let process_memory = process.memory(); + let process_virtual_memory = process.virtual_memory(); + + return (process_memory, process_virtual_memory); + } else { + return (0, 0); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 311e973..d7c94b0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,6 +14,7 @@ "withGlobalTauri": true, "windows": [ { + "label": "main", "title": "Kaede", "width": 800, "height": 600, @@ -21,12 +22,18 @@ "fullscreen": false, "visible": false, "center": true, - "minWidth": 400, - "minHeight": 300 + "minWidth": 512, + "minHeight": 288 } ], "security": { - "csp": null + "csp": null, + "assetProtocol": { + "enable": true, + "scope": { + "allow": ["$APPDATA", "$APPDATA/**/*"] + } + } } }, "bundle": { diff --git a/src/App.vue b/src/App.vue index 97833f2..4561670 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,59 +1,232 @@ diff --git a/src/README.md b/src/README.md index 2ff09c4..77517f0 100644 --- a/src/README.md +++ b/src/README.md @@ -1 +1,30 @@ -I wonder how github will handle README.md... +README for JavaScript-related code | [README for Rust-related code](../src-tauri/README.md) | [Contributing Guidelines](../docs/CONTRIBUTING.md) + +# Frontend & backend code + +This folder contains frontend-specific and backend-specific code. The frontend uses Vue.js, the backend uses Tauri JS API. Every single file here uses TypeScript instead of JavaScript. + +Let me explain the file structure. + +## Top-level files + +- `App.vue` is the Vue entry point file. + - Code-wise, it contains a single `shallowReactive` object that has almost all application states. Gathering application states in a single big `shallowReactive` object is a bad practice, but in this case it allows for extension hooks to have stable, predictable behaviour. With this approach, we also do not need to use `pinia` for global stores. + - HTML-wise, it contains application's layout with error boundaries. +- `declarations.ts` contain the `window` type definitions. Otherwise, TypeScript will not know about the custom `window.__KAEDE__` object. Note: `HookReturnType`'s second argument accepts `"nothing"` and any other type (including `void`). `void` and `"nothing"` values there serve different purposes: + - `void` means that the hook returns `{ "status": "stop" | "continue", "response": void }`. Hooks with this type can control whether to continue caller's code execution or not (caller is the function that executes these hooks). + - `"nothing"` means that the hook returns `void` (or anything else, the caller will just not care about it). Hooks with this type cannot abort caller's code execution. +- `globals.css` contain global CSS styles that are not possible or not convenient to write using the UnoCSS. +- `main.ts` is the main entry point file. It contains all initialization code and imports CSS styles. This code will be executed the first when the webview has loaded. +- `vite-env.d.ts` is just a default file that the `vite` generates. + +## Top-level folders + +Every folder has its own `README` file for more detailed explanations. + +- `__mocks__` contain library mocks and are purely for testing environment. [More](./__mocks__/README.md) +- `components` contain only Vue components. These components are used for the application UI. [More](./components/README.md) +- `constants` contain reusable global constants. [More](./constants/README.md) +- `lib` contains the backend part. [More](./lib/README.md) +- `resources` contain application assets, such as images, GIFs, videos, etc. [More](./resources/README.md) +- `types` contain reusable TypeScript types and interfaces. [More](./types/README.md) diff --git a/src/__mocks__/README.md b/src/__mocks__/README.md index 8787880..499008a 100644 --- a/src/__mocks__/README.md +++ b/src/__mocks__/README.md @@ -1,5 +1,7 @@ -# Vitest mocks +`[README for JavaScript-related code](../README.md) -This folder contains fake versions of some libraries (for example, Tauri) that can't run in a default testing (Vitest) environment. +`# Vitest mocks + +This folder contains a code to imitate some libraries behaviour (for example, Tauri) in the `Vitest` testing environment. See https://vitest.dev/guide/mocking for more. diff --git a/src/__mocks__/log.cjs b/src/__mocks__/log.cjs index 69d3ace..6c3be7f 100644 --- a/src/__mocks__/log.cjs +++ b/src/__mocks__/log.cjs @@ -1,8 +1,21 @@ +const fn = () => {}; + module.exports = { "log": { - "debug": () => {}, - "info" : () => {}, - "warn" : () => {}, - "error": () => {}, + "debug" : fn, + "info" : fn, + "warn" : fn, + "error" : fn, + "templates": { + "hooks": { + "iterate": { + "start" : fn, + "execution" : fn, + "response" : fn, + "no-response": fn, + "end" : fn, + }, + }, + }, }, }; diff --git a/src/components/README.md b/src/components/README.md new file mode 100644 index 0000000..69471b5 --- /dev/null +++ b/src/components/README.md @@ -0,0 +1,39 @@ +[README for JavaScript-related code](../README.md) + +# `components` folder + +This folder contains only Vue components. All components use [Composition API](https://vuejs.org/guide/extras/composition-api-faq.html) with TypeScript. + +Let me explain the file structure. + +## Top-level folders + +- `general` contains all **reusable** components that do not correspond to a specific page. +- `logging` contains all components that are used only in the log viewer screen. +- `add-instance` contains all components that are used only on the `Add Instance` page. +- `home` contains all components that are used only on the `Home` page. +- `library` contains all components that are used only on the `Library` page. +- `settings` contains all components that are used only on the `Settings` page. +- `profile` contains all components that are used only on the `Profile` page. + +### `general` folder + +- `base` contains all basic UI blocks with pre-defined styles and logic, such as buttons, inputs, sliders. + +## Styles + +### `z-index` + +``` +(development mode) +frames per second counter has 65000 z-index + +(not development mode) +context menu has 50000 z-index +permissions modal has 49500 z-index +sidebar hovering tooltip has 49000 z-index +log menu has 40000 z-index +sidebar has 10000 z-index +config sync loader icon has 5000 z-index +global background has -10 z-index +``` diff --git a/src/components/add-instance/AddInstance.vue b/src/components/add-instance/AddInstance.vue new file mode 100644 index 0000000..0391e44 --- /dev/null +++ b/src/components/add-instance/AddInstance.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/components/general/base/CustomButton.vue b/src/components/general/base/CustomButton.vue new file mode 100644 index 0000000..4e2b6d9 --- /dev/null +++ b/src/components/general/base/CustomButton.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/general/base/CustomInput.vue b/src/components/general/base/CustomInput.vue new file mode 100644 index 0000000..bd28332 --- /dev/null +++ b/src/components/general/base/CustomInput.vue @@ -0,0 +1,121 @@ + + + diff --git a/src/components/general/base/Image.vue b/src/components/general/base/Image.vue new file mode 100644 index 0000000..97ac2b5 --- /dev/null +++ b/src/components/general/base/Image.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/general/base/MaterialRipple.vue b/src/components/general/base/MaterialRipple.vue new file mode 100644 index 0000000..ae16798 --- /dev/null +++ b/src/components/general/base/MaterialRipple.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/general/development-mode/DevelopmentMode.vue b/src/components/general/development-mode/DevelopmentMode.vue new file mode 100644 index 0000000..d340554 --- /dev/null +++ b/src/components/general/development-mode/DevelopmentMode.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/general/development-mode/FramesPerSecondCounter.vue b/src/components/general/development-mode/FramesPerSecondCounter.vue new file mode 100644 index 0000000..e5ddc9f --- /dev/null +++ b/src/components/general/development-mode/FramesPerSecondCounter.vue @@ -0,0 +1,117 @@ + + + diff --git a/src/components/general/errors/ErrorBoundary.vue b/src/components/general/errors/ErrorBoundary.vue new file mode 100644 index 0000000..148d385 --- /dev/null +++ b/src/components/general/errors/ErrorBoundary.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/general/errors/ExtensionsError.vue b/src/components/general/errors/ExtensionsError.vue new file mode 100644 index 0000000..1086dd8 --- /dev/null +++ b/src/components/general/errors/ExtensionsError.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/src/components/general/errors/GlobalError.vue b/src/components/general/errors/GlobalError.vue new file mode 100644 index 0000000..cd61829 --- /dev/null +++ b/src/components/general/errors/GlobalError.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/general/errors/PageError.vue b/src/components/general/errors/PageError.vue new file mode 100644 index 0000000..5b13a97 --- /dev/null +++ b/src/components/general/errors/PageError.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/components/general/extensions/ExtensionLoader.vue b/src/components/general/extensions/ExtensionLoader.vue new file mode 100644 index 0000000..676435e --- /dev/null +++ b/src/components/general/extensions/ExtensionLoader.vue @@ -0,0 +1,111 @@ + + + \ No newline at end of file diff --git a/src/components/general/extensions/PermissionsHandler.vue b/src/components/general/extensions/PermissionsHandler.vue new file mode 100644 index 0000000..0a92184 --- /dev/null +++ b/src/components/general/extensions/PermissionsHandler.vue @@ -0,0 +1,169 @@ + + + \ No newline at end of file diff --git a/src/components/general/layout/ContextMenu.vue b/src/components/general/layout/ContextMenu.vue new file mode 100644 index 0000000..53eec6d --- /dev/null +++ b/src/components/general/layout/ContextMenu.vue @@ -0,0 +1,46 @@ + + + \ No newline at end of file diff --git a/src/components/general/layout/CustomLayout.vue b/src/components/general/layout/CustomLayout.vue new file mode 100644 index 0000000..73022aa --- /dev/null +++ b/src/components/general/layout/CustomLayout.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/general/layout/GlobalBackground.vue b/src/components/general/layout/GlobalBackground.vue new file mode 100644 index 0000000..c3b47e8 --- /dev/null +++ b/src/components/general/layout/GlobalBackground.vue @@ -0,0 +1,52 @@ + + + diff --git a/src/components/general/layout/Layout.vue b/src/components/general/layout/Layout.vue new file mode 100644 index 0000000..1d4d0b2 --- /dev/null +++ b/src/components/general/layout/Layout.vue @@ -0,0 +1,91 @@ + + + diff --git a/src/components/general/layout/PageTeleports.vue b/src/components/general/layout/PageTeleports.vue new file mode 100644 index 0000000..8c344f5 --- /dev/null +++ b/src/components/general/layout/PageTeleports.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/components/general/layout/PageWrapper.vue b/src/components/general/layout/PageWrapper.vue new file mode 100644 index 0000000..18b37f4 --- /dev/null +++ b/src/components/general/layout/PageWrapper.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/general/layout/PagesSelector.vue b/src/components/general/layout/PagesSelector.vue new file mode 100644 index 0000000..9d4202e --- /dev/null +++ b/src/components/general/layout/PagesSelector.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/general/layout/Router.vue b/src/components/general/layout/Router.vue new file mode 100644 index 0000000..cfac438 --- /dev/null +++ b/src/components/general/layout/Router.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/general/layout/Sidebar.vue b/src/components/general/layout/Sidebar.vue new file mode 100644 index 0000000..f2b5b7d --- /dev/null +++ b/src/components/general/layout/Sidebar.vue @@ -0,0 +1,182 @@ + + + diff --git a/src/components/general/layout/SidebarProfile.vue b/src/components/general/layout/SidebarProfile.vue new file mode 100644 index 0000000..f57563a --- /dev/null +++ b/src/components/general/layout/SidebarProfile.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/components/general/misc/ConfigSyncer.vue b/src/components/general/misc/ConfigSyncer.vue new file mode 100644 index 0000000..f5d0d2d --- /dev/null +++ b/src/components/general/misc/ConfigSyncer.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/components/general/misc/NonBundledClasses.vue b/src/components/general/misc/NonBundledClasses.vue new file mode 100644 index 0000000..2ddca8b --- /dev/null +++ b/src/components/general/misc/NonBundledClasses.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/handlers/ErrorBoundary.vue b/src/components/handlers/ErrorBoundary.vue deleted file mode 100644 index cdbd8ae..0000000 --- a/src/components/handlers/ErrorBoundary.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/src/components/home/Home.vue b/src/components/home/Home.vue new file mode 100644 index 0000000..dac3e54 --- /dev/null +++ b/src/components/home/Home.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/components/home/glance/AtAGlance.vue b/src/components/home/glance/AtAGlance.vue new file mode 100644 index 0000000..0db0bd0 --- /dev/null +++ b/src/components/home/glance/AtAGlance.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/home/instance/CurrentInstance.vue b/src/components/home/instance/CurrentInstance.vue new file mode 100644 index 0000000..8a85226 --- /dev/null +++ b/src/components/home/instance/CurrentInstance.vue @@ -0,0 +1,36 @@ + + + \ No newline at end of file diff --git a/src/components/home/instance/CurrentPlaytime.vue b/src/components/home/instance/CurrentPlaytime.vue new file mode 100644 index 0000000..01d7f0f --- /dev/null +++ b/src/components/home/instance/CurrentPlaytime.vue @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/src/components/home/instance/Launch.vue b/src/components/home/instance/Launch.vue new file mode 100644 index 0000000..2dc03e8 --- /dev/null +++ b/src/components/home/instance/Launch.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/src/components/home/instance/LaunchOptions.vue b/src/components/home/instance/LaunchOptions.vue new file mode 100644 index 0000000..6f2fefd --- /dev/null +++ b/src/components/home/instance/LaunchOptions.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/src/components/layout/Layout.vue b/src/components/layout/Layout.vue deleted file mode 100644 index c8b129c..0000000 --- a/src/components/layout/Layout.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/src/components/layout/Sidebar.vue b/src/components/layout/Sidebar.vue deleted file mode 100644 index 2d771b8..0000000 --- a/src/components/layout/Sidebar.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/components/library/Library.vue b/src/components/library/Library.vue new file mode 100644 index 0000000..1e9510d --- /dev/null +++ b/src/components/library/Library.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/components/logging/LogViewer.vue b/src/components/logging/LogViewer.vue new file mode 100644 index 0000000..21a51c1 --- /dev/null +++ b/src/components/logging/LogViewer.vue @@ -0,0 +1,297 @@ + + + diff --git a/src/components/logging/NonVirtualizedLogs.vue b/src/components/logging/NonVirtualizedLogs.vue new file mode 100644 index 0000000..2f72207 --- /dev/null +++ b/src/components/logging/NonVirtualizedLogs.vue @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/src/components/logging/controls/LogControls.vue b/src/components/logging/controls/LogControls.vue new file mode 100644 index 0000000..6e3361f --- /dev/null +++ b/src/components/logging/controls/LogControls.vue @@ -0,0 +1,246 @@ + + + diff --git a/src/components/logging/controls/LogFilterer.vue b/src/components/logging/controls/LogFilterer.vue new file mode 100644 index 0000000..7d27b4d --- /dev/null +++ b/src/components/logging/controls/LogFilterer.vue @@ -0,0 +1,36 @@ + + + \ No newline at end of file diff --git a/src/components/logging/controls/LogSearcher.vue b/src/components/logging/controls/LogSearcher.vue new file mode 100644 index 0000000..e97de11 --- /dev/null +++ b/src/components/logging/controls/LogSearcher.vue @@ -0,0 +1,168 @@ + + + \ No newline at end of file diff --git a/src/components/logging/lines/LogEntry.vue b/src/components/logging/lines/LogEntry.vue new file mode 100644 index 0000000..4faa723 --- /dev/null +++ b/src/components/logging/lines/LogEntry.vue @@ -0,0 +1,173 @@ + + + diff --git a/src/components/logging/lines/LogHighlighter.vue b/src/components/logging/lines/LogHighlighter.vue new file mode 100644 index 0000000..9c58730 --- /dev/null +++ b/src/components/logging/lines/LogHighlighter.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/src/components/profile/Profile.vue b/src/components/profile/Profile.vue new file mode 100644 index 0000000..216e897 --- /dev/null +++ b/src/components/profile/Profile.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/components/settings/Settings.vue b/src/components/settings/Settings.vue new file mode 100644 index 0000000..7a82d2b --- /dev/null +++ b/src/components/settings/Settings.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/constants/README.md b/src/constants/README.md new file mode 100644 index 0000000..7b5f684 --- /dev/null +++ b/src/constants/README.md @@ -0,0 +1,2 @@ +[README for JavaScript-related code](../README.md) + diff --git a/src/constants/application.ts b/src/constants/application.ts index c7c11b7..3a376f5 100644 --- a/src/constants/application.ts +++ b/src/constants/application.ts @@ -1,12 +1,47 @@ import { BaseDirectory } from "@tauri-apps/plugin-fs"; +import { revealItemInDir } from "@tauri-apps/plugin-opener"; + +import Errors from "@/lib/errors"; +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import { log } from "@/lib/logging/scopes/log.ts"; export const ApplicationName = "Kaede"; export const ApplicationNamespace = "__KAEDE__"; export const ApplicationRootID = "#app"; -export const ApplicationStopExecutionWord = "stop"; -export const ConfigFilename = "config.json"; +export const GlobalStatesContextKey = Symbol(); +export const TranslationsContextKey = Symbol(); +export const InstanceStatesContextKey = Symbol(); + +export const ContextMenuItems = [ + { + "name" : "Restart UI", + "icon" : "i-lucide-rotate-ccw", + "action": (): void => window.location.reload(), + }, + { + "name" : "Show Logs", + "icon" : "i-lucide-bug", + "action": (): void => { + GlobalStateHelpers.Logs.toggle("show", true); + window[ApplicationNamespace].libs.ContextMenu.close(); + }, + }, + { + "name" : "Open Root Folder", + "icon" : "i-lucide-folder", + "action": (): void => { + window[ApplicationNamespace].libs.ContextMenu.close(); + revealItemInDir( + GlobalStateHelpers.get().fileSystem?.files?.config ?? "", + ).catch((error: unknown) => { + log.error("Failed to reveal the config file in the explorer:", Errors.prettify(error)); + }); + }, + }, +] as const; +// TODO: remove these export const TreeResources = `${BaseDirectory.AppData}/resources`; export const TreeAssets = `${TreeResources}/assets`; export const TreeAssetObjects = `${TreeAssets}/objects`; diff --git a/src/constants/ascii-art.ts b/src/constants/ascii-art.ts new file mode 100644 index 0000000..a9c9011 --- /dev/null +++ b/src/constants/ascii-art.ts @@ -0,0 +1,22 @@ +import { arch, platform, version } from "@tauri-apps/plugin-os"; + +import { ApplicationName } from "@/constants/application.ts"; + +export function getASCIIArt(portable: boolean): string { + return ( + "\n" + + "\n __ __ " + + " me@" + ApplicationName.toLowerCase() + + "\n / /______ ____ ____/ /__ " + + " os " + platform() + " " + version() + + "\n / //_/ __ `/ _ \\/ __ / _ \\" + + " arch " + arch() + + "\n / ,< / /_/ / __/ /_/ / __/" + + " mode " + (portable ? "portable" : "non-portable") + + "\n/_/|_|\\__,_/\\___/\\__,_/\\___/ " + + " online " + ( + navigator?.onLine ? "yes" : "no" + ) + + "\n " + ); +} diff --git a/src/constants/english.json b/src/constants/english.json new file mode 100644 index 0000000..6f51340 --- /dev/null +++ b/src/constants/english.json @@ -0,0 +1,13 @@ +{ + "Info": { + "Code": "en", + "Name": "English", + "Flag": "\uD83C\uDDEC\uD83C\uDDE7", + "RTL": false + }, + "Messages": { + "general.errors.global-error.emoji": ":c", + "general.errors.global-error.message": "Kaede ran into a problem and needs to restart.\nYou can do it by closing this window and then opening Kaede again.", + "general.errors.page-error.message": "Something went wrong." + } +} diff --git a/src/constants/file-structure.ts b/src/constants/file-structure.ts new file mode 100644 index 0000000..7f1f1dc --- /dev/null +++ b/src/constants/file-structure.ts @@ -0,0 +1,15 @@ +export const FileStructure = { + "Config": { + "Name": "config.json", + }, + "Resources": { + "Path": "resources", + }, + "Instances": { + "Path": "instances", + }, + "Logs": { + "Path": "logs", + "Name": "latest.log", + }, +} as const; \ No newline at end of file diff --git a/src/constants/hooks.ts b/src/constants/hooks.ts new file mode 100644 index 0000000..34dee84 --- /dev/null +++ b/src/constants/hooks.ts @@ -0,0 +1,17 @@ +export const HookMappings = { + "translations" : "onTranslationsChange", + "fileSystem" : "onFileSystemChange", + "layout" : "onLayoutChange", + "pages" : "onPagesChange", + "logs" : "onLogsChange", + "sidebarItems" : "onSidebarItemsChange", + "contextMenuItems": "onContextMenuItemsChange", + "development" : "onDevelopmentChange", + "misc" : "onMiscChange", + "minecraft" : "onMinecraftChange", +} as const; + +export const HookResponseStatus = { + "Stop" : "stop", + "Continue": "continue", +} as const; diff --git a/src/constants/permissions.ts b/src/constants/permissions.ts new file mode 100644 index 0000000..197090e --- /dev/null +++ b/src/constants/permissions.ts @@ -0,0 +1,21 @@ +import type { PermissionType } from "@/types/extensions/permission.type.ts"; + +/* 'any' is required since 'GrantedScopes[string]' will contain literally anything */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const GrantedScopes: Record = {}; +export const IgnoredExtensionPermissions: Record> = {}; +export const Permissions = { + "Internet": { + "General": "internet", + }, + "ExternalStorage": { + "Read" : "read-external-storage", + "Write": "write-external-storage", + }, + "InternalStorage": { + "Read" : "read-internal-storage", + "Write": "write-internal-storage", + }, +} as const; \ No newline at end of file diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 44884c7..39adcb8 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -1,20 +1,22 @@ -import { createRoute } from "@kitbag/router"; -import { defineAsyncComponent } from "vue"; - export const Routes = { - "Home": { - "Key" : "Home", - "Path": "/", + "Home" : "home", + "Library" : "library", + "Settings" : "settings", + "AddInstance": "add-instance", + "Profile" : "profile", + "None" : "none", +} as const; +export const RouteItems = [ + { + "Path": Routes.Home, + "Icon": "i-lucide-home", }, - "About": { - "Key" : "About", - "Path": "/about", + { + "Path": Routes.Library, + "Icon": "i-lucide-boxes", }, -} as const; -export const RoutesConfiguration = Object - .values(Routes) - .map(({ Key, Path }) => createRoute({ - "name" : Key, - "path" : Path, - "component": defineAsyncComponent(() => import(`@/pages/${Key}.vue`)), - })); \ No newline at end of file + { + "Path": Routes.Settings, + "Icon": "i-lucide-settings", + }, +] as const; \ No newline at end of file diff --git a/src/declarations.ts b/src/declarations.ts index 55d62cb..2da42be 100644 --- a/src/declarations.ts +++ b/src/declarations.ts @@ -1,39 +1,659 @@ -import type { ConfigType } from "@/types/config/config.schema.ts"; -import type { log } from "@/lib/handlers/log.ts"; -import type { extractError } from "@/lib/helpers/extract-error.ts"; -import type { getRelativeDate } from "@/lib/helpers/get-relative-date.ts"; -import type { getConfigFile } from "@/lib/main/get-config-file.ts"; -import type { getDefaultConfig } from "@/lib/main/get-default-config.ts"; -import type { initializeConfigFile } from "@/lib/main/initialize-config-file.ts"; -import type { HookReturnType } from "@/types/extensions/hook-return.type.ts"; +import * as TauriOAuth2 from "@fabianlars/tauri-plugin-oauth"; import * as TauriApi from "@tauri-apps/api"; +import * as TauriDialog from "@tauri-apps/plugin-dialog"; import * as TauriFs from "@tauri-apps/plugin-fs"; +import * as TauriHttp from "@tauri-apps/plugin-http"; +import * as TauriNotification from "@tauri-apps/plugin-notification"; +import * as TauriOpener from "@tauri-apps/plugin-opener"; +import * as TauriOs from "@tauri-apps/plugin-os"; +import * as TauriProcess from "@tauri-apps/plugin-process"; +import * as TauriShell from "@tauri-apps/plugin-shell"; +import * as TauriUpload from "@tauri-apps/plugin-upload"; +import * as TauriDiscordRpc from "tauri-plugin-drpc"; +import * as TauriDiscordRpcClasses from "tauri-plugin-drpc/activity"; + +import type Configs from "@/lib/configs"; +import type DevelopmentModeHelpers from "@/lib/development-mode-helpers"; +import type Errors from "@/lib/errors"; +import type ExtensionsManager from "@/lib/extensions-manager"; +import type General from "@/lib/general"; +import type GlobalStateHelpers from "@/lib/global-state-helpers"; +import type Globals from "@/lib/globals"; +import type Instances from "@/lib/instances"; +import type Logging from "@/lib/logging"; +import type Schemas from "@/lib/schemas"; +import type { ConfigType } from "@/types/application/config.type.ts"; +import type { + GlobalStatesChangerType, + GlobalStatesType, +} from "@/types/application/global-states.type.ts"; +import type { + InstanceStatesChangerType, + InstanceStatesType, +} from "@/types/application/instance-states.type.ts"; +import type { RouteType } from "@/types/application/route.type.ts"; +import type { HookReturnType } from "@/types/extensions/hook-return.type.ts"; +import type { PermissionType } from "@/types/extensions/permission.type.ts"; +import type { TranslationsType } from "@/types/translations/translations.type.ts"; +import type { AtAGlanceType } from "@/types/ui/at-a-glance.type.ts"; +/* Expand the globals with Kaede and Tauri namespaces */ declare global { + + /* Declared in the '@/lib/globals/scopes/declare-window.ts' */ interface Window { - "__TAURI__": { - "api": typeof TauriApi; - "fs" : typeof TauriFs; + + /* Tauri exposes these */ + "__TAURI__": typeof TauriApi & { + "dialog" : typeof TauriDialog; + "fs" : typeof TauriFs; + "http" : typeof TauriHttp; + "notification": typeof TauriNotification; + "opener" : typeof TauriOpener; + "os" : typeof TauriOs; + "process" : typeof TauriProcess; + "shell" : typeof TauriShell; + "upload" : typeof TauriUpload; }; + + /* Tauri community plugins */ + "__TAURI_PLUGINS_COMMUNITY__": { + "discord": typeof TauriDiscordRpc & typeof TauriDiscordRpcClasses; + "oauth2" : typeof TauriOAuth2; + }; + + /** + * Application namespace. + * + * Extensions can extend this namespace + */ "__KAEDE__": { - "constants": object; // TODO - "variables": object; // TODO - "functions": { - "log" : typeof log; - "extractError" : typeof extractError; - "getRelativeDate" : typeof getRelativeDate; - "getConfigFile" : typeof getConfigFile; - "getDefaultConfig" : typeof getDefaultConfig; - "initializeConfigFile": typeof initializeConfigFile; + + /** + * Workarounds for application internals. + * + * These fields are not intended to be modified by extensions + */ + "__internals": { + // Gets current application's global states (use 'libs.GlobalStateHelpers#get') + "getGlobalStates" : () => GlobalStatesType; + // Changes application's global states (use 'libs.GlobalStateHelpers#change') + "changeGlobalStates" : GlobalStatesChangerType; + // Gets current application's instance states (use 'libs.Instances#get') + "getInstanceStates" : () => InstanceStatesType; + // Changes application's instance states (use 'libs.Instances#change') + "changeInstanceStates": InstanceStatesChangerType; + // Requests plugin permissions from user + "requestPermissions" : ( + permissions: Array, + extension: string + ) => Promise>; + // Application's config state before launcher initialization + "initialConfig" : ConfigType; + // Application's portable state before launcher initialization + "initialPortable" ?: boolean; + // Application's base directory state before launcher initialization + "initialBaseDirectory"?: string; + // A temporary storage for the 'At a Glance' widget + "atAGlance" ?: AtAGlanceType; + }; + + /** + * Global utilities. + * + * Changing any field of the listed objects + * will alter behaviour of that field for everyone. + * + * Example: + * + * ```ts + * // Somewhere in a plugin + * const arrayInADifferentScope: Array = []; + * + * function customDebugFunction(...input: Array): void { + * arrayInADifferentScope.push(input); + * }; + * + * // This assignment overwrites the 'debug' field in the 'log' object + * // with a reference to the 'customDebugFunction' function, + * // so all upcoming 'log#debug' calls will use the 'customDebugFunction' function + * // even if calls were not made via accessing the 'window' object + * window[ApplicationNamespace].libs.Logging.log.debug = customDebugFunction; + * ``` + */ + "libs": { + + /** + * Launcher's configuration-related collection of utilities + */ + "Configs": typeof Configs; + + /** + * Launcher's development mode related collection of utilities + */ + "DevelopmentModeHelpers": typeof DevelopmentModeHelpers; + + /** + * Launcher's errors-related collection of utilities + */ + "Errors": typeof Errors; + + /** + * Launcher's extension system related collection of utilities + */ + "ExtensionsManager": typeof ExtensionsManager; + + /** + * Launcher's general-purpose collection of utilities + */ + "General": typeof General; + + /** + * Launcher's global states related collection of utilities + */ + "GlobalStateHelpers": typeof GlobalStateHelpers; + + /** + * Launcher's 'window' object related collection of utilities + */ + "Globals": typeof Globals; + + /** + * Launcher's Minecraft instances related collection of utilities + */ + "Instances": typeof Instances; + + /** + * Launcher's logging-related collection of utilities + */ + "Logging": typeof Logging; + + /** + * Launcher's collection of typebox validation schemas + */ + "Schemas": typeof Schemas; + + /** + * Launcher's context menu related collection of utilities + */ + "ContextMenu": { + + /* + * Shows context menu. Requires the 'MouseEvent' typed event + * as the argument, since the context menu dynamically calculates + * its absolute position in the DOM by reading the provided event + */ + "show" : (event: MouseEvent) => void; + // Hides context menu + "close": () => void; + }; + + /** + * Launcher's pages-related collection of utilities + */ + "Pages": { + // Teleports the specified page to an element with the provided selector + "mount" : (page: Exclude, id: string) => void; + // Removes the specified page from DOM + "unmount": (page: Exclude) => void; + }; + }; + + /** + * Global variables that are allowed to be changed by plugins + */ + "variables": { + // Applies a background color to the ripple effect + "rippleColor" : string; + // Applies a sparkles color to the ripple effect + "sparklesColorRGB": string; }; + + /** + * Application hooks + */ "hooks": { + + /** + * Executed on the config retrieve + */ "getConfigFile": { - "before": HookReturnType; + + /** + * Executes 'async' or 'sync' functions before the config was read. + * + * Absolute pathname of the config file is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'ConfigType' typed object in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType; + + /** + * Executes 'async' or 'sync' functions after the config was read, parsed, and validated. + * + * A validated config is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'ConfigType' typed object in the 'response' field. + * + * If the hook returns a 'continue' status, + * it may add properties to the passed config argument or do nothing. + */ + "after": HookReturnType; }; + + /** + * Executed on the default config retrieve + */ "getDefaultConfig": { - "before": HookReturnType; + + /** + * Executes 'async' or 'sync' functions before the default config was returned. + * + * No arguments. + * + * If the hook returns a 'stop' status, + * it should also return a 'ConfigType' typed object in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType; + }; + + /** + * Executed on the translations replacement in global states + */ + "onTranslationsChange": { + + /** + * Executes 'sync'-only functions before the 'translations' property + * in the global states will change. + * + * 'TranslationsType' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'TranslationsType' typed object in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'translations' property in the global states has changed. + * + * 'TranslationsType' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'fileSystem' field replacement in global states + */ + "onFileSystemChange": { + + /** + * Executes 'sync'-only functions before the 'fileSystem' property + * in the global states will change. + * + * 'GlobalStatesType["fileSystem"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["fileSystem"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["fileSystem"], + GlobalStatesType["fileSystem"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'fileSystem' property in the global states has changed. + * + * 'GlobalStatesType["fileSystem"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'layout' field replacement in global states + */ + "onLayoutChange": { + + /** + * Executes 'sync'-only functions before the 'layout' property + * in the global states will change. + * + * 'GlobalStatesType["layout"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["layout"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["layout"], + GlobalStatesType["layout"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'layout' property in the global states has changed. + * + * 'GlobalStatesType["layout"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'pages' field replacement in global states + */ + "onPagesChange": { + + /** + * Executes 'sync'-only functions before the 'pages' property + * in the global states will change. + * + * 'GlobalStatesType["pages"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["pages"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["pages"], + GlobalStatesType["pages"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'pages' property in the global states has changed. + * + * 'GlobalStatesType["pages"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'logs' field replacement in global states + */ + "onLogsChange": { + + /** + * Executes 'sync'-only functions before the 'logs' property + * in the global states will change. + * + * 'GlobalStatesType["logs"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["logs"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["logs"], + GlobalStatesType["logs"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'logs' property in the global states has changed. + * + * 'GlobalStatesType["logs"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'sidebarItems' field replacement in global states + */ + "onSidebarItemsChange": { + + /** + * Executes 'sync'-only functions before the 'sidebarItems' property + * in the global states will change. + * + * 'GlobalStatesType["sidebarItems"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["sidebarItems"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["sidebarItems"], + GlobalStatesType["sidebarItems"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'sidebarItems' property in the global states has changed. + * + * 'GlobalStatesType["sidebarItems"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'contextMenuItems' field replacement in global states + */ + "onContextMenuItemsChange": { + + /** + * Executes 'sync'-only functions before the 'contextMenuItems' property + * in the global states will change. + * + * 'GlobalStatesType["contextMenuItems"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["contextMenuItems"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["contextMenuItems"], + GlobalStatesType["contextMenuItems"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'contextMenuItems' property in the global states has changed. + * + * 'GlobalStatesType["contextMenuItems"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'development' field replacement in global states + */ + "onDevelopmentChange": { + + /** + * Executes 'sync'-only functions before the 'development' property + * in the global states will change. + * + * 'GlobalStatesType["development"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["development"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["development"], + GlobalStatesType["development"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'development' property in the global states has changed. + * + * 'GlobalStatesType["development"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'misc' field replacement in global states + */ + "onMiscChange": { + + /** + * Executes 'sync'-only functions before the 'misc' property + * in the global states will change. + * + * 'GlobalStatesType["misc"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["misc"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["misc"], + GlobalStatesType["misc"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'misc' property in the global states has changed. + * + * 'GlobalStatesType["misc"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the 'minecraft' field replacement in global states + */ + "onMinecraftChange": { + + /** + * Executes 'sync'-only functions before the 'minecraft' property + * in the global states will change. + * + * 'GlobalStatesType["minecraft"]' typed object is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return a 'GlobalStatesType["minecraft"]' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + GlobalStatesType["minecraft"], + GlobalStatesType["minecraft"], + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the 'minecraft' property in the global states has changed. + * + * 'GlobalStatesType["minecraft"]' typed object is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType; + }; + + /** + * Executed on the field addition/overwrite/deletion in instance states + */ + "onInstanceChange": { + + /** + * Executes 'sync'-only functions before the provided field + * in the instance states will change. + * + * '{ key: string, value: GlobalStatesType["minecraft"] }' typed object + * is passed as the argument. + * + * If the hook returns a 'stop' status, + * it should also return + * a '{ key: string, value: GlobalStatesType["minecraft"] }' typed object + * in the 'response' field. + * + * If the hook returns a 'continue' status, + * code execution will continue as if that hook did not exist. + */ + "before": HookReturnType< + { "key": string; "value": GlobalStatesType["minecraft"] }, + { "key": string; "value": GlobalStatesType["minecraft"] }, + "non-promise" + >; + + /** + * Executes 'async' or 'sync' functions on the next Vue tick, + * after the provided field in the instance states has changed. + * + * '{ key: string, value: GlobalStatesType["minecraft"] }' typed object + * is passed as the argument. + * + * Hook should not return anything since the response will not be read. + */ + "after": HookReturnType< + { "key": string; "value": GlobalStatesType["minecraft"] }, + "nothing" + >; }; }; }; } -} \ No newline at end of file +} + +/* Export the Kaede namespace type */ +export type KaedeNamespaceType = Window["__KAEDE__"]; diff --git a/src/globals.css b/src/globals.css index caf9530..bd92874 100644 --- a/src/globals.css +++ b/src/globals.css @@ -1,12 +1,155 @@ -/* styling */ +/** + * Styling + */ +html { + /* Remove the overscroll bounce effect */ + height: 100%; + overflow: hidden; + + /* Default theme is dark */ + color-scheme: dark; +} + #app { - /* make all sub-elements' text not selectable */ + /* Text selections are going to be whitelisted */ user-select: none; } -/* data attributes */ +.text-justify-last { + /* Justify the last line of text */ + text-align-last: justify; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { width: 12px } +::-webkit-scrollbar-corner { background: transparent } +::-webkit-scrollbar-track { background: transparent } +::-webkit-scrollbar-thumb { background: #393939; cursor: pointer } +::-webkit-scrollbar-thumb:hover { background: #494949 } + +.thin-scrollbar::-webkit-scrollbar { width: 8px } + +.scroll-gutter-stable-both { + /* Leave a fixed space for scrollbar on both edges */ + scrollbar-gutter: stable both-edges; +} + +/* Remove the '' arrows */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Show a custom tooltip on button hover */ +button[data-tooltip] { + position: relative; + + &:after { + content: attr(data-tooltip); + font-size: 14px; + line-height: 1; + white-space: wrap; + + position: absolute; + left: 0; + top: calc(100% + 8px); + z-index: 10; + + width: 256px; + max-width: max-content; + padding: 8px; + border-radius: 6px; + + background: #262626; + visibility: hidden; + opacity: 0; + + transition: opacity 150ms ease; + } + &:hover:after { + visibility: visible; + opacity: 1; + } +} + +/* Allow log viewer to have a horizontal scroll */ +#__non-virtualized-list-logs { + .__log-entry__text-wrapper { + white-space: nowrap; + } +} +#__virtualized-list-logs > div { + width: fit-content; + + /* + * A library that implements the virtual list overwrites + * this field to 'hidden', so we use the '!important' directive + */ + overflow: visible !important; +} +#__virtualized-list-logs { + .__log-entry__text-wrapper { + white-space: nowrap; + } +} + +/** + * Data Attributes + */ [data-transitions-disabled] { * { transition: none; } } + +/** + * Vue Transitions + */ + +/* 'page' transition */ +.page-enter-active { + transition: opacity 150ms ease, transform 400ms ease; +} +.page-leave-active {} + +.page-enter-from { + transform: translateY(32px); + opacity: 0; +} +.page-leave-to {} + +/* 'global-background' transition */ +.global-background-enter-active { + transition: opacity 150ms ease; +} +.global-background-leave-active { + transition: opacity 150ms ease; + opacity: 0; +} + +.global-background-enter-from { + opacity: 1; +} +.global-background-leave-to {} + +/* 'pop' transition */ +.pop-enter-active { + transition: opacity 150ms ease-in-out, transform 150ms ease-in-out; +} + +.pop-enter-from, +.pop-leave-to { + opacity: 0; + transform: scale(90%); +} + +/* 'fade' transition */ +.fade-enter-active { + transition: opacity 150ms ease-in-out; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} diff --git a/src/lib/README.md b/src/lib/README.md new file mode 100644 index 0000000..7b5f684 --- /dev/null +++ b/src/lib/README.md @@ -0,0 +1,2 @@ +[README for JavaScript-related code](../README.md) + diff --git a/src/lib/helpers/checksum.ts b/src/lib/__delete/helpers/checksum.ts similarity index 73% rename from src/lib/helpers/checksum.ts rename to src/lib/__delete/helpers/checksum.ts index f3d1218..d174150 100644 --- a/src/lib/helpers/checksum.ts +++ b/src/lib/__delete/helpers/checksum.ts @@ -1,7 +1,8 @@ import { readFile } from "@tauri-apps/plugin-fs"; -import { ChecksumError } from "../launcher/core/errors"; -export async function checksum(path: string, hash: string) { +import { ChecksumError } from "@/lib/__delete/launcher/core/errors.ts"; + +export async function checksum(path: string, hash: string): Promise { const file = await readFile(path); const arrayBuffer = file.buffer.slice( file.byteOffset, diff --git a/src/lib/helpers/parse-rules.ts b/src/lib/__delete/helpers/parse-rules.ts similarity index 69% rename from src/lib/helpers/parse-rules.ts rename to src/lib/__delete/helpers/parse-rules.ts index 1b45cea..9ec85ea 100644 --- a/src/lib/helpers/parse-rules.ts +++ b/src/lib/__delete/helpers/parse-rules.ts @@ -1,8 +1,9 @@ import { arch, version } from "@tauri-apps/plugin-os"; -import type { Rule } from "../schemas/minecrafts-schemas"; -import { transformPlatform } from "./transform-platform"; -export async function evaluateRules(rules: Rule[]): Promise { +import { transformPlatform } from "@/lib/__delete/helpers/transform-platform.ts"; +import type { LibraryRuleType } from "@/types/minecraft/minecraft.type.ts"; + +export async function evaluateRules(rules: LibraryRuleType[]): Promise { const platform = transformPlatform(); const architecture = arch(); const version_ = version(); diff --git a/src/lib/helpers/transform-platform.ts b/src/lib/__delete/helpers/transform-platform.ts similarity index 100% rename from src/lib/helpers/transform-platform.ts rename to src/lib/__delete/helpers/transform-platform.ts diff --git a/src/lib/helpers/unzip-file.ts b/src/lib/__delete/helpers/unzip-file.ts similarity index 82% rename from src/lib/helpers/unzip-file.ts rename to src/lib/__delete/helpers/unzip-file.ts index c089520..6b3d79f 100644 --- a/src/lib/helpers/unzip-file.ts +++ b/src/lib/__delete/helpers/unzip-file.ts @@ -1,7 +1,8 @@ import { createReadStream, createWriteStream } from "node:fs"; + import unzipper from "unzipper"; -export async function unzipFile(input: string, output: string) { +export async function unzipFile(input: string, output: string): Promise { createReadStream(input) .pipe(unzipper.Parse()) .on("entry", entry => { diff --git a/src/lib/helpers/validate-file-size.ts b/src/lib/__delete/helpers/validate-file-size.ts similarity index 69% rename from src/lib/helpers/validate-file-size.ts rename to src/lib/__delete/helpers/validate-file-size.ts index 716075b..651c4ee 100644 --- a/src/lib/helpers/validate-file-size.ts +++ b/src/lib/__delete/helpers/validate-file-size.ts @@ -1,7 +1,8 @@ import { stat } from "@tauri-apps/plugin-fs"; -import { SizeError } from "../launcher/core/errors"; -export async function validateFileSize(path: string, size: number) { +import { SizeError } from "@/lib/__delete/launcher/core/errors.ts"; + +export async function validateFileSize(path: string, size: number): Promise { const file = await stat(path); if (file.size != size) { diff --git a/src/lib/launcher/README.md b/src/lib/__delete/launcher/README.md similarity index 100% rename from src/lib/launcher/README.md rename to src/lib/__delete/launcher/README.md diff --git a/src/lib/launcher/core/download.ts b/src/lib/__delete/launcher/core/download.ts similarity index 53% rename from src/lib/launcher/core/download.ts rename to src/lib/__delete/launcher/core/download.ts index 05cda43..6249c1d 100644 --- a/src/lib/launcher/core/download.ts +++ b/src/lib/__delete/launcher/core/download.ts @@ -1,42 +1,42 @@ -import { - AssetsValidator, - VersionManifest, - type Artifact, - type Asset, - type AssetIndex, - type Classifiers, - type Library, - type LoggingConfig, - type Manifest, - type VersionMetaModern, -} from "@/lib/schemas/minecrafts-schemas"; +import { readTextFile } from "@tauri-apps/plugin-fs"; import { download } from "@tauri-apps/plugin-upload"; import Value from "typebox/value"; + import { TreeAssetIndexes, TreeAssetObjects, TreeLibraries, TreeLogging, TreeNatives, -} from "@/constants/application"; -import { readTextFile } from "@tauri-apps/plugin-fs"; -import { extractError } from "@/lib/helpers/extract-error"; -import { log } from "@/lib/handlers/log"; -import { validateFileSize } from "@/lib/helpers/validate-file-size"; -import { checksum } from "@/lib/helpers/checksum"; -import { evaluateRules } from "@/lib/helpers/parse-rules"; -import { transformPlatform } from "@/lib/helpers/transform-platform"; -import { unzipFile } from "@/lib/helpers/unzip-file"; - - -async function fetchVersionManifest(): Promise { +} from "@/constants/application.ts"; +import { checksum } from "@/lib/__delete/helpers/checksum.ts"; +import { evaluateRules } from "@/lib/__delete/helpers/parse-rules.ts"; +import { transformPlatform } from "@/lib/__delete/helpers/transform-platform.ts"; +import { unzipFile } from "@/lib/__delete/helpers/unzip-file.ts"; +import { validateFileSize } from "@/lib/__delete/helpers/validate-file-size.ts"; +import Errors from "@/lib/errors"; +import { log } from "@/lib/logging/scopes/log.ts"; +import Schemas from "@/lib/schemas"; +import { VersionManifestSchema } from "@/lib/schemas/scopes/version-manifest.schema.ts"; +import type { + ArtifactType, + AssetIndexType, AssetType, ClassifiersType, LibraryType, + LoggingConfigType, + VersionManifestType, + VersionMetaModernType, +} from "@/types/minecraft/minecraft.type.ts"; + +export async function fetchVersionManifest(): Promise { const raw = await fetch("https://piston-meta.mojang.com/mc/game/version_manifest.json"); - return Value.Parse(VersionManifest, raw); + return Value.Parse(VersionManifestSchema, raw); } /* TODO: Add support for older versions */ -async function fetchVersionMeta(manifest: Manifest, version: string): Promise { +export async function fetchVersionMeta( + manifest: VersionManifestType, + version: string, +): Promise { const object = manifest.versions.find(version_ => version_.id = version); if (!object) { @@ -44,10 +44,10 @@ async function fetchVersionMeta(manifest: Manifest, version: string): Promise { const filePath = `${prefix}/${artifact.path}`; await download(artifact.url, filePath); @@ -56,7 +56,7 @@ async function downloadArtifact(artifact: Artifact, prefix: string) { await checksum(filePath, artifact.sha1); } -async function downloadAssetIndex(index: AssetIndex): Promise { +export async function downloadAssetIndex(index: AssetIndexType): Promise { const path = `${TreeAssetIndexes}/${index.id}.json`; await download(index.url, path); @@ -67,7 +67,7 @@ async function downloadAssetIndex(index: AssetIndex): Promise { return path; } -async function downloadLoggingConfig(config: LoggingConfig) { +export async function downloadLoggingConfig(config: LoggingConfigType): Promise { const path = `${TreeLogging}/${config.id}`; await download(config.url, path); @@ -76,7 +76,7 @@ async function downloadLoggingConfig(config: LoggingConfig) { await checksum(path, config.sha1); } -async function downloadAsset(asset: Asset) { +export async function downloadAsset(asset: AssetType): Promise { const twoBytes = asset.hash.slice(0, 2); const uri = `${twoBytes}/${asset.hash}`; const url = `https://resources.download.minecraft.net/${uri}`; @@ -88,7 +88,7 @@ async function downloadAsset(asset: Asset) { await validateFileSize(path, asset.size); await checksum(path, asset.hash); } catch (error: unknown) { - const extracted = extractError(error); + const extracted = Errors.extract(error); return { "success": false, "reason": extracted.message }; } @@ -96,24 +96,33 @@ async function downloadAsset(asset: Asset) { return { "success": true }; } -async function downloadAssets(path: string) { +export async function downloadAssets(path: string): Promise { const index = await readTextFile(path) .then(async text => await JSON.parse(text)); - if (!AssetsValidator.Check(index)) { + if (!Schemas.AssetsValidator.Check(index)) { throw new TypeError(`Invalid AssetIndex ${path}`); } - const objects: Asset[] = index.objects; - const promises = objects.map(object => downloadAsset(object)); - const results = await Promise.all(promises); - const errors = results.filter(result => result.success === false); - - for (const error of errors) { - log.error(`Downloading asset (${path}) failed: ${error.reason}`); + const objects: AssetType[] = index.objects; + const [] = objects.map(object => downloadAsset(object)); + + /* + * Kaeeraa code + * + * const results = await Promise.all(promises); + * const errors = results.filter(result => result.success === false); + */ + + for (const error of []) { + log.error(`Downloading asset (${path}) failed: ${error}`); } } -async function downloadClassifiers(classifiers: Classifiers, version: string, extract: boolean) { +export async function downloadClassifiers( + classifiers: ClassifiersType, + version: string, + extract: boolean, +): Promise { const platform = transformPlatform(); const key = `natives-${platform}` as keyof typeof classifiers; const native = classifiers[key]; @@ -131,7 +140,7 @@ async function downloadClassifiers(classifiers: Classifiers, version: string, ex } } -async function downloadLibraries(libraries: Library[], version: string) { +export async function downloadLibraries(libraries: LibraryType[], version: string): Promise { for (const library of libraries) { const rules = library.rules; @@ -148,4 +157,4 @@ async function downloadLibraries(libraries: Library[], version: string) { ); } } -} +} \ No newline at end of file diff --git a/src/lib/launcher/core/errors.ts b/src/lib/__delete/launcher/core/errors.ts similarity index 100% rename from src/lib/launcher/core/errors.ts rename to src/lib/__delete/launcher/core/errors.ts diff --git a/src/lib/__delete/launcher/core/folder.ts b/src/lib/__delete/launcher/core/folder.ts new file mode 100644 index 0000000..3e17dfa --- /dev/null +++ b/src/lib/__delete/launcher/core/folder.ts @@ -0,0 +1 @@ +export function folder(): void {} \ No newline at end of file diff --git a/src/lib/launcher/core/index.ts b/src/lib/__delete/launcher/core/index.ts similarity index 80% rename from src/lib/launcher/core/index.ts rename to src/lib/__delete/launcher/core/index.ts index 8faf9ef..f462315 100644 --- a/src/lib/launcher/core/index.ts +++ b/src/lib/__delete/launcher/core/index.ts @@ -2,5 +2,5 @@ export * from "./launch.ts"; export * from "./version.ts"; export * from "./platform.ts"; export * from "./folder.ts"; -export * from "./diagnose.ts"; +export * from "./scopes/diagnose.ts"; export { checksum } from "./utilities.ts"; \ No newline at end of file diff --git a/src/lib/__delete/launcher/core/launch.ts b/src/lib/__delete/launcher/core/launch.ts new file mode 100644 index 0000000..8ee94bb --- /dev/null +++ b/src/lib/__delete/launcher/core/launch.ts @@ -0,0 +1 @@ +export function launch(): void {} \ No newline at end of file diff --git a/src/lib/__delete/launcher/core/platform.ts b/src/lib/__delete/launcher/core/platform.ts new file mode 100644 index 0000000..6612aa8 --- /dev/null +++ b/src/lib/__delete/launcher/core/platform.ts @@ -0,0 +1 @@ +export function platform(): void {} \ No newline at end of file diff --git a/src/lib/__delete/launcher/core/scopes/diagnose.ts b/src/lib/__delete/launcher/core/scopes/diagnose.ts new file mode 100644 index 0000000..a51be29 --- /dev/null +++ b/src/lib/__delete/launcher/core/scopes/diagnose.ts @@ -0,0 +1 @@ +export function diagnose(): void {} \ No newline at end of file diff --git a/src/lib/__delete/launcher/core/utilities.ts b/src/lib/__delete/launcher/core/utilities.ts new file mode 100644 index 0000000..e8235ed --- /dev/null +++ b/src/lib/__delete/launcher/core/utilities.ts @@ -0,0 +1 @@ +export function checksum(): void {} \ No newline at end of file diff --git a/src/lib/__delete/launcher/core/version.ts b/src/lib/__delete/launcher/core/version.ts new file mode 100644 index 0000000..9d1391c --- /dev/null +++ b/src/lib/__delete/launcher/core/version.ts @@ -0,0 +1 @@ +export function version(): void {} \ No newline at end of file diff --git a/src/lib/configs/index.ts b/src/lib/configs/index.ts new file mode 100644 index 0000000..b6119e0 --- /dev/null +++ b/src/lib/configs/index.ts @@ -0,0 +1,11 @@ +import { getConfigFile } from "@/lib/configs/scopes/get-config-file.ts"; +import { getDefaultConfig } from "@/lib/configs/scopes/get-default-config.ts"; +import { getSafeConfigFile } from "@/lib/configs/scopes/get-safe-config-file.ts"; +import { initializeConfigFile } from "@/lib/configs/scopes/initialize-config-file.ts"; + +export default { + "get" : getConfigFile, + "getDefault": getDefaultConfig, + "getSafe" : getSafeConfigFile, + "initialize": initializeConfigFile, +} as const; diff --git a/src/lib/configs/scopes/get-config-file.test.ts b/src/lib/configs/scopes/get-config-file.test.ts new file mode 100644 index 0000000..5a2a5e2 --- /dev/null +++ b/src/lib/configs/scopes/get-config-file.test.ts @@ -0,0 +1,207 @@ +import { beforeEach, expect, test, vi } from "vitest"; + +import type { ConfigType } from "@/types/application/config.type.ts"; + +const defaultConfig: ConfigType = { + "development": null, + "layout" : { + "custom" : false, + "background": { + "url" : null, + "key" : null, + "blur" : null, + "color": null, + }, + "sidebar": { + "background": null, + "blur" : null, + "color" : null, + "ripple" : null, + "sparkles" : null, + }, + "atAGlance": { + "title" : null, + "subtitle": null, + }, + }, + "logs": { + "show" : false, + "lineBreaks" : false, + "virtualized": false, + "dates" : false, + "filtering" : "", + }, + "minecraft": { + "windowHeight": 480, + "windowWidth" : 854, + "jvmArgs" : "", + }, + "misc": { + "enableDiscordRPC" : false, + "showBeforeInitialization": false, + }, +}; + +vi.mock("@/lib/configs/scopes/get-default-config.ts", async () => { + return { + "getDefaultConfig": async (): Promise => defaultConfig, + }; +}); +vi.mock("@/lib/configs/scopes/initialize-config-file.ts", async () => { + return { + "initializeConfigFile": async (): Promise => {}, + }; +}); + +const tests: Array<{ + "arguments": { + "exists" : boolean; + "fetchedConfig": string; + }; + "output": unknown; +}> = [ + { + "arguments": { + "exists" : true, + "fetchedConfig": JSON.stringify({}), + }, + "output": defaultConfig, + }, + { + "arguments": { + "exists" : true, + "fetchedConfig": JSON.stringify({ + ...defaultConfig, + "layout": { + ...defaultConfig.layout, + "apparently": "extra fields are going to pass the validation. i " + + "spent 2 days thinking why my tests were broken xd", + }, + "TUYU": "is awesome", + }), + }, + "output": { + ...defaultConfig, + "layout": { + ...defaultConfig.layout, + "apparently": "extra fields are going to pass the validation. i " + + "spent 2 days thinking why my tests were broken xd", + }, + "TUYU": "is awesome", + }, + }, + { + "arguments": { + "exists" : true, + "fetchedConfig": JSON.stringify({ + ...defaultConfig, + "layout": { + // 'custom' field can only be a boolean + "custom": "blue", + }, + }), + }, + "output": defaultConfig, + }, + { + "arguments": { + "exists" : true, + "fetchedConfig": JSON.stringify({ + ...defaultConfig, + "minecraft": { + // 'windowHeight' should have a 'number' type + "windowHeight": "480", + "windowWidth" : 854, + "jvmArgs" : "", + }, + }), + }, + "output": defaultConfig, + }, + { + "arguments": { + "exists" : true, + "fetchedConfig": JSON.stringify({ + ...defaultConfig, + "layout": { + ...defaultConfig.layout, + "background": { + ...defaultConfig.layout.background, + "url": "some-url", + }, + }, + }), + }, + "output": { + ...defaultConfig, + "layout": { + ...defaultConfig.layout, + "background": { + ...defaultConfig.layout.background, + "url": "some-url", + }, + }, + }, + }, + { + "arguments": { + "exists" : true, + "fetchedConfig": "0", + }, + "output": defaultConfig, + }, + { + "arguments": { + "exists" : false, + // If 'exists' were 'true', it would throw an error (expected behaviour) + "fetchedConfig": "", + }, + "output": defaultConfig, + }, +]; + +let index = 0; + +beforeEach(() => { + vi.doMock("@/lib/general", async () => { + return { + "default": { + "checkIsPortable" : async (): Promise => false, + "getBaseDirectory": async (): Promise => "", + }, + }; + }); + vi.doMock("@tauri-apps/api/path", async () => { + return { + "join": async (): Promise => "", + }; + }); + vi.doMock("@tauri-apps/plugin-fs", async () => { + return { + "BaseDirectory": { + "AppData": 14, + }, + "rename" : async (): Promise => {}, + "exists" : async (): Promise => tests[index].arguments.exists, + "readTextFile": async (): Promise => tests[index].arguments.fetchedConfig, + }; + }); +}); + +test.for(tests)( + "Get Config File: %o", async ({ output }) => { + /* + * Original import (top-level) will not be mocked, because 'vi.doMock' is evaluated + * after all imports and 'vi.mock' can't be called dynamically + */ + const { getConfigFile } = await import("./get-config-file.ts"); + + expect( + JSON.stringify(await getConfigFile()), + ).toBe( + JSON.stringify(output), + ); + + index++; + }, +); diff --git a/src/lib/configs/scopes/get-config-file.ts b/src/lib/configs/scopes/get-config-file.ts new file mode 100644 index 0000000..224a401 --- /dev/null +++ b/src/lib/configs/scopes/get-config-file.ts @@ -0,0 +1,123 @@ +import { join } from "@tauri-apps/api/path"; +import { exists, readTextFile, rename } from "@tauri-apps/plugin-fs"; + +import { ApplicationNamespace } from "@/constants/application.ts"; +import { FileStructure } from "@/constants/file-structure.ts"; +import { getDefaultConfig } from "@/lib/configs/scopes/get-default-config.ts"; +import { initializeConfigFile } from "@/lib/configs/scopes/initialize-config-file.ts"; +import Errors from "@/lib/errors"; +import General from "@/lib/general"; +import { log } from "@/lib/logging/scopes/log.ts"; +import Schemas from "@/lib/schemas"; +import type { ConfigType } from "@/types/application/config.type.ts"; + +export async function getConfigFile(passedBaseDirectory?: string): Promise { + const hooksArray = window[ApplicationNamespace].hooks.getConfigFile.before; + let baseDirectory: string | undefined = passedBaseDirectory; + + if (!baseDirectory) { + log.debug("No base directory was passed"); + log.debug("Checking if launcher is in portable version"); + const portable = await General.checkIsPortable(); + + log.debug("Getting base directory"); + baseDirectory = await General.getBaseDirectory(portable); + } + + const configFileDirectory = await join(baseDirectory, FileStructure.Config.Name); + + log.debug(log.templates.hooks.iterate.start( + "getConfigFile", + "before", + hooksArray.length, + )); + for (const [hookIndex, hookFunction] of hooksArray.entries()) { + const timeMeasurementStartHook = performance.now(); + + log.debug(log.templates.hooks.iterate.execution( + "getConfigFile", + "before", + hookIndex, + "async", + )); + const hookResponse = await hookFunction(configFileDirectory); + const timeMeasurementEndHook = performance.now(); + const currentBeforeHookTime = timeMeasurementEndHook - timeMeasurementStartHook; + + if (hookResponse.status === "stop") { + log.debug(log.templates.hooks.iterate.response( + "getConfigFile", + hookResponse, + "before", + hookIndex, + currentBeforeHookTime, + )); + + // Awaiting here will just be an unnecessary action + return hookResponse.response; + } + } + + log.debug("Checking if config file exists"); + const configExists = await exists(configFileDirectory); + + if (!configExists) { + log.info("Config file doesn't exist"); + log.debug("Initializing a config file"); + await initializeConfigFile(configFileDirectory); + + log.debug("Returning a promise with default config"); + + // Awaiting here will just be an unnecessary action + return getDefaultConfig(); + } + + log.info("Config file exists"); + log.debug("Reading a config file"); + const configFile = await readTextFile(configFileDirectory); + + log.debug("Parsing a config file"); + let parsedConfig: unknown; + + try { + parsedConfig = JSON.parse(configFile); + } catch (error: unknown) { + log.error("Couldn't parse the config file:", Errors.prettify(error)); + parsedConfig = {}; + } + + log.debug("Validating the config file"); + + /* + * If there is additional unknown properties in object, validation will pass them + * which is actually good because extensions can use same config as the app + */ + const validatedConfig = Schemas.ConfigValidator.Check(parsedConfig); + + if (!validatedConfig) { + log.warn("Config file is invalid"); + log.debug("Renaming the invalid config file"); + const currentTimestamp: string = Date.now().toString(); + + await rename( + configFileDirectory, + await join( + baseDirectory, + "config_invalid_" + currentTimestamp + ".json", + ), + ); + + log.debug("Initializing a new config file with default values"); + await initializeConfigFile(configFileDirectory); + + log.debug("Returning a promise with default config"); + + // Awaiting here will just be an unnecessary action + return getDefaultConfig(); + } + + log.info("Config file is valid"); + + // Assure TypeScript that the parsed config is valid + return parsedConfig as ConfigType; +} diff --git a/src/lib/configs/scopes/get-default-config.test.ts b/src/lib/configs/scopes/get-default-config.test.ts new file mode 100644 index 0000000..126545f --- /dev/null +++ b/src/lib/configs/scopes/get-default-config.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "vitest"; + +import type { ConfigType } from "@/types/application/config.type.ts"; + +import { getDefaultConfig } from "./get-default-config.ts"; + +const testName = "Default Config: No arguments"; + +test(testName, async () => { + const defaultConfig: ConfigType = { + "development": null, + "layout" : { + "custom" : false, + "background": { + "url" : null, + "key" : null, + "blur" : null, + "color": null, + }, + "sidebar": { + "background": null, + "blur" : null, + "color" : null, + "ripple" : null, + "sparkles" : null, + }, + "atAGlance": { + "title" : null, + "subtitle": null, + }, + }, + "logs": { + "show" : false, + "lineBreaks" : false, + "virtualized": false, + "dates" : false, + "filtering" : "", + }, + "minecraft": { + "windowHeight": 480, + "windowWidth" : 854, + "jvmArgs" : "", + }, + "misc": { + "enableDiscordRPC" : false, + "showBeforeInitialization": false, + }, + }; + + expect( + JSON.stringify(await getDefaultConfig()), + ).toBe( + JSON.stringify(defaultConfig), + ); +}); diff --git a/src/lib/configs/scopes/get-default-config.ts b/src/lib/configs/scopes/get-default-config.ts new file mode 100644 index 0000000..5d6bf5d --- /dev/null +++ b/src/lib/configs/scopes/get-default-config.ts @@ -0,0 +1,60 @@ +import { ApplicationNamespace } from "@/constants/application.ts"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { ConfigType } from "@/types/application/config.type.ts"; + +export async function getDefaultConfig(): Promise { + const hooksArray = window[ApplicationNamespace].hooks.getDefaultConfig.before; + + log.debug("Starting iterating through hooks for 'getDefaultConfig.before'"); + for (const [hookIndex, hookFunction] of hooksArray.entries()) { + log.debug("Executing a hook with the next index:", hookIndex.toString()); + const hookResponse = await hookFunction(); + + if (hookResponse.status === "stop") { + log.debug(`A hook with the index of ${hookIndex} has aborted execution`); + + // Awaiting here will just be an unnecessary action + return hookResponse.response; + } + } + + return { + "development": null, + "layout" : { + "custom" : false, + "background": { + "url" : null, + "key" : null, + "blur" : null, + "color": null, + }, + "sidebar": { + "background": null, + "blur" : null, + "color" : null, + "ripple" : null, + "sparkles" : null, + }, + "atAGlance": { + "title" : null, + "subtitle": null, + }, + }, + "logs": { + "show" : false, + "lineBreaks" : false, + "virtualized": false, + "dates" : false, + "filtering" : "", + }, + "minecraft": { + "windowHeight": 480, + "windowWidth" : 854, + "jvmArgs" : "", + }, + "misc": { + "enableDiscordRPC" : false, + "showBeforeInitialization": false, + }, + }; +} diff --git a/src/lib/configs/scopes/get-safe-config-file.ts b/src/lib/configs/scopes/get-safe-config-file.ts new file mode 100644 index 0000000..436fdc6 --- /dev/null +++ b/src/lib/configs/scopes/get-safe-config-file.ts @@ -0,0 +1,18 @@ +import Configs from "@/lib/configs"; +import Errors from "@/lib/errors"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { ConfigType } from "@/types/application/config.type.ts"; + +export async function getSafeConfigFile(baseDirectory?: string): Promise { + try { + log.debug("Getting a config file"); + + // Await here to catch errors + return await Configs.get(baseDirectory); + } catch (error: unknown) { + log.error("Failed to get a config file:", Errors.prettify(error)); + log.debug("Getting a default config"); + + return Configs.getDefault(); + } +} diff --git a/src/lib/main/initialize-config-file.ts b/src/lib/configs/scopes/initialize-config-file.ts similarity index 52% rename from src/lib/main/initialize-config-file.ts rename to src/lib/configs/scopes/initialize-config-file.ts index e577b80..f66c03e 100644 --- a/src/lib/main/initialize-config-file.ts +++ b/src/lib/configs/scopes/initialize-config-file.ts @@ -1,9 +1,9 @@ -import { ConfigFilename } from "@/constants/application.ts"; -import { getDefaultConfig } from "@/lib/main/get-default-config.ts"; -import { log } from "@/lib/handlers/log.ts"; -import { BaseDirectory, writeFile } from "@tauri-apps/plugin-fs"; +import { writeFile } from "@tauri-apps/plugin-fs"; -export async function initializeConfigFile(): Promise { +import { getDefaultConfig } from "@/lib/configs/scopes/get-default-config.ts"; +import { log } from "@/lib/logging/scopes/log.ts"; + +export async function initializeConfigFile(configFilePath: string): Promise { log.debug("Getting default config"); const defaultConfig = await getDefaultConfig(); @@ -19,7 +19,5 @@ export async function initializeConfigFile(): Promise { )); log.debug("Writing the encoded config file"); - await writeFile(ConfigFilename, data, { - "baseDir": BaseDirectory.AppData, - }); + await writeFile(configFilePath, data); } diff --git a/src/lib/development-mode-helpers/index.ts b/src/lib/development-mode-helpers/index.ts new file mode 100644 index 0000000..79da5ed --- /dev/null +++ b/src/lib/development-mode-helpers/index.ts @@ -0,0 +1,19 @@ +import { disableDebugMode } from "@/lib/development-mode-helpers/scopes/disable-debug-mode.ts"; +import { enableDebugMode } from "@/lib/development-mode-helpers/scopes/enable-debug-mode.ts"; +import { exit } from "@/lib/development-mode-helpers/scopes/exit.ts"; +import { + getDefaultDevelopmentStates, +} from "@/lib/development-mode-helpers/scopes/get-default-development-states.ts"; +import { + handleNativeReloadKeyBinds, +} from "@/lib/development-mode-helpers/scopes/handle-native-reload-key-binds.ts"; +import { initialize } from "@/lib/development-mode-helpers/scopes/initialize.ts"; + +export default { + "getDefault": getDefaultDevelopmentStates, + disableDebugMode, + enableDebugMode, + exit, + handleNativeReloadKeyBinds, + initialize, +} as const; diff --git a/src/lib/development-mode-helpers/scopes/disable-debug-mode.ts b/src/lib/development-mode-helpers/scopes/disable-debug-mode.ts new file mode 100644 index 0000000..88ac79d --- /dev/null +++ b/src/lib/development-mode-helpers/scopes/disable-debug-mode.ts @@ -0,0 +1,20 @@ +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import { log } from "@/lib/logging/scopes/log.ts"; + +export function disableDebugMode(): void { + const currentDevelopmentMode = GlobalStateHelpers.get()?.development; + + if (!currentDevelopmentMode) { + log.warn("Debug mode is already disabled."); + + return; + } + + GlobalStateHelpers.change("development", { + ...currentDevelopmentMode, + "enableDebugMode": false, + }); + + log.debug = log["__debug-undefined"]; + log.info("Debug mode disabled"); +} diff --git a/src/lib/development-mode-helpers/scopes/enable-debug-mode.ts b/src/lib/development-mode-helpers/scopes/enable-debug-mode.ts new file mode 100644 index 0000000..ed66e8c --- /dev/null +++ b/src/lib/development-mode-helpers/scopes/enable-debug-mode.ts @@ -0,0 +1,21 @@ +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; + +export function enableDebugMode(developmentModeEntries: GlobalStatesType["development"]): void { + const currentDevelopmentMode = developmentModeEntries ?? GlobalStateHelpers.get()?.development; + + if (!currentDevelopmentMode) { + log.error("Debug mode was triggered, but development mode is disabled."); + + return; + } + + GlobalStateHelpers.change("development", { + ...currentDevelopmentMode, + "enableDebugMode": true, + }); + + log.debug = log["__debug-defined"]; + log.warn("Debug mode enabled"); +} diff --git a/src/lib/development-mode-helpers/scopes/exit.ts b/src/lib/development-mode-helpers/scopes/exit.ts new file mode 100644 index 0000000..fb0b541 --- /dev/null +++ b/src/lib/development-mode-helpers/scopes/exit.ts @@ -0,0 +1,5 @@ +import GlobalStateHelpers from "@/lib/global-state-helpers"; + +export function exit(): void { + GlobalStateHelpers.change("development", null); +} diff --git a/src/lib/development-mode-helpers/scopes/get-default-development-states.ts b/src/lib/development-mode-helpers/scopes/get-default-development-states.ts new file mode 100644 index 0000000..e28ca0f --- /dev/null +++ b/src/lib/development-mode-helpers/scopes/get-default-development-states.ts @@ -0,0 +1,9 @@ +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; + +export function getDefaultDevelopmentStates(): GlobalStatesType["development"] { + return { + "showFPS" : false, + "enableDebugMode" : false, + "enableNativeReloadKeyBinds": false, + }; +} diff --git a/src/lib/development-mode-helpers/scopes/handle-native-reload-key-binds.ts b/src/lib/development-mode-helpers/scopes/handle-native-reload-key-binds.ts new file mode 100644 index 0000000..814e67f --- /dev/null +++ b/src/lib/development-mode-helpers/scopes/handle-native-reload-key-binds.ts @@ -0,0 +1,17 @@ +import { log } from "@/lib/logging/scopes/log.ts"; + +export function handleNativeReloadKeyBinds(event: KeyboardEvent, ignore?: boolean): void { + // If user has enabled native reload key binds, then do not prevent them from firing + if (ignore) { + return; + } + + if ( + event.key === "F5" || + (event.ctrlKey && event.key === "r") || + (event.metaKey && event.key === "r") + ) { + log.debug("Prevented native behaviour when triggering reload key binds"); + event.preventDefault(); + } +} diff --git a/src/lib/development-mode-helpers/scopes/initialize.ts b/src/lib/development-mode-helpers/scopes/initialize.ts new file mode 100644 index 0000000..806a98a --- /dev/null +++ b/src/lib/development-mode-helpers/scopes/initialize.ts @@ -0,0 +1,8 @@ +import { + getDefaultDevelopmentStates, +} from "@/lib/development-mode-helpers/scopes/get-default-development-states.ts"; +import GlobalStateHelpers from "@/lib/global-state-helpers"; + +export function initialize(): void { + GlobalStateHelpers.change("development", getDefaultDevelopmentStates()); +} diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts new file mode 100644 index 0000000..6d5d422 --- /dev/null +++ b/src/lib/errors/index.ts @@ -0,0 +1,11 @@ +import { extract } from "@/lib/errors/scopes/extract.ts"; +import { handleCapture } from "@/lib/errors/scopes/handle-capture.ts"; +import { prettify } from "@/lib/errors/scopes/prettify.ts"; +import { stringify } from "@/lib/errors/scopes/stringify.ts"; + +export default { + extract, + handleCapture, + prettify, + stringify, +} as const; \ No newline at end of file diff --git a/src/lib/helpers/extract-error.test.ts b/src/lib/errors/scopes/extract.test.ts similarity index 53% rename from src/lib/helpers/extract-error.test.ts rename to src/lib/errors/scopes/extract.test.ts index 1a32232..f39234c 100644 --- a/src/lib/helpers/extract-error.test.ts +++ b/src/lib/errors/scopes/extract.test.ts @@ -1,35 +1,46 @@ import { expect, test } from "vitest"; -import { extractError } from "./extract-error"; + import type { DeepRequired } from "@/types/utils/deep-required.type.ts"; +import { extract } from "./extract"; + +const defaultError = { + "name" : "Unknown Error", + "message": "unknown", + "stack" : "Unknown Error: unknown", +}; const testData: Array<[unknown, DeepRequired]> = [ [ { "days": 8 }, - { "name": "", "message": "", "stack": "" }, + defaultError, ], [ "", - { "name": "", "message": "", "stack": "" }, + defaultError, ], [ 1, - { "name": "", "message": "", "stack": "" }, + defaultError, ], [ undefined, - { "name": "", "message": "", "stack": "" }, + defaultError, ], [ { "name": 3, "message": "lol" }, - { "name": "", "message": "lol", "stack": "" }, + { "name": defaultError.name, "message": "lol", "stack": defaultError.stack }, ], [ { "name": "moondrop space travel", "message": 0 }, - { "name": "moondrop space travel", "message": "", "stack": "" }, + { + "name" : "moondrop space travel", + "message": defaultError.message, + "stack" : defaultError.stack, + }, ], [ { "name": { "name": "tf is this" }, "message": "" }, - { "name": "", "message": "", "stack": "" }, + { "name": defaultError.name, "message": "", "stack": defaultError.stack }, ], [ { @@ -38,7 +49,11 @@ const testData: Array<[unknown, DeepRequired]> = [ "stack" : "bruh i am listening to a different song", "trace" : "whar", }, - { "name": "DECO*27", "message": "Cherry Pop", "stack": "bruh i am listening to a different song" }, + { + "name" : "DECO*27", + "message": "Cherry Pop", + "stack" : "bruh i am listening to a different song", + }, ], ]; @@ -47,7 +62,7 @@ for (const [input, output] of testData) { test(testName, () => { expect( - JSON.stringify(extractError(input)), + JSON.stringify(extract(input)), ).toBe( JSON.stringify(output), ); diff --git a/src/lib/helpers/extract-error.ts b/src/lib/errors/scopes/extract.ts similarity index 64% rename from src/lib/helpers/extract-error.ts rename to src/lib/errors/scopes/extract.ts index 0f077aa..941abc3 100644 --- a/src/lib/helpers/extract-error.ts +++ b/src/lib/errors/scopes/extract.ts @@ -1,12 +1,10 @@ -export function extractError(error: unknown): { - "name" : string; - "message": string; - "stack" : string; -} { +import type { NativeErrorType } from "@/types/application/error-handling.type.ts"; + +export function extract(error: unknown): NativeErrorType { const safeError = { - "name" : "", - "message": "", - "stack" : "", + "name" : "Unknown Error", + "message": "unknown", + "stack" : "Unknown Error: unknown", }; if (typeof error !== "object" || error === null) { @@ -26,4 +24,4 @@ export function extractError(error: unknown): { } return safeError; -} +} \ No newline at end of file diff --git a/src/lib/errors/scopes/handle-capture.ts b/src/lib/errors/scopes/handle-capture.ts new file mode 100644 index 0000000..f586f14 --- /dev/null +++ b/src/lib/errors/scopes/handle-capture.ts @@ -0,0 +1,17 @@ +import { extract } from "@/lib/errors/scopes/extract.ts"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { NativeErrorType } from "@/types/application/error-handling.type.ts"; + +export function handleCapture(error: Error): NativeErrorType { + const extractedError = extract(error); + + log.error( + "A global error was captured:", + extractedError.name + ":", + extractedError.message + ";", + "Stack:", + "\n" + extractedError.stack, + ); + + return extractedError; +} \ No newline at end of file diff --git a/src/lib/errors/scopes/prettify.ts b/src/lib/errors/scopes/prettify.ts new file mode 100644 index 0000000..fd9baa9 --- /dev/null +++ b/src/lib/errors/scopes/prettify.ts @@ -0,0 +1,12 @@ +import { extract } from "@/lib/errors/scopes/extract.ts"; + +export function prettify(error: unknown): string { + const extracted = extract(error); + + return [ + extracted.name + ":", + extracted.message + ";", + "Stack:", + "\n" + extracted.stack, + ].join(" "); +} \ No newline at end of file diff --git a/src/lib/errors/scopes/stringify.ts b/src/lib/errors/scopes/stringify.ts new file mode 100644 index 0000000..e89532a --- /dev/null +++ b/src/lib/errors/scopes/stringify.ts @@ -0,0 +1,5 @@ +import { extract } from "@/lib/errors/scopes/extract.ts"; + +export function stringify(error: unknown): string { + return JSON.stringify(extract(error)); +} \ No newline at end of file diff --git a/src/lib/extensions-manager/index.ts b/src/lib/extensions-manager/index.ts new file mode 100644 index 0000000..33737ba --- /dev/null +++ b/src/lib/extensions-manager/index.ts @@ -0,0 +1,16 @@ +import { ApplicationNamespace } from "@/constants/application.ts"; +import { handlePermission } from "@/lib/extensions-manager/scopes/handle-permission.ts"; +import { readAllExtensions } from "@/lib/extensions-manager/scopes/read-all-extensions.ts"; +import type { PermissionType } from "@/types/extensions/permission.type.ts"; + +export default { + "requestPermissions": ( + permissions: Array, + extension: string, + ): Promise> => window[ApplicationNamespace].__internals.requestPermissions( + permissions, + extension, + ), + handlePermission, + readAllExtensions, +} as const; \ No newline at end of file diff --git a/src/lib/extensions-manager/scopes/handle-permission.ts b/src/lib/extensions-manager/scopes/handle-permission.ts new file mode 100644 index 0000000..40d4b14 --- /dev/null +++ b/src/lib/extensions-manager/scopes/handle-permission.ts @@ -0,0 +1,11 @@ +import { GrantedScopes } from "@/constants/permissions.ts"; +import type { PermissionType } from "@/types/extensions/permission.type.ts"; + +export function handlePermission(permission: PermissionType, id: string): void { + if (!GrantedScopes[id]["__allowed"]) { + GrantedScopes[id]["__allowed"] = []; + } + + GrantedScopes[id]["__allowed"].push(permission); + GrantedScopes[id].fetch = fetch.bind({}); +} \ No newline at end of file diff --git a/src/lib/extensions-manager/scopes/read-all-extensions.ts b/src/lib/extensions-manager/scopes/read-all-extensions.ts new file mode 100644 index 0000000..711fffe --- /dev/null +++ b/src/lib/extensions-manager/scopes/read-all-extensions.ts @@ -0,0 +1,3 @@ +export async function readAllExtensions(): Promise> { + return []; +} \ No newline at end of file diff --git a/src/lib/extensions-manager/scopes/request-permissions.ts b/src/lib/extensions-manager/scopes/request-permissions.ts new file mode 100644 index 0000000..9bb4c92 --- /dev/null +++ b/src/lib/extensions-manager/scopes/request-permissions.ts @@ -0,0 +1,42 @@ +import { IgnoredExtensionPermissions } from "@/constants/permissions.ts"; +import { handlePermission } from "@/lib/extensions-manager/scopes/handle-permission.ts"; +import type { PermissionType } from "@/types/extensions/permission.type.ts"; + +export async function __requestPermissions( + permissions: Array, + extension: string, + request: ( + permission?: PermissionType, + extension?: string, + resolve?: (state: boolean) => void + ) => void, +): Promise> { + const states = []; + + for (const permission of permissions) { + const isIgnored: boolean | undefined = IgnoredExtensionPermissions?.[extension]?.[permission]; + + if (isIgnored !== undefined) { + if (isIgnored) { + handlePermission(permission, extension); + } + + continue; + } + + const state = await new Promise((resolve: (value: boolean) => void) => { + request(permission, extension, resolve); + }); + + if (state) { + handlePermission(permission, extension); + } + + states.push(state); + } + + // Clear the permissions request state by passing nothing + request(); + + return states; +} \ No newline at end of file diff --git a/src/lib/general/index.ts b/src/lib/general/index.ts new file mode 100644 index 0000000..f5163d5 --- /dev/null +++ b/src/lib/general/index.ts @@ -0,0 +1,19 @@ +import { capitalize } from "@/lib/general/scopes/capitalize.ts"; +import { checkIsPortable } from "@/lib/general/scopes/check-is-portable.ts"; +import { getAtAGlance } from "@/lib/general/scopes/get-at-a-glance.ts"; +import { getBaseDirectory } from "@/lib/general/scopes/get-base-directory.ts"; +import { getExecutableDirectory } from "@/lib/general/scopes/get-executable-directory.ts"; +import { getRelativeDate } from "@/lib/general/scopes/get-relative-date.ts"; +import { getSidebarInnerStyles } from "@/lib/general/scopes/get-sidebar-inner-styles.ts"; +import { initializeLauncher } from "@/lib/general/scopes/initialize-launcher.ts"; + +export default { + capitalize, + checkIsPortable, + getAtAGlance, + getBaseDirectory, + getExecutableDirectory, + getRelativeDate, + getSidebarInnerStyles, + initializeLauncher, +} as const; diff --git a/src/lib/general/scopes/capitalize.ts b/src/lib/general/scopes/capitalize.ts new file mode 100644 index 0000000..f634d4d --- /dev/null +++ b/src/lib/general/scopes/capitalize.ts @@ -0,0 +1,3 @@ +export function capitalize(input: string): string { + return input.charAt(0).toUpperCase() + input.slice(1); +} \ No newline at end of file diff --git a/src/lib/general/scopes/check-is-portable.ts b/src/lib/general/scopes/check-is-portable.ts new file mode 100644 index 0000000..06fccce --- /dev/null +++ b/src/lib/general/scopes/check-is-portable.ts @@ -0,0 +1,17 @@ +import { BaseDirectory, exists } from "@tauri-apps/plugin-fs"; + +export async function checkIsPortable(): Promise { + try { + await exists("i_need_to_prepare_for_my_final_terms.txt", { + "baseDir": BaseDirectory.Desktop, + }); + + /* + * If user is using a non-portable version of the launcher, + * then the 'exists' function should throw an error before this statement + */ + return true; + } catch { + return false; + } +} \ No newline at end of file diff --git a/src/lib/general/scopes/get-at-a-glance.ts b/src/lib/general/scopes/get-at-a-glance.ts new file mode 100644 index 0000000..b21d62f --- /dev/null +++ b/src/lib/general/scopes/get-at-a-glance.ts @@ -0,0 +1,51 @@ +import type { AtAGlanceType } from "@/types/ui/at-a-glance.type.ts"; + +const atAGlanceMessages = [ + { + "title" : "A promising future", + "subtitle": "without JavaScript", + }, + { + "title" : "These messages", + "subtitle": "were inspired by the \"At a Glance\" android widget", + }, + { + "title" : "Kaede", + "subtitle": "was not built in a day", + }, + { + "title" : "%date%", + "subtitle": "What a great day to play Minecraft, right?", + }, +]; + +function transformAtAGlanceMessages(message: AtAGlanceType): AtAGlanceType { + const currentDate = (new Date) + .toDateString() + .split(" "); + + message.title = message.title.replace("%date%", ( + currentDate[0] + ", " + currentDate[1] + " " + currentDate[2] + )); + + return message; +} + +export function getAtAGlance(currentTitle?: string): AtAGlanceType { + const randomIndex = Math.floor( + Math.random() * atAGlanceMessages.length, + ); + const newMessage = atAGlanceMessages[randomIndex]; + + if (currentTitle === newMessage.title) { + const uniqueIndex = randomIndex - 1; + + if (uniqueIndex < 0) { + return transformAtAGlanceMessages(atAGlanceMessages[randomIndex + 1]); + } + + return transformAtAGlanceMessages(atAGlanceMessages[uniqueIndex]); + } + + return transformAtAGlanceMessages(newMessage); +} diff --git a/src/lib/general/scopes/get-base-directory.ts b/src/lib/general/scopes/get-base-directory.ts new file mode 100644 index 0000000..2a1b20a --- /dev/null +++ b/src/lib/general/scopes/get-base-directory.ts @@ -0,0 +1,11 @@ +import { appDataDir } from "@tauri-apps/api/path"; + +import General from "@/lib/general"; + +export async function getBaseDirectory(portable?: boolean): Promise { + const safePortable = portable ?? await General.checkIsPortable(); + + return safePortable + ? await General.getExecutableDirectory() + : await appDataDir(); +} \ No newline at end of file diff --git a/src/lib/general/scopes/get-executable-directory.ts b/src/lib/general/scopes/get-executable-directory.ts new file mode 100644 index 0000000..eae9648 --- /dev/null +++ b/src/lib/general/scopes/get-executable-directory.ts @@ -0,0 +1,5 @@ +import { invoke } from "@tauri-apps/api/core"; + +export async function getExecutableDirectory(): Promise { + return await invoke("get_executable_directory"); +} \ No newline at end of file diff --git a/src/lib/helpers/get-relative-date.test.ts b/src/lib/general/scopes/get-relative-date.test.ts similarity index 83% rename from src/lib/helpers/get-relative-date.test.ts rename to src/lib/general/scopes/get-relative-date.test.ts index 910b05a..767615f 100644 --- a/src/lib/helpers/get-relative-date.test.ts +++ b/src/lib/general/scopes/get-relative-date.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "vitest"; -import { getRelativeDate } from "./get-relative-date"; + +import { getRelativeDate } from "./get-relative-date.ts"; const testData = [ [ @@ -34,8 +35,8 @@ const testData = [ for (const [input, output] of testData) { const testName = `Relative Date: ${JSON.stringify(input)}`; - // example date was taken from MDN docs - const equalDate = new Date("December 17, 1995 03:24:00"); + // Example date was taken from MDN docs + const equalDate = new Date("December 17, 1995 00:24:00 UTC"); test(testName, () => { expect( diff --git a/src/lib/helpers/get-relative-date.ts b/src/lib/general/scopes/get-relative-date.ts similarity index 80% rename from src/lib/helpers/get-relative-date.ts rename to src/lib/general/scopes/get-relative-date.ts index 351193b..a709c96 100644 --- a/src/lib/helpers/get-relative-date.ts +++ b/src/lib/general/scopes/get-relative-date.ts @@ -6,12 +6,12 @@ export function getRelativeDate({ milliseconds = 0, from = new Date, }: { - "days"? : number; - "hours"? : number; - "minutes"? : number; - "seconds"? : number; + "days" ?: number; + "hours" ?: number; + "minutes" ?: number; + "seconds" ?: number; "milliseconds"?: number; - "from"? : Date; + "from" ?: Date; }): Date { const relativeDate = new Date(from); const result = { diff --git a/src/lib/general/scopes/get-sidebar-inner-styles.ts b/src/lib/general/scopes/get-sidebar-inner-styles.ts new file mode 100644 index 0000000..3a4b5be --- /dev/null +++ b/src/lib/general/scopes/get-sidebar-inner-styles.ts @@ -0,0 +1,22 @@ +export function getSidebarInnerStyles( + background: string | null | undefined, + textColor: string | null | undefined, + blur: number | null | undefined, +): { + "background" : string; + "color" : string; + "backdropFilter"?: string; +} { + const backgroundOutput = background ?? "rgb(10, 10, 10)"; + const color = textColor ?? "rgb(255, 255, 255)"; + + if (!blur) { + return { "background": backgroundOutput, color }; + } + + return { + color, + "background" : backgroundOutput, + "backdropFilter": `blur(${blur}px)`, + }; +} diff --git a/src/lib/general/scopes/hash-string.ts b/src/lib/general/scopes/hash-string.ts new file mode 100644 index 0000000..93e172e --- /dev/null +++ b/src/lib/general/scopes/hash-string.ts @@ -0,0 +1,12 @@ +export function hashString(input: string): number { + let hash: number = 0; + + for (let index = 0; index < input.length; index++) { + hash = Math.trunc( + Math.imul(31, hash) + + (input.codePointAt(index) ?? 0), + ); + } + + return hash; +} diff --git a/src/lib/general/scopes/initialize-launcher.ts b/src/lib/general/scopes/initialize-launcher.ts new file mode 100644 index 0000000..25ac45b --- /dev/null +++ b/src/lib/general/scopes/initialize-launcher.ts @@ -0,0 +1,24 @@ +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; + +import { log } from "@/lib/logging/scopes/log.ts"; +import type { ConfigType } from "@/types/application/config.type.ts"; + +export async function initializeLauncher( + config: ConfigType, + startTime: number, +): Promise { + if (!config.misc.showBeforeInitialization) { + /* + * Webview window is still hidden, so make it visible now + * Because frontend is already loaded by this time + */ + log.debug("Making current webview window visible"); + await getCurrentWebviewWindow().show(); + } + + log.info( + "Launcher successfully started in:", + (performance.now() - startTime).toFixed(1), + "ms", + ); +} diff --git a/src/lib/global-state-helpers/index.ts b/src/lib/global-state-helpers/index.ts new file mode 100644 index 0000000..6547bfc --- /dev/null +++ b/src/lib/global-state-helpers/index.ts @@ -0,0 +1,24 @@ +import { ApplicationNamespace } from "@/constants/application.ts"; +import { + getConfigGlobalStates, +} from "@/lib/global-state-helpers/scopes/get-config-global-states.ts"; +import { + getDefaultGlobalStates, +} from "@/lib/global-state-helpers/scopes/get-default-global-states.ts"; +import { Layout } from "@/lib/global-state-helpers/scopes/layout.ts"; +import { Logs } from "@/lib/global-state-helpers/scopes/logs.ts"; +import { Pages } from "@/lib/global-state-helpers/scopes/pages.ts"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; + +export default { + "get" : (): GlobalStatesType => window[ApplicationNamespace].__internals.getGlobalStates(), + "change": ( + key: Key, + value: GlobalStatesType[Key], + ): void => window[ApplicationNamespace].__internals.changeGlobalStates(key, value), + "getFromConfig": getConfigGlobalStates, + "getDefault" : getDefaultGlobalStates, + Layout, + Logs, + Pages, +} as const; diff --git a/src/lib/global-state-helpers/scopes/change-global-state.ts b/src/lib/global-state-helpers/scopes/change-global-state.ts new file mode 100644 index 0000000..66b814a --- /dev/null +++ b/src/lib/global-state-helpers/scopes/change-global-state.ts @@ -0,0 +1,112 @@ +import { nextTick } from "vue"; + +import { ApplicationNamespace } from "@/constants/application.ts"; +import { HookMappings } from "@/constants/hooks.ts"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; +import type { ExtensionStatusType } from "@/types/extensions/hook-return.type.ts"; + +export function __changeGlobalState( + key: Key, + value: GlobalStatesType[Key], + setValue: (key: Key, value: GlobalStatesType[Key]) => void, +): void { + const timeMeasurementStartBefore = performance.now(); + const mappedKey = HookMappings[key]; + const beforeHooks = window[ApplicationNamespace].hooks[mappedKey].before; + const afterHooks = window[ApplicationNamespace].hooks[mappedKey].after; + + // Global states have not changed yet + log.debug(log.templates.hooks.iterate.start( + mappedKey, + "before", + beforeHooks.length, + )); + for (const [index, storedFunction] of beforeHooks.entries()) { + const timeMeasurementStartHook = performance.now(); + const hook = storedFunction as (anything: unknown) => unknown; + + log.debug(log.templates.hooks.iterate.execution( + mappedKey, + "before", + index, + "sync", + )); + const { status, response } = hook(value) as { + "status" : ExtensionStatusType; + "response": GlobalStatesType[Key] | undefined; + }; + const timeMeasurementEndHook = performance.now(); + const currentBeforeHookTime = timeMeasurementEndHook - timeMeasurementStartHook; + + log.debug(log.templates.hooks.iterate.response( + mappedKey, + { status, response }, + "before", + index, + currentBeforeHookTime, + )); + + if (status === "stop") { + if (response !== undefined) { + setValue(key, response); + } + + return; + } + } + + const timeMeasurementEndBefore = performance.now(); + const beforeHooksTime = timeMeasurementEndBefore - timeMeasurementStartBefore; + + log.debug(log.templates.hooks.iterate.end( + mappedKey, + "before", + beforeHooksTime, + )); + log.debug(`Changing global state. Key: ${key}; value: \n${JSON.stringify(value, null, 2)}`); + setValue(key, value); + + nextTick().then(async () => { + const timeMeasurementStartAfter = performance.now(); + + // Global states have changed now + log.debug(log.templates.hooks.iterate.start( + mappedKey, + "after", + afterHooks.length, + )); + for (const [index, storedFunction] of afterHooks.entries()) { + const timeMeasurementStartHook = performance.now(); + const hook = storedFunction as (anything: unknown) => Promise; + + log.debug(log.templates.hooks.iterate.execution( + mappedKey, + "after", + index, + "async", + )); + await hook(value); + + const timeMeasurementEndHook = performance.now(); + const currentAfterHookTime = timeMeasurementEndHook - timeMeasurementStartHook; + + log.debug(log.templates.hooks.iterate["no-response"]( + mappedKey, + "after", + index, + currentAfterHookTime, + "async", + )); + } + + const timeMeasurementEndAfter = performance.now(); + const afterHooksTime = timeMeasurementEndAfter - timeMeasurementStartAfter; + + log.debug(log.templates.hooks.iterate.end( + mappedKey, + "after", + afterHooksTime, + )); + }); +} \ No newline at end of file diff --git a/src/lib/global-state-helpers/scopes/get-config-global-states.ts b/src/lib/global-state-helpers/scopes/get-config-global-states.ts new file mode 100644 index 0000000..e30b634 --- /dev/null +++ b/src/lib/global-state-helpers/scopes/get-config-global-states.ts @@ -0,0 +1,91 @@ +import { join } from "@tauri-apps/api/path"; + +import { ApplicationNamespace, ContextMenuItems } from "@/constants/application.ts"; +import EnglishTranslations from "@/constants/english.json"; +import { FileStructure } from "@/constants/file-structure.ts"; +import { RouteItems, Routes } from "@/constants/routes.ts"; +import Configs from "@/lib/configs"; +import General from "@/lib/general"; +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { ConfigType } from "@/types/application/config.type.ts"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; + +export async function getConfigGlobalStates(fresh?: boolean): Promise { + const searchParameters = new URLSearchParams(location.search); + + /* + * We are saving at least 30 ms by re-using already fetched config, portable and base directory. + * + * Initial config was provided by the 'main.ts' code + */ + let configFile: ConfigType = window[ApplicationNamespace].__internals.initialConfig; + + log.debug("Checking if launcher is in portable version"); + // Portable status was provided by the 'main.ts' code + const portable: boolean = window[ApplicationNamespace].__internals.initialPortable + // Unless it was not? + ?? await General.checkIsPortable(); + + log.debug("Getting base directory"); + // Base directory was provided by the 'main.ts' code + const baseDirectory = window[ApplicationNamespace].__internals.initialBaseDirectory + // Unless it was not? + ?? await General.getBaseDirectory(portable); + + if (fresh) { + log.debug("Getting a fresh copy of config since the 'fresh' state is:", fresh.toString()); + configFile = await Configs.getSafe(baseDirectory); + } + + const portableVersion = portable ? "Portable" : "Non-portable"; + + log.info(`Running in the '${portableVersion}' version`); + log.debug("Finishing 'getConfigGlobalStates' execution"); + + return { + ...configFile, + "translations": EnglishTranslations, + "fileSystem" : { + "portable": portable, + "base" : baseDirectory, + "folders" : { + "logs" : await join(baseDirectory, FileStructure.Logs.Path), + "instances": await join(baseDirectory, FileStructure.Instances.Path), + "resources": await join(baseDirectory, FileStructure.Resources.Path), + }, + "files": { + "config": await join(baseDirectory, FileStructure.Config.Name), + "log" : await join(baseDirectory, FileStructure.Logs.Path, FileStructure.Logs.Name), + }, + }, + "pages": { + "current": GlobalStateHelpers.Pages.getRouteFromSearchParameters(searchParameters), + "states" : { + "home" : {}, + "library" : {}, + "settings" : { "tab": "extensions" }, + "add-instance": {}, + "none" : {}, + }, + }, + "sidebarItems": [ + ...RouteItems.map(item => { + return { + "path" : item.Path, + "icon" : item.Icon, + "name" : item.Path, + "action": (): void => GlobalStateHelpers.Pages.navigate(item.Path), + }; + }), + "divider", + { + "path" : Routes.AddInstance, + "icon" : "i-lucide-plus", + "name" : Routes.AddInstance, + "action": (): void => GlobalStateHelpers.Pages.navigate(Routes.AddInstance), + }, + ], + "contextMenuItems": [...ContextMenuItems], + }; +} diff --git a/src/lib/global-state-helpers/scopes/get-default-global-states.ts b/src/lib/global-state-helpers/scopes/get-default-global-states.ts new file mode 100644 index 0000000..33c09bc --- /dev/null +++ b/src/lib/global-state-helpers/scopes/get-default-global-states.ts @@ -0,0 +1,83 @@ +import { ContextMenuItems } from "@/constants/application.ts"; +import EnglishTranslations from "@/constants/english.json"; +import { RouteItems, Routes } from "@/constants/routes.ts"; +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; + +export function getDefaultGlobalStates(): GlobalStatesType { + const searchParameters = new URLSearchParams(location.search); + + return { + "translations": EnglishTranslations, + "fileSystem" : undefined, + "layout" : { + "custom" : false, + "background": { + "url" : null, + "key" : null, + "blur" : null, + "color": null, + }, + "sidebar": { + "blur" : null, + "color" : null, + "ripple" : null, + "sparkles" : null, + "background": null, + }, + "atAGlance": { + "title" : null, + "subtitle": null, + }, + }, + "pages": { + "current": GlobalStateHelpers.Pages.getRouteFromSearchParameters(searchParameters), + "states" : { + "home" : {}, + "library" : {}, + "settings" : { "tab": "extensions" }, + "add-instance": {}, + "none" : {}, + }, + }, + "logs": { + "show" : false, + "lineBreaks" : false, + "virtualized": false, + "dates" : false, + "filtering" : "", + }, + "sidebarItems": [ + ...RouteItems.map(item => { + return { + "path" : item.Path, + "icon" : item.Icon, + "name" : item.Path, + "action": (): void => GlobalStateHelpers.Pages.navigate(item.Path), + }; + }), + "divider", + { + "path" : Routes.AddInstance, + "icon" : "i-lucide-plus", + "name" : Routes.AddInstance, + "action": (): void => GlobalStateHelpers.Pages.navigate(Routes.AddInstance), + }, + ], + "contextMenuItems": [...ContextMenuItems], + "development" : { + "showFPS" : false, + "enableDebugMode" : false, + "enableNativeReloadKeyBinds": false, + }, + "misc": { + "showBeforeInitialization": false, + "enableDiscordRPC" : false, + }, + "minecraft": { + "windowHeight": 480, + "windowWidth" : 854, + "jvmArgs" : "", + }, + }; +} diff --git a/src/lib/global-state-helpers/scopes/layout.ts b/src/lib/global-state-helpers/scopes/layout.ts new file mode 100644 index 0000000..8bb8cae --- /dev/null +++ b/src/lib/global-state-helpers/scopes/layout.ts @@ -0,0 +1,14 @@ +import GlobalStateHelpers from "@/lib/global-state-helpers"; + +function toggle(state?: boolean): void { + const layout = GlobalStateHelpers.get().layout; + + GlobalStateHelpers.change("layout", { + ...layout, + "custom": state ?? !layout.custom, + }); +} + +export const Layout = { + toggle, +}; \ No newline at end of file diff --git a/src/lib/global-state-helpers/scopes/logs.ts b/src/lib/global-state-helpers/scopes/logs.ts new file mode 100644 index 0000000..9953430 --- /dev/null +++ b/src/lib/global-state-helpers/scopes/logs.ts @@ -0,0 +1,33 @@ +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; + +function toggle( + key: Key, + state?: boolean, +): void { + const logs = GlobalStateHelpers.get().logs; + + if (typeof logs[key] !== "boolean") { + return; + } + + const newLogs = { + ...logs, + [key]: state ?? !logs[key], + }; + + GlobalStateHelpers.change("logs", newLogs); +} +function filterBy(newValue: string): void { + const logs = GlobalStateHelpers.get().logs; + + GlobalStateHelpers.change("logs", { + ...logs, + "filtering": newValue, + }); +} + +export const Logs = { + toggle, + filterBy, +}; \ No newline at end of file diff --git a/src/lib/global-state-helpers/scopes/pages.ts b/src/lib/global-state-helpers/scopes/pages.ts new file mode 100644 index 0000000..ee2be5a --- /dev/null +++ b/src/lib/global-state-helpers/scopes/pages.ts @@ -0,0 +1,74 @@ +import { Routes } from "@/constants/routes.ts"; +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; +import type { RouteType } from "@/types/application/route.type.ts"; + +const navigate = (path: RouteType): void => { + const pages = GlobalStateHelpers.get().pages; + + GlobalStateHelpers.change("pages", { + ...pages, + "current": path, + }); +}; +const getState = ( + key: Key, +): GlobalStatesType["pages"]["states"][Key] => { + return GlobalStateHelpers.get().pages.states[key]; +}; +const getAllStates = (): GlobalStatesType["pages"]["states"] => { + return GlobalStateHelpers.get().pages.states; +}; +const addToState = ( + key: Key, + value: GlobalStatesType["pages"]["states"][Key], +): void => { + const pages = GlobalStateHelpers.get().pages; + const newStates = { ...pages.states }; + + newStates[key] = { + ...newStates[key], + ...value, + }; + + GlobalStateHelpers.change("pages", { + ...pages, + "states": newStates, + }); +}; +const replaceState = ( + key: Key, + value: GlobalStatesType["pages"]["states"][Key], +): void => { + const pages = GlobalStateHelpers.get().pages; + const newStates = { ...pages.states, [key]: value }; + + GlobalStateHelpers.change("pages", { + ...pages, + "states": newStates, + }); +}; +const getRouteFromSearchParameters = (searchParameters: URLSearchParams): RouteType => { + const route: string | null = searchParameters.get("route"); + + if (!route) { + return Routes.Home; + } + + for (const path of Object.values(Routes)) { + if (route === path) { + return path; + } + } + + return Routes.Home; +}; + +export const Pages = { + navigate, + getState, + getAllStates, + addToState, + replaceState, + getRouteFromSearchParameters, +}; \ No newline at end of file diff --git a/src/lib/globals/index.ts b/src/lib/globals/index.ts new file mode 100644 index 0000000..44f2284 --- /dev/null +++ b/src/lib/globals/index.ts @@ -0,0 +1,5 @@ +import { declareWindow } from "@/lib/globals/scopes/declare-window.ts"; + +export default { + declareWindow, +} as const; \ No newline at end of file diff --git a/src/lib/globals/scopes/declare-window.ts b/src/lib/globals/scopes/declare-window.ts new file mode 100644 index 0000000..221314b --- /dev/null +++ b/src/lib/globals/scopes/declare-window.ts @@ -0,0 +1,129 @@ +import { cancel, onInvalidUrl, onUrl, start } from "@fabianlars/tauri-plugin-oauth"; +import * as DiscordRPC from "tauri-plugin-drpc"; +import * as DiscordRPCClasses from "tauri-plugin-drpc/activity.ts"; + +import { ApplicationNamespace } from "@/constants/application.ts"; +import Configs from "@/lib/configs"; +import DevelopmentModeHelpers from "@/lib/development-mode-helpers"; +import Errors from "@/lib/errors"; +import ExtensionsManager from "@/lib/extensions-manager"; +import General from "@/lib/general"; +import GlobalStateHelpers from "@/lib/global-state-helpers"; +import Globals from "@/lib/globals"; +import Instances from "@/lib/instances"; +import Logging from "@/lib/logging"; +import Schemas from "@/lib/schemas"; +import type { ConfigType } from "@/types/application/config.type.ts"; +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; +import type { InstanceStatesType } from "@/types/application/instance-states.type.ts"; + +function placeholderFunction(): void {} + +export function declareWindow(): void { + window.__TAURI_PLUGINS_COMMUNITY__ = { + "discord": { + ...DiscordRPC, + ...DiscordRPCClasses, + }, + "oauth2": { + cancel, + onInvalidUrl, + onUrl, + start, + }, + }; + window[ApplicationNamespace] = { + "__internals": { + + /* Fields that contain a 'placeholderFunction' will be overwritten */ + "getGlobalStates" : placeholderFunction as () => GlobalStatesType, + "changeGlobalStates" : placeholderFunction, + "getInstanceStates" : placeholderFunction as () => InstanceStatesType, + "changeInstanceStates": placeholderFunction, + "requestPermissions" : placeholderFunction as () => Promise>, + // This field will be overwritten too + "initialConfig" : {} as ConfigType, + }, + "variables": { + "rippleColor" : "#ffffff15", + "sparklesColorRGB": "255 255 255", + }, + "libs": { + Configs, + DevelopmentModeHelpers, + Errors, + ExtensionsManager, + General, + GlobalStateHelpers, + Globals, + Instances, + Logging, + Schemas, + "ContextMenu": { + + /* Fields that contain a 'placeholderFunction' will be overwritten */ + "show" : placeholderFunction, + "close": placeholderFunction, + }, + "Pages": { + + /* Fields that contain a 'placeholderFunction' will be overwritten */ + "mount" : placeholderFunction, + "unmount": placeholderFunction, + }, + }, + "hooks": { + "getConfigFile": { + "before": [], + "after" : [], + }, + "getDefaultConfig": { + "before": [], + }, + "onFileSystemChange": { + "before": [], + "after" : [], + }, + "onPagesChange": { + "before": [], + "after" : [], + }, + "onLayoutChange": { + "before": [], + "after" : [], + }, + "onLogsChange": { + "before": [], + "after" : [], + }, + "onSidebarItemsChange": { + "before": [], + "after" : [], + }, + "onContextMenuItemsChange": { + "before": [], + "after" : [], + }, + "onDevelopmentChange": { + "before": [], + "after" : [], + }, + "onMiscChange": { + "before": [], + "after" : [], + }, + "onMinecraftChange": { + "before": [], + "after" : [], + }, + "onTranslationsChange": { + "before": [], + "after" : [], + }, + "onInstanceChange": { + "before": [], + "after" : [], + }, + }, + }; +} diff --git a/src/lib/handlers/log.ts b/src/lib/handlers/log.ts deleted file mode 100644 index 629380e..0000000 --- a/src/lib/handlers/log.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { debug, info, warn, error } from "@tauri-apps/plugin-log"; - -// We do not care about promises here -export const log = { - "debug": (...input: string[]) => { - debug(input.join(" ")); - }, - "info": (...input: string[]) => { - info(input.join(" ")); - }, - "warn": (...input: string[]) => { - warn(input.join(" ")); - }, - "error": (...input: string[]) => { - error(input.join(" ")); - }, -}; diff --git a/src/lib/instances/index.ts b/src/lib/instances/index.ts new file mode 100644 index 0000000..2f4870b --- /dev/null +++ b/src/lib/instances/index.ts @@ -0,0 +1,12 @@ +import { ApplicationNamespace } from "@/constants/application.ts"; +import { readStoredInstances } from "@/lib/instances/scopes/read-stored-instances.ts"; +import type { InstanceStatesType } from "@/types/application/instance-states.type.ts"; + +export default { + "get" : (): InstanceStatesType => window[ApplicationNamespace].__internals.getInstanceStates(), + "change": ( + key: Key, + value: InstanceStatesType[Key], + ): void => window[ApplicationNamespace].__internals.changeInstanceStates(key, value), + "readStored": readStoredInstances, +} as const; \ No newline at end of file diff --git a/src/lib/instances/scopes/change-instance-state.ts b/src/lib/instances/scopes/change-instance-state.ts new file mode 100644 index 0000000..4541086 --- /dev/null +++ b/src/lib/instances/scopes/change-instance-state.ts @@ -0,0 +1,111 @@ +import { nextTick } from "vue"; + +import { ApplicationNamespace } from "@/constants/application.ts"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { InstanceStatesType } from "@/types/application/instance-states.type.ts"; +import type { ExtensionStatusType } from "@/types/extensions/hook-return.type.ts"; + +export function __changeInstanceState( + key: Key, + value: InstanceStatesType[Key], + setValue: (key: Key, value: InstanceStatesType[Key]) => void, +): void { + const timeMeasurementStartBefore = performance.now(); + const beforeHooks = window[ApplicationNamespace].hooks.onInstanceChange.before; + const afterHooks = window[ApplicationNamespace].hooks.onInstanceChange.after; + + // Instance states have not changed yet + log.debug(log.templates.hooks.iterate.start( + "onInstanceChange", + "before", + beforeHooks.length, + )); + for (const [index, hook] of beforeHooks.entries()) { + const timeMeasurementStartHook = performance.now(); + + log.debug(log.templates.hooks.iterate.execution( + "onInstanceChange", + "before", + index, + "sync", + )); + const { status, response } = hook({ key, value }) as { + "status" : ExtensionStatusType; + "response": { + "key" : Key; + "value": InstanceStatesType[Key]; + } | undefined; + }; + const timeMeasurementEndHook = performance.now(); + const currentBeforeHookTime = timeMeasurementEndHook - timeMeasurementStartHook; + + log.debug(log.templates.hooks.iterate.response( + "onInstanceChange", + { status, response }, + "before", + index, + currentBeforeHookTime, + )); + + if (status === "stop") { + if (response !== undefined) { + setValue(response.key, response.value); + } + + return; + } + } + + const timeMeasurementEndBefore = performance.now(); + const beforeHooksTime = timeMeasurementEndBefore - timeMeasurementStartBefore; + + log.debug(log.templates.hooks.iterate.end( + "onInstanceChange", + "before", + beforeHooksTime, + )); + log.debug(`Changing instance state. Key: ${key}; value: \n${JSON.stringify(value, null, 2)}`); + setValue(key, value); + + nextTick().then(async () => { + const timeMeasurementStartAfter = performance.now(); + + // Global states have changed now + log.debug(log.templates.hooks.iterate.start( + "onInstanceChange", + "after", + afterHooks.length, + )); + for (const [index, hook] of afterHooks.entries()) { + const timeMeasurementStartHook = performance.now(); + + log.debug(log.templates.hooks.iterate.execution( + "onInstanceChange", + "after", + index, + "async", + )); + await hook({ key, value }); + + const timeMeasurementEndHook = performance.now(); + const currentAfterHookTime = timeMeasurementEndHook - timeMeasurementStartHook; + + log.debug(log.templates.hooks.iterate["no-response"]( + "onInstanceChange", + "after", + index, + currentAfterHookTime, + "async", + )); + } + + const timeMeasurementEndAfter = performance.now(); + const afterHooksTime = timeMeasurementEndAfter - timeMeasurementStartAfter; + + log.debug(log.templates.hooks.iterate.end( + "onInstanceChange", + "after", + afterHooksTime, + )); + }); +} \ No newline at end of file diff --git a/src/lib/instances/scopes/read-stored-instances.ts b/src/lib/instances/scopes/read-stored-instances.ts new file mode 100644 index 0000000..d1af87b --- /dev/null +++ b/src/lib/instances/scopes/read-stored-instances.ts @@ -0,0 +1,5 @@ +import type { InstanceStatesType } from "@/types/application/instance-states.type.ts"; + +export async function readStoredInstances(): Promise { + return {}; +} \ No newline at end of file diff --git a/src/lib/launcher/core/folder.ts b/src/lib/launcher/core/folder.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/launcher/core/launch.ts b/src/lib/launcher/core/launch.ts deleted file mode 100644 index 0ffdd02..0000000 --- a/src/lib/launcher/core/launch.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/src/lib/launcher/core/platform.ts b/src/lib/launcher/core/platform.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/launcher/core/version.ts b/src/lib/launcher/core/version.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/logging/index.ts b/src/lib/logging/index.ts new file mode 100644 index 0000000..5f7f716 --- /dev/null +++ b/src/lib/logging/index.ts @@ -0,0 +1,21 @@ +import { getLogEntryInformation } from "@/lib/logging/scopes/get-log-entry-information.ts"; +import { getLogFieldText } from "@/lib/logging/scopes/get-log-field-text.ts"; +import { getLogLevelColor } from "@/lib/logging/scopes/get-log-level-color.ts"; +import { + handleVirtualListTextSelection, +} from "@/lib/logging/scopes/handle-virtual-list-text-selection.ts"; +import { handleVirtualTextCopy } from "@/lib/logging/scopes/handle-virtual-text-copy.ts"; +import { log } from "@/lib/logging/scopes/log.ts"; +import { prepareLogFile } from "@/lib/logging/scopes/prepare-log-file.ts"; + +export default { + getLogEntryInformation, + getLogFieldText, + getLogLevelColor, + handleVirtualListTextSelection, + handleVirtualTextCopy, + prepareLogFile, + + /* 'log' is used separately */ + log, +} as const; \ No newline at end of file diff --git a/src/lib/logging/scopes/get-log-entry-information.ts b/src/lib/logging/scopes/get-log-entry-information.ts new file mode 100644 index 0000000..de2744b --- /dev/null +++ b/src/lib/logging/scopes/get-log-entry-information.ts @@ -0,0 +1,105 @@ +import type { LogEntryInformationType } from "@/types/application/log-entry-information.type.ts"; + +export function getLogEntryInformation(line: string | [number, string]): LogEntryInformationType { + const actualLine = typeof line === "string" ? line : line[1]; + + const parts = actualLine.split("]"); + const current: { + "date" : string; + "time" : string; + "target" : string; + "level" : string; + "message": string; + } = { + "date" : "", + "time" : "", + "target" : "", + "level" : "", + "message": "", + }; + + if (actualLine.startsWith("__kaede-trigger-initial")) { + current.target = "All logs will be displayed here ᓀ‸ᓂ"; + + return current; + } + + if (actualLine.startsWith("__kaede-trigger-virtualized")) { + current.target = "Virtualized mode. All logs will be displayed here ᓀ‸ᓂ"; + + return current; + } + + if (actualLine.startsWith("__kaede-trigger-loading")) { + current.target = "Loading your logs..."; + + return current; + } + + for (const [partIndex, part] of parts.entries()) { + switch (partIndex) { + case 0: { + if (parts.length === 1) { + current.time = part; + + break; + } + + current.date = "[ " + part.slice(1) + " ]"; + + break; + } + case 1: { + current.time = "[ " + part.slice(1) + " ]"; + + break; + } + case 2: { + // If true, then the target part is empty + if (part.startsWith(" ")) { + current.level = "[ " + part.trim().slice(1) + " ]"; + + break; + } + + if (part.startsWith("[webview:")) { + const targetParts = part.split("/"); + + current.target = "[ " + targetParts[targetParts.length - 1] + " ]"; + + break; + } + + current.target = "[ " + part.slice(1) + " ]"; + + break; + } + case 3: { + // If true, then the target part is empty + if (current.level !== "") { + current.message = part; + + break; + } + + current.level = "[ " + part.slice(1) + " ]"; + + break; + } + default: { + // If true, then we stumbled upon a closing bracket in the message + if (partIndex !== parts.length - 1) { + current.message = current.message + part + "]"; + + break; + } + + current.message = current.message + part; + + break; + } + } + } + + return current; +} \ No newline at end of file diff --git a/src/lib/logging/scopes/get-log-field-text.ts b/src/lib/logging/scopes/get-log-field-text.ts new file mode 100644 index 0000000..c14596b --- /dev/null +++ b/src/lib/logging/scopes/get-log-field-text.ts @@ -0,0 +1,52 @@ +import Errors from "@/lib/errors"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { FieldTextType } from "@/types/application/log-field-text.type.ts"; + +export function getLogFieldText(input: string, toSearch: string): string | FieldTextType { + const lowerCasedInput = input.toLowerCase(); + + if (toSearch === "" || !lowerCasedInput.includes(toSearch)) { + return input; + } + + let occurrences: Array; + + try { + occurrences = [ + ...lowerCasedInput.matchAll(new RegExp(toSearch, "g")), + ]; + } catch (error: unknown) { + log.error( + "Couldn't match all search keyword occurrences in the logs:", + Errors.prettify(error), + ); + + return input; + } + + const noOccurrenceFields = []; + const occurrenceExtractions = []; + let previousIndex = 0; + + for (const occurrence of occurrences) { + const occurrenceIndex = occurrence.index; + + noOccurrenceFields.push(input.slice( + previousIndex, + occurrenceIndex, + )); + occurrenceExtractions.push(input.slice( + occurrenceIndex, + occurrenceIndex + toSearch.length, + )); + + previousIndex = occurrenceIndex + toSearch.length; + } + + noOccurrenceFields.push(input.slice(previousIndex)); + + return { + "extractions": occurrenceExtractions, + "fields" : noOccurrenceFields, + }; +} \ No newline at end of file diff --git a/src/lib/logging/scopes/get-log-level-color.ts b/src/lib/logging/scopes/get-log-level-color.ts new file mode 100644 index 0000000..95f3a0e --- /dev/null +++ b/src/lib/logging/scopes/get-log-level-color.ts @@ -0,0 +1,19 @@ +export function getLogLevelColor(level: string): string { + if (level.includes("DEBUG")) { + return "text-white"; + } + + if (level.includes("INFO")) { + return "text-blue-400"; + } + + if (level.includes("WARN")) { + return "text-yellow-400"; + } + + if (level.includes("ERROR")) { + return "text-red-600"; + } + + return "text-neutral-400"; +} \ No newline at end of file diff --git a/src/lib/logging/scopes/handle-virtual-list-text-selection.ts b/src/lib/logging/scopes/handle-virtual-list-text-selection.ts new file mode 100644 index 0000000..f002cd2 --- /dev/null +++ b/src/lib/logging/scopes/handle-virtual-list-text-selection.ts @@ -0,0 +1,34 @@ +export function handleVirtualListTextSelection( + event: MouseEvent, + isSelection: boolean, + currentSelectionRange: [number, number] | undefined, +): [number, number] | "save" | undefined { + if (!isSelection) { + return undefined; + } + + const initialTarget = event.target as HTMLSpanElement; + + if (!initialTarget?.id) { + return "save"; + } + + const idValues = initialTarget.id.split("-"); + const extractedIndex = idValues[idValues.length - 1]; + + if (!extractedIndex) { + return undefined; + } + + const parsedIndex = Number.parseInt(extractedIndex); + + if (Number.isNaN(parsedIndex)) { + return undefined; + } + + if (!currentSelectionRange) { + return [parsedIndex, parsedIndex]; + } + + return [currentSelectionRange[0], parsedIndex]; +} \ No newline at end of file diff --git a/src/lib/logging/scopes/handle-virtual-text-copy.ts b/src/lib/logging/scopes/handle-virtual-text-copy.ts new file mode 100644 index 0000000..da6758d --- /dev/null +++ b/src/lib/logging/scopes/handle-virtual-text-copy.ts @@ -0,0 +1,62 @@ +import Errors from "@/lib/errors"; +import { log } from "@/lib/logging/scopes/log.ts"; + +export async function handleVirtualTextCopy( + copied: boolean, + range: [number, number] | undefined, + logs: Array, + setCopied: (state: boolean) => void, +): Promise { + if (copied || !range || !logs[0]) { + return; + } + + try { + const rangeStartIndex = Math.min(range[0], range[1]); + const rangeEndIndex = Math.max(range[0], range[1]); + + let slicedSelectionText: string; + + if (typeof logs[0] === "string") { + slicedSelectionText = logs.slice(rangeStartIndex, rangeEndIndex + 1).join("\n"); + } else { + let relativeIndexStart: number = 0; + let relativeIndexEnd: number = 0; + + for (const [index, currentLog] of logs.entries()) { + const currentLogIndex = currentLog[0] as number; + + if (currentLogIndex === rangeStartIndex) { + relativeIndexStart = index; + } + + if (currentLogIndex === rangeEndIndex) { + relativeIndexEnd = index; + + break; + } + } + + slicedSelectionText = logs + .map(mappingEntry => mappingEntry[1]) + .slice( + relativeIndexStart, + relativeIndexEnd + 1, + ) + .join("\n"); + } + + await navigator.clipboard.writeText(slicedSelectionText); + + setCopied(true); + + setTimeout(() => { + setCopied(false); + }, 500); + } catch (error: unknown) { + log.error( + "Couldn't copy the selected text in virtualized log viewer:", + Errors.prettify(error), + ); + } +} \ No newline at end of file diff --git a/src/lib/logging/scopes/log.ts b/src/lib/logging/scopes/log.ts new file mode 100644 index 0000000..1fb1c29 --- /dev/null +++ b/src/lib/logging/scopes/log.ts @@ -0,0 +1,89 @@ +import { debug, error, info, warn } from "@tauri-apps/plugin-log"; + +import { capitalize } from "@/lib/general/scopes/capitalize.ts"; +import type { LogMethodType } from "@/types/application/log-method.type.ts"; + +/* + * We do not care about promises here + * Yeah, that can possibly lead to racing conditions... + */ +export const log = { + + /* + * 'log#debug' will point to the '__debug-defined' when debug mode is enabled + */ + "__debug-defined": (...input: string[]): void => { + debug(input.join(" ")); + }, + + /* + * 'log#debug' will point to the '__debug-undefined' when debug mode is disabled + */ + "__debug-undefined": ((): void => {}) as LogMethodType, + + /* + * Actual logging methods + */ + "debug": ((): void => {}) as LogMethodType, + "info" : (...input: string[]): void => { + info(input.join(" ")); + }, + "warn": (...input: string[]): void => { + warn(input.join(" ")); + }, + "error": (...input: string[]): void => { + error(input.join(" ")); + }, + + /* + * Some log messages have the same structure + */ + "templates": { + "hooks": { + "iterate": { + "start": (key: string, position: "before" | "after", length: number): string => ( + [ + `Starting iterating through hooks for '${key}.${position}'.`, + `Array length: ${length}`, + ].join(" ") + ), + "execution": ( + key: string, + position: "before" | "after", + index: number, + type: "sync" | "async", + ): string => ( + `'${key}.${position}' iterate: '${index}' index. ${capitalize(type)} hook execution` + ), + "response": ( + key: string, + value: unknown, + position: "before" | "after", + index: number, + time: number, + ): string => ( + [ + `'${key}.${position}' iterate: '${index}' index.`, + `Hook execution ended in ${time} ms.`, + `Hook response: \n${JSON.stringify(value, null, 2)}`, + ].join(" ") + ), + "no-response": ( + key: string, + position: "before" | "after", + index: number, + time: number, + type: "sync" | "async", + ): string => ( + [ + `'${key}.${position}' iterate: '${index}' index.`, + `${capitalize(type)} hook executed in ${time} ms`, + ].join(" ") + ), + "end": (key: string, position: "before" | "after", time: number): string => ( + `All '${key}.${position}' hooks were executed in ${time} ms` + ), + }, + }, + }, +}; diff --git a/src/lib/logging/scopes/prepare-log-file.ts b/src/lib/logging/scopes/prepare-log-file.ts new file mode 100644 index 0000000..388c7f8 --- /dev/null +++ b/src/lib/logging/scopes/prepare-log-file.ts @@ -0,0 +1,95 @@ +import { join } from "@tauri-apps/api/path"; +import { + copyFile, + create, + type DirEntry, + mkdir, + readDir, + readTextFile, + writeTextFile, +} from "@tauri-apps/plugin-fs"; + +import { ApplicationName } from "@/constants/application.ts"; + +function getNumberFromLogFilename(filename: string): number { + // 'kaede-0.log' -> 'kaede-0' + const currentLogName: string | undefined = filename.split(".")?.[0]; + // 'kaede-0' -> '0'; + const currentLogNumber: number = Number(currentLogName?.split?.("-")?.[1]); + + // User might have created some nonsense files in the 'logs' directory + if (Number.isNaN(currentLogNumber)) { + return 0; + } + + return currentLogNumber; +} + +/** + * Strategy: + * + * if 'latest.log' exists, and it is not empty, + * then we copy everything from it to the 'kaede-{number}.log' file + * and clear the 'latest.log' file since we want to separate previous launcher logs from current + * + * else just write into 'latest.log' without other manipulations + */ +export async function prepareLogFile(baseDirectory: string): Promise { + const logsDirectory = await join(baseDirectory, "logs"); + + // We are assuming that 'latest.log' doesn't exist + let latestLogExists = false; + // And we will keep track of the biggest log file number to make a unique name + let biggestLogNumber = 0; + + // User can delete 'logs' folder while running launcher, and then refresh the launcher + let entries: DirEntry[]; + + try { + entries = await readDir(logsDirectory); + } catch { + entries = []; + + // Error means that 'logs' directory doesn't exist + await mkdir(logsDirectory); + } + + for (const entry of entries) { + // 'latest.log' exists + if (entry.name === "latest.log") { + latestLogExists = true; + + continue; + } + + biggestLogNumber = Math.max(biggestLogNumber, getNumberFromLogFilename(entry.name)); + } + + const latestLogPath = await join(logsDirectory, "latest.log"); + + if (!latestLogExists) { + // 'latest.log' doesn't exist, so we manually create it + const newLatestLogFile = await create(latestLogPath); + + await newLatestLogFile.close(); + + return; + } + + const latestLogContent = await readTextFile(latestLogPath); + + // 'latest.log' is empty; no need to copy empty contents into another log file + if (latestLogContent === "") { + return; + } + + const renamedLogPath = await join( + logsDirectory, + `${ApplicationName.toLowerCase()}-${biggestLogNumber + 1}.log`, + ); + + // Copy existing contents from 'latest.log' to 'kaede-{number}.log' + await copyFile(latestLogPath, renamedLogPath); + // Clear the 'latest.log' file + await writeTextFile(latestLogPath, ""); +} diff --git a/src/lib/main/declare-window.ts b/src/lib/main/declare-window.ts deleted file mode 100644 index 7cba556..0000000 --- a/src/lib/main/declare-window.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApplicationNamespace } from "@/constants/application.ts"; -import { getDefaultConfig } from "@/lib/main/get-default-config.ts"; -import { log } from "@/lib/handlers/log.ts"; -import { extractError } from "@/lib/helpers/extract-error.ts"; -import { getRelativeDate } from "@/lib/helpers/get-relative-date.ts"; -import { getConfigFile } from "@/lib/main/get-config-file.ts"; -import { initializeConfigFile } from "@/lib/main/initialize-config-file.ts"; - -export function declareWindow() { - window[ApplicationNamespace] = { - "constants": {}, - "variables": {}, - "functions": { - log, - extractError, - getRelativeDate, - getConfigFile, - getDefaultConfig, - initializeConfigFile, - }, - "hooks": { - "getConfigFile": { - "before": [], - }, - "getDefaultConfig": { - "before": [], - }, - }, - }; -} \ No newline at end of file diff --git a/src/lib/main/get-config-file.test.ts b/src/lib/main/get-config-file.test.ts deleted file mode 100644 index b0c8bfb..0000000 --- a/src/lib/main/get-config-file.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { beforeEach, expect, test, vi } from "vitest"; -import type { ConfigType } from "@/types/config/config.schema.ts"; - -const defaultConfig: ConfigType = { - "__do_not_touch_VERSION": 1, - "customization" : { - "theme" : "dark", - "accent" : "rose", - "background": "none", - }, - "locale" : "system", - "minecraftWindowHeight": 480, - "minecraftWindowWidth" : 854, -}; - -vi.mock("@/lib/main/get-default-config.ts", async () => { - return { - "getDefaultConfig": async (): Promise => defaultConfig, - }; -}); -vi.mock("@/lib/main/initialize-config-file.ts", async () => { - return { - "initializeConfigFile": async (): Promise => {}, - }; -}); - -const tests: Array<{ - "arguments": { - "exists" : boolean; - "fetchedConfig": string; - }; - "output": unknown; -}> = [ - { - "arguments": { - "exists" : true, - "fetchedConfig": JSON.stringify({}), - }, - "output": defaultConfig, - }, - { - "arguments": { - "exists" : true, - "fetchedConfig": JSON.stringify({ - ...defaultConfig, - "customization": { - ...defaultConfig.customization, - "apparently": "not full validation is a feature. i spent 2 days thinking my tests were broken xd", - }, - "TUYU": "is awesome", - }), - }, - "output": { - "__do_not_touch_VERSION": 1, - "customization" : { - "theme" : "dark", - "accent" : "rose", - "background": "none", - "apparently": "not full validation is a feature. i spent 2 days thinking my tests were broken xd", - }, - "locale" : "system", - "minecraftWindowHeight": 480, - "minecraftWindowWidth" : 854, - "TUYU" : "is awesome", - }, - }, - { - "arguments": { - "exists" : true, - "fetchedConfig": JSON.stringify({ - ...defaultConfig, - "__do_not_touch_VERSION": "-1", - "customization" : { - "theme": "blue", - }, - }), - }, - "output": defaultConfig, - }, - { - "arguments": { - "exists" : true, - "fetchedConfig": JSON.stringify({ - ...defaultConfig, - "__do_not_touch_VERSION": -1, - "customization" : { - "theme" : "light", - "accent" : "red", - "background": "some-url", - "nyaa" : "si", - }, - "locale" : 2, - "minecraftWindowHeight" : "480", - "minecraftWindowHeight ": 500, - }), - }, - "output": defaultConfig, - }, - { - "arguments": { - "exists" : true, - "fetchedConfig": JSON.stringify({ - ...defaultConfig, - "__do_not_touch_VERSION": 2, - "customization" : { - ...defaultConfig.customization, - "background": "some-url", - }, - }), - }, - "output": { - "__do_not_touch_VERSION": 2, - "customization" : { - "theme" : "dark", - "accent" : "rose", - "background": "some-url", - }, - "locale" : "system", - "minecraftWindowHeight": 480, - "minecraftWindowWidth" : 854, - }, - }, - { - "arguments": { - "exists" : true, - "fetchedConfig": "0", - }, - "output": defaultConfig, - }, - { - "arguments": { - "exists" : false, - // If 'exists' were 'true', it would throw an error (expected behaviour) - "fetchedConfig": "", - }, - "output": defaultConfig, - }, -]; - -let index = 0; - -beforeEach(() => { - vi.doMock("@tauri-apps/plugin-fs", async () => { - return { - "BaseDirectory": { - "AppData": 14, - }, - "exists" : async (): Promise => tests[index].arguments.exists, - "readTextFile": async (): Promise => tests[index].arguments.fetchedConfig, - }; - }); -}); - -test.for(tests)( - "Get Config File: %o", async ({ output }) => { - /* - * Original import (top-level) will not be mocked, because 'vi.doMock' is evaluated - * after all imports and 'vi.mock' can't be called dynamically - */ - const { getConfigFile } = await import("./get-config-file.ts"); - - expect( - JSON.stringify(await getConfigFile()), - ).toBe( - JSON.stringify(output), - ); - - index++; - }, -); diff --git a/src/lib/main/get-config-file.ts b/src/lib/main/get-config-file.ts deleted file mode 100644 index a77f027..0000000 --- a/src/lib/main/get-config-file.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { log } from "@/lib/handlers/log.ts"; -import { ApplicationNamespace, ConfigFilename } from "@/constants/application.ts"; -import { getDefaultConfig } from "@/lib/main/get-default-config.ts"; -import { initializeConfigFile } from "@/lib/main/initialize-config-file.ts"; -import { BaseDirectory, exists, readTextFile } from "@tauri-apps/plugin-fs"; -import { type ConfigType, ConfigValidator } from "@/types/config/config.schema.ts"; - -export async function getConfigFile(): Promise { - const hooksArray = window[ApplicationNamespace].hooks.getConfigFile.before; - - log.debug("Starting iterating through hooks for 'getConfigFile.before'"); - for (const [hookIndex, hookFunction] of hooksArray.entries()) { - log.debug("Executing a hook with the next index:", hookIndex.toString()); - const hookResponse = await hookFunction(); - - if (hookResponse.status === "stop") { - log.debug(`A hook with the index of ${hookIndex} has aborted execution`); - - // Awaiting here will just be an unnecessary action - return hookResponse.response; - } - } - - log.debug("Checking if config file exists"); - const configExists = await exists(ConfigFilename, { - "baseDir": BaseDirectory.AppData, - }); - - if (!configExists) { - log.info("Config file doesn't exist"); - log.debug("Initializing a config file"); - await initializeConfigFile(); - - log.debug("Returning a promise with default config"); - - // Awaiting here will just be an unnecessary action - return getDefaultConfig(); - } - - log.info("Config file exists"); - log.debug("Reading a config file"); - const configFile = await readTextFile(ConfigFilename, { - "baseDir": BaseDirectory.AppData, - }); - - log.debug("Parsing config file"); - const parsedConfig: unknown = await JSON.parse(configFile); - - log.debug("Validating config file"); - - /* - * If there is additional unknown properties in object, validation will pass it - * which is actually good because extensions can use same config as the app - */ - const validatedConfig = ConfigValidator.Check(parsedConfig); - - if (!validatedConfig) { - log.info("Config file is invalid"); - log.debug("Returning a promise with default config"); - - // Awaiting here will just be an unnecessary action - return getDefaultConfig(); - } - - log.info("Config file is valid"); - - return parsedConfig; -} diff --git a/src/lib/main/get-default-config.test.ts b/src/lib/main/get-default-config.test.ts deleted file mode 100644 index 89e8422..0000000 --- a/src/lib/main/get-default-config.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect, test, vi } from "vitest"; -import { getDefaultConfig } from "./get-default-config.ts"; -import type { ConfigType } from "@/types/config/config.schema.ts"; - -vi.mock("@tauri-apps/api/window", async () => { - return { - "getCurrentWindow": () => ({ - "theme": async () => "dark", - }), - }; -}); - -const testName = "Default Config: No arguments"; - -test(testName, async () => { - const defaultConfig: ConfigType = { - "__do_not_touch_VERSION": 1, - "customization" : { - "theme" : "dark", - "accent" : "rose", - "background": "none", - }, - "locale" : "system", - "minecraftWindowHeight": 480, - "minecraftWindowWidth" : 854, - }; - - expect( - JSON.stringify(await getDefaultConfig()), - ).toBe( - JSON.stringify(defaultConfig), - ); -}); diff --git a/src/lib/main/get-default-config.ts b/src/lib/main/get-default-config.ts deleted file mode 100644 index 1ffc25d..0000000 --- a/src/lib/main/get-default-config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ConfigType } from "@/types/config/config.schema.ts"; -import { log } from "@/lib/handlers/log.ts"; -import { ApplicationNamespace } from "@/constants/application.ts"; -import { getCurrentWindow } from "@tauri-apps/api/window"; - -export async function getDefaultConfig(): Promise { - const hooksArray = window[ApplicationNamespace].hooks.getDefaultConfig.before; - - log.debug("Starting iterating through hooks for 'getDefaultConfig.before'"); - for (const [hookIndex, hookFunction] of hooksArray.entries()) { - log.debug("Executing a hook with the next index:", hookIndex.toString()); - const hookResponse = await hookFunction(); - - if (hookResponse.status === "stop") { - log.debug(`A hook with the index of ${hookIndex} has aborted execution`); - - // Awaiting here will just be an unnecessary action - return hookResponse.response; - } - } - - log.debug("Getting current window theme"); - const currentTheme: "dark" | "light" = await getCurrentWindow().theme() ?? "dark"; - - return { - "__do_not_touch_VERSION": 1, - "customization" : { - "theme" : currentTheme, - "accent" : "rose", - "background": "none", - }, - "locale" : "system", - "minecraftWindowHeight": 480, - "minecraftWindowWidth" : 854, - }; -} diff --git a/src/lib/main/initialize-launcher.ts b/src/lib/main/initialize-launcher.ts deleted file mode 100644 index f7cf0ac..0000000 --- a/src/lib/main/initialize-launcher.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { log } from "@/lib/handlers/log.ts"; -import { getConfigFile } from "@/lib/main/get-config-file.ts"; -import { getDefaultConfig } from "@/lib/main/get-default-config.ts"; -import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import type { ConfigType } from "@/types/config/config.schema.ts"; - -export async function initializeLauncher(): Promise { - let config: ConfigType; - - try { - log.debug("Getting a config file"); - config = await getConfigFile(); - } catch (error: unknown) { - log.error("Failed to get a config file:", JSON.stringify(error)); - log.debug("Getting default config"); - config = await getDefaultConfig(); - } - - log.info(JSON.stringify(config)); - - /* - * Webview window was hidden by default, so make it visible now - * Because frontend is already loaded by this time - */ - log.debug("Making current webview window visible"); - await getCurrentWebviewWindow().show(); - - log.info("Launcher successfully started"); -} diff --git a/src/lib/schemas/index.ts b/src/lib/schemas/index.ts new file mode 100644 index 0000000..0131b2d --- /dev/null +++ b/src/lib/schemas/index.ts @@ -0,0 +1,12 @@ +import { Compile } from "typebox/compile"; + +import { AssetsSchema } from "@/lib/schemas/scopes/assets.schema.ts"; +import { ConfigSchema } from "@/lib/schemas/scopes/config.schema.ts"; + +const AssetsValidator = Compile(AssetsSchema); +const ConfigValidator = Compile(ConfigSchema); + +export default { + AssetsValidator, + ConfigValidator, +} as const; \ No newline at end of file diff --git a/src/lib/schemas/minecrafts-schemas.ts b/src/lib/schemas/minecrafts-schemas.ts deleted file mode 100644 index 5b57258..0000000 --- a/src/lib/schemas/minecrafts-schemas.ts +++ /dev/null @@ -1,148 +0,0 @@ -import Type, { type Static } from "typebox"; -import { Compile } from "typebox/compile"; - -export const VersionManifest = Type.Object({ - "latest": Type.Object({ - "release" : Type.String(), - "snapshot": Type.String(), - }), - "versions": Type.Array( - Type.Object({ - "id" : Type.String(), - "type": Type.Union([ - Type.Literal("release"), - Type.Literal("snapshot"), - Type.Literal("old-beta"), - Type.Literal("old-alpha"), - ]), - "url" : Type.String({ "format": "url" }), - "time" : Type.String({ "format": "date-time" }), - "releaseTime": Type.String({ "format": "date-time" }), - }), - ), -}); -export type Manifest = Static; - -export const RuleSchema = Type.Object({ - "action": Type.Union([ - Type.Literal("allow"), - Type.Literal("disallow"), - ]), - "os": Type.Optional( - Type.Object({ - "name" : Type.String(), - "arch" : Type.String(), - "version": Type.String(), - }), - ), -}); -export type Rule = Static; - -export const ValueSchema = Type.Union([ - Type.String(), - Type.Array(Type.String()), -]); - -export const ArgumentSchema = Type.Union([ - Type.String(), - Type.Object({ - "rules": Type.Array(RuleSchema), - "value": ValueSchema, - }), -]); - -export const ArgumentsSchema = Type.Object({ - "game": Type.Array(ArgumentSchema), - "jvm" : Type.Array(ArgumentSchema), -}); - -export const ArtifactSchema = Type.Object({ - "path": Type.String(), - "sha1": Type.String(), - "size": Type.Number(), - "url" : Type.String(), -}); -export type Artifact = Static; - -export const ClassifiersSchema = Type.Object({ - "natives-windows": Type.Optional(ArtifactSchema), - "natives-linux" : Type.Optional(ArtifactSchema), - "natives-osx" : Type.Optional(ArtifactSchema), -}); -export type Classifiers = Static; - -export const DownloadSchema = Type.Object({ - "artifact" : ArtifactSchema, - "classifiers": Type.Optional(ClassifiersSchema), -}); - -export const LibrarySchema = Type.Object({ - "name" : Type.String(), - "downloads": DownloadSchema, - "rules" : Type.Optional(Type.Array(RuleSchema)), - "extract" : Type.Optional(Type.Object({ - "exclude": Type.Any(), - })), -}); -export type Library = Static; -export const LibraryValidator = Compile(LibrarySchema); - -export const AssetIndexSchema = Type.Object({ - "id" : Type.String(), - "sha1" : Type.String(), - "size" : Type.Number(), - "totalSize": Type.Number(), - "url" : Type.String(), -}); -export type AssetIndex = Static; -export const AssetIndexValidator = Compile(AssetIndexSchema); - -export const AssetSchema = Type.Object({ - "hash": Type.String(), - "size": Type.Number(), -}); -export type Asset = Static; - -export const AssetsSchema = Type.Object({ - "objects": Type.Array(AssetSchema), -}); -export type Assets = Static; -export const AssetsValidator = Compile(AssetsSchema); - -export const LoggingConfigSchema = Type.Object({ - "id" : Type.String(), - "sha1": Type.String(), - "size": Type.Number(), - "url" : Type.String(), -}); -export type LoggingConfig = Static; - -export const LoggingSchema = Type.Object({ - "client": Type.Object({ - "argument": Type.String(), - "file" : LoggingConfigSchema, - "type" : Type.String(), - }), -}); - -export const JavaVersionSchema = Type.Object({ - "component" : Type.String(), - "majorVersion": Type.Number(), -}); - -export const VersionMetaModernSchema = Type.Object({ - "id" : Type.String(), - "type" : Type.String(), - "mainClass": Type.String(), - "arguments": ArgumentsSchema, - "libraries": Type.Array(LibrarySchema), - "downloads": Type.Object({ - "client": ArtifactSchema, - "server": Type.Optional(ArtifactSchema), - }), - "assetIndex" : AssetIndexSchema, - "assets" : Type.String(), - "javaVersion": JavaVersionSchema, - "logging" : LoggingSchema, -}); -export type VersionMetaModern = Static; \ No newline at end of file diff --git a/src/lib/schemas/scopes/argument.schema.ts b/src/lib/schemas/scopes/argument.schema.ts new file mode 100644 index 0000000..a98f4fe --- /dev/null +++ b/src/lib/schemas/scopes/argument.schema.ts @@ -0,0 +1,14 @@ +import Type from "typebox"; + +import { LibraryRuleSchema } from "@/lib/schemas/scopes/library-rule.schema.ts"; + +export const ArgumentSchema = Type.Union([ + Type.String(), + Type.Object({ + "rules": Type.Array(LibraryRuleSchema), + "value": Type.Union([ + Type.String(), + Type.Array(Type.String()), + ]), + }), +]); \ No newline at end of file diff --git a/src/lib/schemas/scopes/artifact.schema.ts b/src/lib/schemas/scopes/artifact.schema.ts new file mode 100644 index 0000000..cbe1e03 --- /dev/null +++ b/src/lib/schemas/scopes/artifact.schema.ts @@ -0,0 +1,8 @@ +import Type from "typebox"; + +export const ArtifactSchema = Type.Object({ + "path": Type.String(), + "sha1": Type.String(), + "size": Type.Number(), + "url" : Type.String(), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/asset-index.schema.ts b/src/lib/schemas/scopes/asset-index.schema.ts new file mode 100644 index 0000000..cab7c8c --- /dev/null +++ b/src/lib/schemas/scopes/asset-index.schema.ts @@ -0,0 +1,9 @@ +import Type from "typebox"; + +export const AssetIndexSchema = Type.Object({ + "id" : Type.String(), + "sha1" : Type.String(), + "size" : Type.Number(), + "totalSize": Type.Number(), + "url" : Type.String(), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/asset.schema.ts b/src/lib/schemas/scopes/asset.schema.ts new file mode 100644 index 0000000..b324f64 --- /dev/null +++ b/src/lib/schemas/scopes/asset.schema.ts @@ -0,0 +1,6 @@ +import Type from "typebox"; + +export const AssetSchema = Type.Object({ + "hash": Type.String(), + "size": Type.Number(), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/assets.schema.ts b/src/lib/schemas/scopes/assets.schema.ts new file mode 100644 index 0000000..c11e136 --- /dev/null +++ b/src/lib/schemas/scopes/assets.schema.ts @@ -0,0 +1,7 @@ +import Type from "typebox"; + +import { AssetSchema } from "@/lib/schemas/scopes/asset.schema.ts"; + +export const AssetsSchema = Type.Object({ + "objects": Type.Array(AssetSchema), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/classifiers.schema.ts b/src/lib/schemas/scopes/classifiers.schema.ts new file mode 100644 index 0000000..c9baae6 --- /dev/null +++ b/src/lib/schemas/scopes/classifiers.schema.ts @@ -0,0 +1,9 @@ +import Type from "typebox"; + +import { ArtifactSchema } from "@/lib/schemas/scopes/artifact.schema.ts"; + +export const ClassifiersSchema = Type.Object({ + "natives-windows": Type.Optional(ArtifactSchema), + "natives-linux" : Type.Optional(ArtifactSchema), + "natives-osx" : Type.Optional(ArtifactSchema), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/config.schema.ts b/src/lib/schemas/scopes/config.schema.ts new file mode 100644 index 0000000..4c5139a --- /dev/null +++ b/src/lib/schemas/scopes/config.schema.ts @@ -0,0 +1,18 @@ +import { Type } from "typebox"; + +import { DevelopmentSchema } from "@/lib/schemas/scopes/config/development.schema.ts"; +import { LayoutSchema } from "@/lib/schemas/scopes/config/layout.schema.ts"; +import { LogsSchema } from "@/lib/schemas/scopes/config/logs.schema.ts"; +import { MinecraftSchema } from "@/lib/schemas/scopes/config/minecraft.schema.ts"; +import { MiscSchema } from "@/lib/schemas/scopes/config/misc.schema.ts"; + +export const ConfigSchema = Type.Object({ + "development": Type.Union([ + DevelopmentSchema, + Type.Null(), + ]), + "layout" : LayoutSchema, + "logs" : LogsSchema, + "minecraft": MinecraftSchema, + "misc" : MiscSchema, +}); diff --git a/src/lib/schemas/scopes/config/development.schema.ts b/src/lib/schemas/scopes/config/development.schema.ts new file mode 100644 index 0000000..26b9985 --- /dev/null +++ b/src/lib/schemas/scopes/config/development.schema.ts @@ -0,0 +1,7 @@ +import { Type } from "typebox"; + +export const DevelopmentSchema = Type.Object({ + "showFPS" : Type.Boolean(), + "enableDebugMode" : Type.Boolean(), + "enableNativeReloadKeyBinds": Type.Boolean(), +}); diff --git a/src/lib/schemas/scopes/config/layout.schema.ts b/src/lib/schemas/scopes/config/layout.schema.ts new file mode 100644 index 0000000..6d6015b --- /dev/null +++ b/src/lib/schemas/scopes/config/layout.schema.ts @@ -0,0 +1,56 @@ +import { Type } from "typebox"; + +export const LayoutSchema = Type.Object({ + "custom" : Type.Boolean(), + "background": Type.Object({ + "url": Type.Union([ + Type.String(), + Type.Null(), + ]), + "key": Type.Union([ + Type.String(), + Type.Number(), + Type.Null(), + ]), + "blur": Type.Union([ + Type.Number(), + Type.Null(), + ]), + "color": Type.Union([ + Type.String(), + Type.Null(), + ]), + }), + "sidebar": Type.Object({ + "background": Type.Union([ + Type.String(), + Type.Null(), + ]), + "color": Type.Union([ + Type.String(), + Type.Null(), + ]), + "blur": Type.Union([ + Type.Number(), + Type.Null(), + ]), + "ripple": Type.Union([ + Type.String(), + Type.Null(), + ]), + "sparkles": Type.Union([ + Type.String(), + Type.Null(), + ]), + }), + "atAGlance": Type.Object({ + "title": Type.Union([ + Type.String(), + Type.Null(), + ]), + "subtitle": Type.Union([ + Type.String(), + Type.Null(), + ]), + }), +}); diff --git a/src/lib/schemas/scopes/config/logs.schema.ts b/src/lib/schemas/scopes/config/logs.schema.ts new file mode 100644 index 0000000..7cb9dae --- /dev/null +++ b/src/lib/schemas/scopes/config/logs.schema.ts @@ -0,0 +1,9 @@ +import { Type } from "typebox"; + +export const LogsSchema = Type.Object({ + "show" : Type.Boolean(), + "lineBreaks" : Type.Boolean(), + "virtualized": Type.Boolean(), + "dates" : Type.Boolean(), + "filtering" : Type.String(), +}); diff --git a/src/lib/schemas/scopes/config/minecraft.schema.ts b/src/lib/schemas/scopes/config/minecraft.schema.ts new file mode 100644 index 0000000..2a0268f --- /dev/null +++ b/src/lib/schemas/scopes/config/minecraft.schema.ts @@ -0,0 +1,7 @@ +import { Type } from "typebox"; + +export const MinecraftSchema = Type.Object({ + "windowHeight": Type.Number(), + "windowWidth" : Type.Number(), + "jvmArgs" : Type.String(), +}); diff --git a/src/lib/schemas/scopes/config/misc.schema.ts b/src/lib/schemas/scopes/config/misc.schema.ts new file mode 100644 index 0000000..716eb91 --- /dev/null +++ b/src/lib/schemas/scopes/config/misc.schema.ts @@ -0,0 +1,6 @@ +import { Type } from "typebox"; + +export const MiscSchema = Type.Object({ + "showBeforeInitialization": Type.Boolean(), + "enableDiscordRPC" : Type.Boolean(), +}); diff --git a/src/lib/schemas/scopes/download.schema.ts b/src/lib/schemas/scopes/download.schema.ts new file mode 100644 index 0000000..09a1e51 --- /dev/null +++ b/src/lib/schemas/scopes/download.schema.ts @@ -0,0 +1,9 @@ +import Type from "typebox"; + +import { ArtifactSchema } from "@/lib/schemas/scopes/artifact.schema.ts"; +import { ClassifiersSchema } from "@/lib/schemas/scopes/classifiers.schema.ts"; + +export const DownloadSchema = Type.Object({ + "artifact" : ArtifactSchema, + "classifiers": Type.Optional(ClassifiersSchema), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/java-version.schema.ts b/src/lib/schemas/scopes/java-version.schema.ts new file mode 100644 index 0000000..747ca15 --- /dev/null +++ b/src/lib/schemas/scopes/java-version.schema.ts @@ -0,0 +1,6 @@ +import Type from "typebox"; + +export const JavaVersionSchema = Type.Object({ + "component" : Type.String(), + "majorVersion": Type.Number(), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/library-rule.schema.ts b/src/lib/schemas/scopes/library-rule.schema.ts new file mode 100644 index 0000000..34cb6f6 --- /dev/null +++ b/src/lib/schemas/scopes/library-rule.schema.ts @@ -0,0 +1,15 @@ +import Type from "typebox"; + +export const LibraryRuleSchema = Type.Object({ + "action": Type.Union([ + Type.Literal("allow"), + Type.Literal("disallow"), + ]), + "os": Type.Optional( + Type.Object({ + "name" : Type.String(), + "arch" : Type.String(), + "version": Type.String(), + }), + ), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/library.schema.ts b/src/lib/schemas/scopes/library.schema.ts new file mode 100644 index 0000000..f990b4b --- /dev/null +++ b/src/lib/schemas/scopes/library.schema.ts @@ -0,0 +1,13 @@ +import Type from "typebox"; + +import { DownloadSchema } from "@/lib/schemas/scopes/download.schema.ts"; +import { LibraryRuleSchema } from "@/lib/schemas/scopes/library-rule.schema.ts"; + +export const LibrarySchema = Type.Object({ + "name" : Type.String(), + "downloads": DownloadSchema, + "rules" : Type.Optional(Type.Array(LibraryRuleSchema)), + "extract" : Type.Optional(Type.Object({ + "exclude": Type.Any(), + })), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/logging-config.schema.ts b/src/lib/schemas/scopes/logging-config.schema.ts new file mode 100644 index 0000000..49fe6e4 --- /dev/null +++ b/src/lib/schemas/scopes/logging-config.schema.ts @@ -0,0 +1,8 @@ +import Type from "typebox"; + +export const LoggingConfigSchema = Type.Object({ + "id" : Type.String(), + "sha1": Type.String(), + "size": Type.Number(), + "url" : Type.String(), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/logging.schema.ts b/src/lib/schemas/scopes/logging.schema.ts new file mode 100644 index 0000000..684ff94 --- /dev/null +++ b/src/lib/schemas/scopes/logging.schema.ts @@ -0,0 +1,11 @@ +import Type from "typebox"; + +import { LoggingConfigSchema } from "@/lib/schemas/scopes/logging-config.schema.ts"; + +export const LoggingSchema = Type.Object({ + "client": Type.Object({ + "argument": Type.String(), + "file" : LoggingConfigSchema, + "type" : Type.String(), + }), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/version-manifest.schema.ts b/src/lib/schemas/scopes/version-manifest.schema.ts new file mode 100644 index 0000000..2746f66 --- /dev/null +++ b/src/lib/schemas/scopes/version-manifest.schema.ts @@ -0,0 +1,22 @@ +import Type from "typebox"; + +export const VersionManifestSchema = Type.Object({ + "latest": Type.Object({ + "release" : Type.String(), + "snapshot": Type.String(), + }), + "versions": Type.Array( + Type.Object({ + "id" : Type.String(), + "type": Type.Union([ + Type.Literal("release"), + Type.Literal("snapshot"), + Type.Literal("old-beta"), + Type.Literal("old-alpha"), + ]), + "url" : Type.String({ "format": "url" }), + "time" : Type.String({ "format": "date-time" }), + "releaseTime": Type.String({ "format": "date-time" }), + }), + ), +}); \ No newline at end of file diff --git a/src/lib/schemas/scopes/version-meta-modern.schema.ts b/src/lib/schemas/scopes/version-meta-modern.schema.ts new file mode 100644 index 0000000..56e1d37 --- /dev/null +++ b/src/lib/schemas/scopes/version-meta-modern.schema.ts @@ -0,0 +1,27 @@ +import Type from "typebox"; + +import { ArgumentSchema } from "@/lib/schemas/scopes/argument.schema.ts"; +import { ArtifactSchema } from "@/lib/schemas/scopes/artifact.schema.ts"; +import { AssetIndexSchema } from "@/lib/schemas/scopes/asset-index.schema.ts"; +import { JavaVersionSchema } from "@/lib/schemas/scopes/java-version.schema.ts"; +import { LibrarySchema } from "@/lib/schemas/scopes/library.schema.ts"; +import { LoggingSchema } from "@/lib/schemas/scopes/logging.schema.ts"; + +export const VersionMetaModernSchema = Type.Object({ + "id" : Type.String(), + "type" : Type.String(), + "mainClass": Type.String(), + "arguments": Type.Object({ + "game": Type.Array(ArgumentSchema), + "jvm" : Type.Array(ArgumentSchema), + }), + "libraries": Type.Array(LibrarySchema), + "downloads": Type.Object({ + "client": ArtifactSchema, + "server": Type.Optional(ArtifactSchema), + }), + "assetIndex" : AssetIndexSchema, + "assets" : Type.String(), + "javaVersion": JavaVersionSchema, + "logging" : LoggingSchema, +}); \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 2032fb2..5500a87 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,34 +1,103 @@ -import { RoutesConfiguration } from "@/constants/routes"; -import { ApplicationRootID } from "@/constants/application"; -import { createApp } from "vue"; -import { createRouter } from "@kitbag/router"; -import { initializeLauncher } from "@/lib/main/initialize-launcher.ts"; -import { declareWindow } from "@/lib/main/declare-window.ts"; -import { log } from "@/lib/handlers/log.ts"; -import App from "@/App.vue"; // Import UnoCSS essentials import "virtual:uno.css"; -// Reset all CSS styles in a Tailwind style +// Reset all CSS styles in a Tailwind-like way import "@unocss/reset/tailwind.css"; +// Import custom CSS styles import "@/globals.css"; +// Import styles that are necessary for Material You ripple effect +import "m3ripple-vue/style.css"; -log.debug("Creating a router instance"); -const RouterInstance = createRouter(RoutesConfiguration); +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { createApp } from "vue"; -log.debug("Creating an app instance"); -const AppInstance = createApp(App); +import App from "@/App.vue"; +import { ApplicationNamespace, ApplicationRootID } from "@/constants/application"; +import { getASCIIArt } from "@/constants/ascii-art.ts"; +import Configs from "@/lib/configs"; +import DevelopmentModeHelpers from "@/lib/development-mode-helpers"; +import Errors from "@/lib/errors"; +import General from "@/lib/general"; +import Globals from "@/lib/globals"; +import Logging from "@/lib/logging"; +import { log } from "@/lib/logging/scopes/log.ts"; +import type { ConfigType } from "@/types/application/config.type.ts"; + +// Measure high resolution timestamp before launcher initialization +const startTime = performance.now(); + +// Check if launcher is in a portable mode to share the status between multiple functions +const portable: boolean = await General.checkIsPortable(); +// Get the launcher's base directory to share the directory between multiple functions +const baseDirectory: string = await General.getBaseDirectory(portable); + +// No need to log yet since all logs will go into the previous launch log file +await Logging.prepareLogFile(baseDirectory).catch((error: unknown) => { + log.error("Failed to prepare a log file:", Errors.prettify(error)); +}); -log.debug("Linking router instance with the app"); -AppInstance.use(RouterInstance); +/* + * Now the log file preparation is done (unless something threw an error). + * + * Show a pretty ASCII art with the launcher name :3 + */ +log.info(getASCIIArt(portable)); + +/* + * Previous code doesn't access the 'window[ApplicationNamespace]' object, + * but config reading does. That's why we extend the globals only now + */ +Globals.declareWindow(); +log.info("Extended the global window object with the app namespace"); + +// Get user's launcher configuration +const config: ConfigType = await Configs.getSafe(baseDirectory); + +/* + * Define launcher's initial values at globals to make them accessible from 'App.vue': + * - portable + * - baseDirectory + * - config + * + * This way we can save at least 30 ms of time + */ +window[ApplicationNamespace].__internals.initialConfig = config; +window[ApplicationNamespace].__internals.initialPortable = portable; +window[ApplicationNamespace].__internals.initialBaseDirectory = baseDirectory; + +// Enabling debug mode means that debug-level messages will be logged +if (config.development?.enableDebugMode) { + DevelopmentModeHelpers.enableDebugMode( + DevelopmentModeHelpers.getDefault(), + ); +} + +/* + * Launcher's window is not visible by default + * to prevent white screen flashing while webview has not loaded + */ +if (config.misc.showBeforeInitialization) { + try { + log.debug("Showing webview window before initialization according to user's config"); + await getCurrentWebviewWindow().show(); + } catch (error: unknown) { + log.error("Failed to show the webview window before initialization:", Errors.prettify(error)); + } +} + +// Log user's launcher configuration +log.debug( + "Config contents:", + "\n" + JSON.stringify(config, null, 2), +); + +// Create a Vue instance with the 'App.vue' entry +const AppInstance = createApp(App); -// Attach the app to an element with the 'ApplicationRootID' id log.debug(`Mounting app instance to the DOM element (${ApplicationRootID})`); +// Attach the app to a DOM element with the '#app' id AppInstance.mount(ApplicationRootID); -log.debug("Extending global window object in the app namespace"); -declareWindow(); - log.debug("Initializing launcher"); -await initializeLauncher().catch((error: unknown) => { - log.error("Failed to initialize launcher:", JSON.stringify(error)); +await General.initializeLauncher(config, startTime).catch((error: unknown) => { + log.error("Failed to initialize launcher:", Errors.prettify(error)); }); diff --git a/src/pages/About.vue b/src/pages/About.vue deleted file mode 100644 index 2a3eda6..0000000 --- a/src/pages/About.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/src/pages/Home.vue b/src/pages/Home.vue deleted file mode 100644 index eb48390..0000000 --- a/src/pages/Home.vue +++ /dev/null @@ -1,29 +0,0 @@ - - -// TODO: remove temp styles - - - diff --git a/src/lib/launcher/core/diagnose.ts b/src/resources/README.md similarity index 100% rename from src/lib/launcher/core/diagnose.ts rename to src/resources/README.md diff --git a/src/resources/iochi_mari_cat.webp b/src/resources/iochi_mari_cat.webp new file mode 100644 index 0000000..621b2be Binary files /dev/null and b/src/resources/iochi_mari_cat.webp differ diff --git a/src/types/README.md b/src/types/README.md new file mode 100644 index 0000000..7b5f684 --- /dev/null +++ b/src/types/README.md @@ -0,0 +1,2 @@ +[README for JavaScript-related code](../README.md) + diff --git a/src/types/application/config.type.ts b/src/types/application/config.type.ts new file mode 100644 index 0000000..c5a34c0 --- /dev/null +++ b/src/types/application/config.type.ts @@ -0,0 +1,5 @@ +import type { Static } from "typebox"; + +import type { ConfigSchema } from "@/lib/schemas/scopes/config.schema.ts"; + +export type ConfigType = Static; \ No newline at end of file diff --git a/src/types/application/error-handling.type.ts b/src/types/application/error-handling.type.ts new file mode 100644 index 0000000..a5f2853 --- /dev/null +++ b/src/types/application/error-handling.type.ts @@ -0,0 +1,5 @@ +export type NativeErrorType = { + "name" : string; + "message": string; + "stack" : string; +}; \ No newline at end of file diff --git a/src/types/application/global-states.type.ts b/src/types/application/global-states.type.ts new file mode 100644 index 0000000..ca577a7 --- /dev/null +++ b/src/types/application/global-states.type.ts @@ -0,0 +1,121 @@ +import type { ShallowReactive } from "vue"; + +import type { RouteType } from "@/types/application/route.type.ts"; +import type { TranslationsType } from "@/types/translations/translations.type.ts"; + +export type GlobalStatesFileSystemType = { + "portable": boolean; + "base" : string; + "folders" : { + "logs" : string; + "instances": string; + "resources": string; + }; + "files": { + "config": string; + "log" : string; + }; +}; +export type GlobalStatesLayoutType = { + "custom" : boolean; + "background": { + "url" : string | null; + "key" : string | number | null; + "blur" : number | null; + "color": string | null; + }; + "sidebar": { + "blur" : number | null; + "color" : string | null; + "ripple" : string | null; + "sparkles" : string | null; + "background": string | null; + }; + "atAGlance": { + "title" : string | null; + "subtitle": string | null; + }; +}; +export type GlobalStatesPagesType = { + "current": RouteType; + "states" : { + "home": Partial<{ + // TODO: none / short / detailed + "stats": unknown; + }>; + "library": Partial<{ + // TODO: instance list groups + "group": unknown; + }>; + "settings": Partial<{ + // TODO: general / extensions / etc. + "tab": unknown; + }>; + "add-instance": Partial<{ + // TODO: modrinth / curseforge / etc. + "provider": unknown; + }>; + // Reserved for extensions' needs + "none": Record; + }; +}; +export type GlobalStatesLogsType = { + "show" : boolean; + "lineBreaks" : boolean; + "virtualized": boolean; + "dates" : boolean; + "filtering" : string; +}; +export type GlobalStatesSidebarItemsType = Array<"divider" | { + "path" : RouteType; + "name" : string; + "action": () => void; + "icon" ?: string; + "image"?: string; +}>; +export type GlobalStatesContextMenuItemsType = Array<{ + "icon" : string; + "name" : string; + "action": () => void; +}>; +export type GlobalStatesDevelopmentType = { + "showFPS" : boolean; + "enableDebugMode" : boolean; + "enableNativeReloadKeyBinds": boolean; +}; +export type GlobalStatesMiscType = { + "showBeforeInitialization": boolean; + "enableDiscordRPC" : boolean; +}; +// Global minecraft settings +export type GlobalStatesMinecraftType = { + "windowHeight": number; + "windowWidth" : number; + "jvmArgs" : string; +}; + +export type GlobalStatesType = { + + /* + * Specified in config (only JSON values) + * + * JSON doesn't have 'undefined' value, so we use 'null' instead of it + */ + "development": GlobalStatesDevelopmentType | null; + "layout" : GlobalStatesLayoutType; + "logs" : GlobalStatesLogsType; + "misc" : GlobalStatesMiscType; + "minecraft" : GlobalStatesMinecraftType; + + // Not specified in config (non-JSON values) + "translations" : TranslationsType; + "sidebarItems" : GlobalStatesSidebarItemsType; + "contextMenuItems": GlobalStatesContextMenuItemsType; + "pages" : GlobalStatesPagesType; + "fileSystem" : GlobalStatesFileSystemType | undefined; +}; +export type GlobalStatesChangerType = ( + key : Key, + value: GlobalStatesType[Key], +) => void; +export type ContextGlobalStatesType = ShallowReactive; diff --git a/src/types/application/instance-states.type.ts b/src/types/application/instance-states.type.ts new file mode 100644 index 0000000..2ea2fb4 --- /dev/null +++ b/src/types/application/instance-states.type.ts @@ -0,0 +1,12 @@ +import type { ShallowReactive } from "vue"; + +import type { GlobalStatesType } from "@/types/application/global-states.type.ts"; + +/* Per-instance Minecraft settings */ +export type InstanceStatesType = Record; + +export type InstanceStatesChangerType = ( + key : Key, + value: InstanceStatesType[Key], +) => void; +export type ContextInstanceStatesType = ShallowReactive; \ No newline at end of file diff --git a/src/types/application/log-entry-information.type.ts b/src/types/application/log-entry-information.type.ts new file mode 100644 index 0000000..e5ebd25 --- /dev/null +++ b/src/types/application/log-entry-information.type.ts @@ -0,0 +1,7 @@ +export type LogEntryInformationType = { + "date" : string; + "time" : string; + "target" : string; + "level" : string; + "message": string; +}; \ No newline at end of file diff --git a/src/types/application/log-field-text.type.ts b/src/types/application/log-field-text.type.ts new file mode 100644 index 0000000..a9463ad --- /dev/null +++ b/src/types/application/log-field-text.type.ts @@ -0,0 +1,4 @@ +export type FieldTextType = { + "extractions": Array; + "fields" : Array; +}; \ No newline at end of file diff --git a/src/types/application/log-method.type.ts b/src/types/application/log-method.type.ts new file mode 100644 index 0000000..6af0ebc --- /dev/null +++ b/src/types/application/log-method.type.ts @@ -0,0 +1 @@ +export type LogMethodType = (...input: string[]) => void; diff --git a/src/types/application/route.type.ts b/src/types/application/route.type.ts new file mode 100644 index 0000000..61cba25 --- /dev/null +++ b/src/types/application/route.type.ts @@ -0,0 +1,3 @@ +import { Routes } from "@/constants/routes.ts"; + +export type RouteType = (typeof Routes)[keyof typeof Routes]; \ No newline at end of file diff --git a/src/types/config/config.schema.ts b/src/types/config/config.schema.ts deleted file mode 100644 index 201745c..0000000 --- a/src/types/config/config.schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Type, type Static } from "typebox"; -import { Compile } from "typebox/compile"; - -const ConfigSchema = Type.Object({ - "__do_not_touch_VERSION": Type.Number(), - "customization" : Type.Object({ - "theme": Type.Union([ - Type.Literal("light"), - Type.Literal("dark"), - ]), - "accent": Type.Union([ - Type.Literal("red"), - Type.Literal("orange"), - Type.Literal("rose"), - ]), - "background": Type.String(), - }), - "locale" : Type.String(), - "minecraftWindowHeight": Type.Number(), - "minecraftWindowWidth" : Type.Number(), -}); - -export const ConfigValidator = Compile(ConfigSchema); -export type ConfigType = Static; diff --git a/src/types/extensions/hook-return.type.ts b/src/types/extensions/hook-return.type.ts index 0569c99..6a05220 100644 --- a/src/types/extensions/hook-return.type.ts +++ b/src/types/extensions/hook-return.type.ts @@ -1,10 +1,25 @@ +import type { HookResponseStatus } from "@/constants/hooks.ts"; + +type ExtensionResponseStatusType = typeof HookResponseStatus; + +export type ExtensionStatusType = ExtensionResponseStatusType[keyof ExtensionResponseStatusType]; +export type ExtensionHookResponseType = ResponseType extends "nothing" ? void : { + // 'stop' aborts a function that executed current hook + "status" : ExtensionStatusType; + // If the 'status' is 'stop', function will return this field + "response": ResponseType | Promise; +}; + /** * Used in '/src/declarations.ts'. - * All hooks are promises and are stored in the array to handle multiple hooks + * All hooks are either async or sync, and they are stored in the array to allow multiple hooks */ -export type HookReturnType = Array<() => Promise<{ - // 'stop' aborts a function that executed current hook - "status" : "stop" | "continue"; - // if 'status' is 'stop', function will return this field - "response": T | Promise; -}>>; \ No newline at end of file +export type HookReturnType< + ArgumentsType, + ResponseType, + IsPromise extends ("promise" | "non-promise") = "promise", +> = Array<(...arguments_: ArgumentsType[]) => ( + IsPromise extends "promise" + ? Promise> + : ExtensionHookResponseType +)>; diff --git a/src/types/extensions/permission.type.ts b/src/types/extensions/permission.type.ts new file mode 100644 index 0000000..65b4166 --- /dev/null +++ b/src/types/extensions/permission.type.ts @@ -0,0 +1,8 @@ +import type { Permissions } from "@/constants/permissions.ts"; + +type PermissionsObjectType = typeof Permissions; +type PermissionsKeyType = keyof PermissionsObjectType; + +export type PermissionType = { + [Key in PermissionsKeyType]: PermissionsObjectType[Key][keyof PermissionsObjectType[Key]]; +}[PermissionsKeyType]; \ No newline at end of file diff --git a/src/types/minecraft/minecraft.type.ts b/src/types/minecraft/minecraft.type.ts new file mode 100644 index 0000000..77ce8c1 --- /dev/null +++ b/src/types/minecraft/minecraft.type.ts @@ -0,0 +1,23 @@ +import type { Static } from "typebox"; + +import type { ArgumentSchema } from "@/lib/schemas/scopes/argument.schema.ts"; +import type { ArtifactSchema } from "@/lib/schemas/scopes/artifact.schema.ts"; +import type { AssetSchema } from "@/lib/schemas/scopes/asset.schema.ts"; +import type { AssetIndexSchema } from "@/lib/schemas/scopes/asset-index.schema.ts"; +import type { ClassifiersSchema } from "@/lib/schemas/scopes/classifiers.schema.ts"; +import type { LibrarySchema } from "@/lib/schemas/scopes/library.schema.ts"; +import type { LibraryRuleSchema } from "@/lib/schemas/scopes/library-rule.schema.ts"; +import type { LoggingConfigSchema } from "@/lib/schemas/scopes/logging-config.schema.ts"; +import type { VersionManifestSchema } from "@/lib/schemas/scopes/version-manifest.schema.ts"; +import type { VersionMetaModernSchema } from "@/lib/schemas/scopes/version-meta-modern.schema.ts"; + +export type ArgumentType = Static; +export type ArtifactType = Static; +export type AssetType = Static; +export type AssetIndexType = Static; +export type ClassifiersType = Static; +export type LibraryType = Static; +export type LibraryRuleType = Static; +export type LoggingConfigType = Static; +export type VersionManifestType = Static; +export type VersionMetaModernType = Static; \ No newline at end of file diff --git a/src/types/translations/translations.type.ts b/src/types/translations/translations.type.ts new file mode 100644 index 0000000..909d2ca --- /dev/null +++ b/src/types/translations/translations.type.ts @@ -0,0 +1,3 @@ +import EnglishTranslations from "@/constants/english.json"; + +export type TranslationsType = typeof EnglishTranslations; diff --git a/src/types/ui/at-a-glance.type.ts b/src/types/ui/at-a-glance.type.ts new file mode 100644 index 0000000..2a3e117 --- /dev/null +++ b/src/types/ui/at-a-glance.type.ts @@ -0,0 +1,4 @@ +export type AtAGlanceType = { + "title" : string; + "subtitle": string; +}; diff --git a/src/types/ui/log-button.type.ts b/src/types/ui/log-button.type.ts new file mode 100644 index 0000000..787474a --- /dev/null +++ b/src/types/ui/log-button.type.ts @@ -0,0 +1,15 @@ +export type LogButtonType = { + "icon" ?: string; + "label"?: string; + "ids" : { + "wrapper": string; + "icon" : string; + "label" ?: string; + }; + "tooltip" ?: string; + "onClick" ?: () => void; + "invert" ?: boolean; + "hideOnSm"?: boolean; + "hideOnMd"?: boolean; + "hidden" ?: boolean; +}; diff --git a/src/types/ui/log-controls.type.ts b/src/types/ui/log-controls.type.ts new file mode 100644 index 0000000..927b6e3 --- /dev/null +++ b/src/types/ui/log-controls.type.ts @@ -0,0 +1,20 @@ +export type LogControlsType = { + "logDatesShown" : boolean | undefined; + "searchPosition" : number; + "setSearchPosition" : (position: number, absolutePosition: number | undefined) => void; + "searching" : string; + "searchLogs" : (search: string) => Array; + "filtering" : string; + "filterLogs" : (filter: string) => void; + "scrollToIndex" : (index: number) => void; + "shouldVirtualize" : boolean; + "toggleShouldVirtualize": () => void; + "horizontalScroll" : boolean; + "toggleHorizontalScroll": () => void; + "selectAllLogs" : () => void; + "textIsInSelection" : boolean; + "toggleTextSelection" : () => void; + "textSelectionRange" : [number, number] | undefined; + "setTextSelectionRange" : (range: [number, number] | undefined) => void; + "logsArray" : Array; +}; diff --git a/src/types/utils/deep-non-nullable.type.ts b/src/types/utils/deep-non-nullable.type.ts new file mode 100644 index 0000000..d79fed8 --- /dev/null +++ b/src/types/utils/deep-non-nullable.type.ts @@ -0,0 +1,3 @@ +export type DeepNonNullable = T extends object ? { + [P in keyof T]: DeepNonNullable>; +} : T; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 1e79746..11f02fe 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,3 +1 @@ -/* eslint-disable unicorn/prevent-abbreviations */ -/* See https://vite.dev/guide/env-and-mode.html#intellisense-for-typescript for more */ /// diff --git a/tsconfig.app.json b/tsconfig.app.json index 9866feb..4c3fdf7 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -8,7 +8,8 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + // Default: true. Conflicts with packages that use typescript enums if true + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, diff --git a/tsconfig.node.json b/tsconfig.node.json index f85a399..d3ac1ec 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -17,7 +17,8 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, + // Default: true. Conflicts with packages that use typescript enums if true + "erasableSyntaxOnly": false, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/uno.config.ts b/uno.config.ts index b52ffaa..97b9b3f 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -1,17 +1,28 @@ import { defineConfig, presetIcons, presetWind3 } from "unocss"; export default defineConfig({ + "theme": { + "breakpoints": { + // All breakpoint sizes were taken from the tailwind website + "xs" : "480px", + "sm" : "640px", + "md" : "768px", + "lg" : "1024px", + "xl" : "1280px", + "2xl": "1536px", + }, + }, "presets": [ /* - * 'presetWind4' requires Chrome 111+ (March 9, 2023) + * 'presetWind4' requires Chrome 111+ (March 9, 2023). * Otherwise colors won't work, so not worth it */ presetWind3({ // Apply dark theme only if there is a 'dark' class on parent elements "dark": "class", }), - // Lucide icons available by class names (@iconify-json/lucide) + // Lucide icons that are available by class names (@iconify-json/lucide) presetIcons(), ], }); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index c895a25..cf7ceea 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,10 @@ -// 'vitest/config' extends 'vite' config -import { defineConfig } from "vitest/config"; +import path from "node:path"; + import vue from "@vitejs/plugin-vue"; import unocss from "unocss/vite"; import eslint from "vite-plugin-eslint2"; -import path from "node:path"; +// 'vitest/config' extends 'vite' config +import { defineConfig } from "vitest/config"; export default defineConfig({ // Better support for Tauri CLI output @@ -20,6 +21,7 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + // Tests-related code "test": { "setupFiles": "vitest.setup.ts", }, diff --git a/vitest.setup.ts b/vitest.setup.ts index 9d61b64..2c063d8 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,17 +1,100 @@ import { vi } from "vitest"; +import type { KaedeNamespaceType } from "./src/declarations"; + +// Overwrite 'window' object for tests only vi.stubGlobal("window", { "__KAEDE__": { + "__internals": { + "getGlobalStates" : (): void => {}, + "changeGlobalStates" : (): void => {}, + "getInstanceStates" : (): void => {}, + "changeInstanceStates": (): void => {}, + "requestPermissions" : async (): Promise> => ([]), + "initialConfig" : {}, + }, + "variables": { + "rippleColor" : "", + "sparklesColorRGB": "255 255 255", + }, "hooks": { "getConfigFile": { - "before": async () => {}, + "before": [], + "after" : [], }, "getDefaultConfig": { - "before": async () => {}, + "before": [], + }, + "onPagesChange": { + "before": [], + "after" : [], + }, + "onLayoutChange": { + "before": [], + "after" : [], + }, + "onLogsChange": { + "before": [], + "after" : [], + }, + "onSidebarItemsChange": { + "before": [], + "after" : [], + }, + "onContextMenuItemsChange": { + "before": [], + "after" : [], + }, + "onTranslationsChange": { + "before": [], + "after" : [], + }, + "onFileSystemChange": { + "before": [], + "after" : [], + }, + "onDevelopmentChange": { + "before": [], + "after" : [], + }, + "onMiscChange": { + "before": [], + "after" : [], + }, + "onMinecraftChange": { + "before": [], + "after" : [], + }, + "onInstanceChange": { + "before": [], + "after" : [], }, }, }, +} satisfies { + + /* + * Kaede itself uses only "__internals", "variables", and "hooks" properties. + * The rest is for the extensions + */ + "__KAEDE__": Pick< + KaedeNamespaceType, + "__internals" | "variables" | "hooks" + >; }); -vi.mock("@/lib/handlers/log.ts", async () => { + +// Mock the logging utility +vi.mock("@/lib/logging/scopes/log.ts", async () => { return await vi.importActual("@/__mocks__/log.cjs"); -}); \ No newline at end of file +}); + +// Mock Tauri APIs +vi.mock("@tauri-apps/api/window", async () => { + const mockedWindow: typeof window.__TAURI__.window = { + "getCurrentWindow": () => ({ + "theme": async () => "dark", + }), + } as typeof window.__TAURI__.window; + + return mockedWindow; +});