diff --git a/.env b/.env new file mode 100644 index 000000000..f63e6164a --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +ATPROTO_CLIENT_ID=https://cimd-service.fly.dev/clients/bafyreiafqw2fao73uzdvg7rzvvcq7d4z5fvjhqc5qcqo6yzqq3p5dh6j2y +ROCKSKY_ATPROTO_CLIENT_ID=https://cimd-service.fly.dev/clients/bafyreihmrs2hsh5fp535lbidz4yt3f63pyh42d6gwpchf3vrr2kxie3cpu +# DISABLE_AUTOMATIC_TRACKS_PROCESSING=t diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index 21b3b9a9e..000000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,44 +0,0 @@ -env: - browser: true - es6: true - worker: true - -extends: - - eslint:recommended - - plugin:@typescript-eslint/recommended - -parser: - "@typescript-eslint/parser" - -parserOptions: - ecmaVersion: 2020 - sourceType: module - -plugins: - - "@typescript-eslint" - -root: - true - -globals: - Elm: readonly - MediaMetadata: readonly - RemoteStorage: readonly - tocca: readonly - -rules: - "@typescript-eslint/ban-ts-comment": - 0 - - no-cond-assign: - 0 - - no-unexpected-multiline: - 0 - - "@typescript-eslint/no-explicit-any": - 0 - - "@typescript-eslint/no-unused-vars": - - error - - argsIgnorePattern: "^_" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8107789b9..538b3589e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,2 @@ github: icidasset ko_fi: icidasset -patreon: diffuse diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 05aef6e72..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Build & create release - -on: - workflow_dispatch: - inputs: - -jobs: - ######### - # BUILD # - ######### - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - - # Tasks - - run: npm install - - run: npx just build-prod - - # Upload artifacts - - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ - - ################## - # CREATE RELEASE # - ################## - create-release: - needs: build - runs-on: ubuntu-latest - - outputs: - RELEASE_UPLOAD_ID: ${{ steps.create_release.outputs.id }} - TAG_NAME: ${{ steps.package-version.outputs.current-version }} - - steps: - - uses: actions/checkout@v4 - - run: mkdir compressed - - # Download artifacts - - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - # Create zip - - uses: montudor/action-zip@v1 - with: - args: zip -qq -r ./compressed/diffuse-web.zip ./dist - - # Create tar.gz - - uses: master-atul/tar-action@v1.1.0 - with: - command: c - cwd: . - files: ./dist - outPath: compressed/diffuse-web.tar.gz - - # Get Diffuse's version number - - id: package-version - uses: martinbeentjes/npm-get-version-action@v1.3.1 - - # Create release - - uses: softprops/action-gh-release@v1 - id: create_release - with: - token: ${{ secrets.GITHUB_TOKEN }} - - tag_name: "${{ steps.package-version.outputs.current-version }}" - name: "v${{ steps.package-version.outputs.current-version }}" - body: "See the assets to download this version and install." - draft: true - - files: | - compressed/diffuse-web.zip - compressed/diffuse-web.tar.gz - - ######### - # TAURI # - ######### - tauri: - needs: create-release - - strategy: - fail-fast: false - matrix: - os: [macos-15, macos-13, ubuntu-latest, windows-latest] - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - - # Install dependencies - - run: npm install - - # OS - - name: install linux dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y libgtk-4-1 libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0 - - # Rust - - uses: moonrepo/setup-rust@v1 - - # Download artifacts - - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - # Make a build - - run: npx tauri build - - # Upload artifacts - - uses: softprops/action-gh-release@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - tag_name: "${{ needs.create-release.outputs.TAG_NAME }}" - draft: true - - files: | - src-tauri/target/release/bundle/appimage/*.AppImage - src-tauri/target/release/bundle/deb/*.deb - src-tauri/target/release/bundle/dmg/*.dmg - src-tauri/target/release/bundle/msi/*.msi - src-tauri/target/release/bundle/nsis/*.exe - src-tauri/target/release/bundle/rpm/*.rpm diff --git a/.gitignore b/.gitignore index 7fc92681c..230a81c62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,15 @@ .DS_Store -.gren -.zed -app -fission.yaml* -elm-stuff +AGENTS.md node_modules -/.deploy* +skills-lock.json + +/.claude +/.vit +/_site +/_vendor /dist -/src/Library/Css/Classes.elm +/src/definitions/types/ +/vendor +/cov +/cov.lcov +/cov_html diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 000000000..be27dd7ed --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,41 @@ +{ + "lsp": { + "deno": { + "settings": { + "deno": { + "enable": true, + }, + }, + }, + "json-language-server": { + "settings": { + "json": { + "schemas": [ + { + "fileMatch": ["deno.json", "deno.jsonc"], + "url": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json", + }, + { + "fileMatch": ["package.json"], + "url": "http://json.schemastore.org/package", + }, + ], + }, + }, + }, + }, + "languages": { + "JavaScript": { + "language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"], + "formatter": "language_server", + }, + "TypeScript": { + "language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"], + "formatter": "language_server", + }, + "TSX": { + "language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"], + "formatter": "language_server", + }, + }, +} diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 000000000..e96163364 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,7 @@ +[ + { + "label": "deno test", + "command": "deno test -A --filter '/^$ZED_CUSTOM_DENO_TEST_NAME$/' '$ZED_FILE'", + "tags": ["js-test"] + } +] diff --git a/CHANGELOG.md b/CHANGELOG.md index 87fe35c6c..b28332e36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 4.0.0 + +**Rewrite from scratch.** + + ## 3.5.0 - **Improve audio playback, processing and error handling**. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..247c55abb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Welcome 👋 + +Thanks for considering to contribute! To get started, take a look at the following items: + +- **`README.md`**: Also in the root of this project, along side this file, for some general context and build instructions. +- **Explainer web page**: Important context, explains all non-technical aspects. You can either check out the [last deployed version](https://elements.diffuse.sh/latest/), or build the version located in this repo (`deno run build` → `dist/index.html`, or alternatively: `deno run serve` and open `http://localhost:3000`) +- **Architecture docs**: Another important piece, talks about code and file/directory organisation. Located at `docs/ARCHITECTURE.md`. diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 000000000..c73169e83 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,7 @@ +:8080 { + root * dist + file_server { + precompressed br gzip + } + try_files {path} {path}/index.html /index.html +} diff --git a/Justfile b/Justfile deleted file mode 100644 index 86bb684f0..000000000 --- a/Justfile +++ /dev/null @@ -1,237 +0,0 @@ -export NODE_NO_WARNINGS := "1" - - -BUILD_DIR := "./dist" -NPM_DIR := "./node_modules" -SRC_DIR := "./src" -SYSTEM_DIR := "./system" - -ESBUILD := "node system/Js/esbuild.mjs" -ELM_REVIEW := NPM_DIR + "/.bin/elm-review " + SRC_DIR + " --config system/Review --compiler " + NPM_DIR + "/.bin/elm --elm-format-path " + NPM_DIR + "/.bin/elm-format" - - -default: dev - - -# Tasks -# ===== - -@build: clean css elm copy-wasm js system license - echo "> Build completed ⚡" - - -@build-prod: quality clean (css "minify") elm-prod copy-wasm js-prod system license - echo "> Production build completed 🛳" - - -check-versions: - #!/usr/bin/env node - console.log("> Checking version numbers 🧮") - const pwd = "{{invocation_directory()}}" - - const package = require(`${pwd}/package.json`) - const manifest = require(`${pwd}/src/Static/Manifests/manifest.json`) - - if (package.version !== manifest.version) { - console.error(`The version from package.json doesn't match the one from the app manifest. The package version is '${package.version}' and the manifest version is '${manifest.version}'.`) - process.exit(1) - } - - -@clean: - echo "> Cleaning build directory" - rm -rf {{BUILD_DIR}} || true - mkdir -p {{BUILD_DIR}} - - -@copy-wasm: - echo "> Copying WASM files" - mkdir -p {{BUILD_DIR}}/wasm - cp {{NPM_DIR}}/mediainfo.js/dist/MediaInfoModule.wasm {{BUILD_DIR}}/wasm/media-info.wasm - - -@css minify="false": - echo "{{ if minify == "minify" { "> Compiling CSS (optimised)" } else { "> Compiling CSS" } }}" - - {{NPM_DIR}}/.bin/tailwind \ - --input {{SRC_DIR}}/Css/About.css \ - --output {{BUILD_DIR}}/about.css \ - --content {{SRC_DIR}}/Static/About/**/*.* \ - --config {{SYSTEM_DIR}}/Css/Tailwind.js \ - --postcss {{SYSTEM_DIR}}/Css/PostCSS.js \ - --jit \ - {{ if minify == "minify" { "--minify" } else { "" } }} - - {{NPM_DIR}}/.bin/tailwind \ - --input {{SRC_DIR}}/Css/Application.css \ - --output {{BUILD_DIR}}/application.css \ - --content "{{SRC_DIR}}/Static/Html/**/*.*,{{SRC_DIR}}/Core/Themes/**/*.elm,{{SRC_DIR}}/Core/UI/**/*.elm,{{SRC_DIR}}/Core/UI.elm,{{SRC_DIR}}/Library/**/*.elm,{{SRC_DIR}}/Javascript/**/*.ts" \ - --config {{SYSTEM_DIR}}/Css/Tailwind.js \ - --postcss {{SYSTEM_DIR}}/Css/PostCSS.js \ - --jit \ - {{ if minify == "minify" { "--minify" } else { "" } }} - - -@elm: - echo "> Compiling Elm application" - {{NPM_DIR}}/.bin/elm make {{SRC_DIR}}/Core/Brain.elm --output {{BUILD_DIR}}/js/brain.elm.js - {{NPM_DIR}}/.bin/elm make {{SRC_DIR}}/Core/UI.elm --output {{BUILD_DIR}}/js/ui.elm.js - - -@elm-prod: - echo "> Compiling Elm application (optimised)" - {{NPM_DIR}}/.bin/elm make {{SRC_DIR}}/Core/Brain.elm --output {{BUILD_DIR}}/js/brain.elm.js --optimize - {{NPM_DIR}}/.bin/elm make {{SRC_DIR}}/Core/UI.elm --output {{BUILD_DIR}}/js/ui.elm.js --optimize - - {{NPM_DIR}}/.bin/esbuild {{BUILD_DIR}}/js/brain.elm.js \ - --minify --outfile={{BUILD_DIR}}/js/brain.elm.tmp.js - - {{NPM_DIR}}/.bin/esbuild {{BUILD_DIR}}/js/ui.elm.js \ - --minify --outfile={{BUILD_DIR}}/js/ui.elm.tmp.js - - rm {{BUILD_DIR}}/js/brain.elm.js - mv {{BUILD_DIR}}/js/brain.elm.tmp.js {{BUILD_DIR}}/js/brain.elm.js - rm {{BUILD_DIR}}/js/ui.elm.js - mv {{BUILD_DIR}}/js/ui.elm.tmp.js {{BUILD_DIR}}/js/ui.elm.js - - -js: - #!/usr/bin/env bash - build_timestamp="`date '+%s'`" - echo "> Compiling Javascript code" - - # Workers - {{ESBUILD}} ./src/Javascript/Workers/search.ts \ - --outfile={{BUILD_DIR}}/search.js - - {{ESBUILD}} ./src/Javascript/Workers/service.ts \ - --outfile={{BUILD_DIR}}/service-worker.js \ - --define:BUILD_TIMESTAMP=$build_timestamp - - {{ESBUILD}} ./src/Javascript/Brain/index.ts \ - --outdir={{BUILD_DIR}}/js/brain/ \ - --splitting \ - --alias:brain.elm.js={{BUILD_DIR}}/js/brain.elm.js \ - --inject:./system/Js/node-shims.js \ - --alias:node:buffer=buffer/ \ - --alias:node:stream=stream - - # Main - {{ESBUILD}} ./src/Javascript/UI/index.ts \ - --outdir={{BUILD_DIR}}/js/ui/ \ - --define:BUILD_TIMESTAMP=$build_timestamp \ - --splitting \ - --alias:node:buffer=buffer/ \ - --alias:node:stream=stream - - -js-prod: - #!/usr/bin/env bash - build_timestamp="`date '+%s'`" - echo "> Compiling Javascript code (optimised)" - - # Workers - {{ESBUILD}} ./src/Javascript/Workers/search.ts \ - --minify \ - --outfile={{BUILD_DIR}}/search.js - - {{ESBUILD}} ./src/Javascript/Workers/service.ts \ - --minify \ - --outfile={{BUILD_DIR}}/service-worker.js \ - --define:BUILD_TIMESTAMP=$build_timestamp - - {{ESBUILD}} ./src/Javascript/Brain/index.ts \ - --outdir={{BUILD_DIR}}/js/brain/ \ - --splitting \ - --minify \ - --alias:brain.elm.js={{BUILD_DIR}}/js/brain.elm.js \ - --inject:./system/Js/node-shims.js \ - --alias:node:buffer=buffer/ \ - --alias:node:stream=stream - - # Main - {{ESBUILD}} ./src/Javascript/UI/index.ts \ - --outdir={{BUILD_DIR}}/js/ui/ \ - --define:BUILD_TIMESTAMP=$build_timestamp \ - --splitting \ - --minify \ - --alias:node:buffer=buffer/ \ - --alias:node:stream=stream - - -@license: - echo "> Copying license file" - cp LICENSE {{BUILD_DIR}}/LICENSE - - -@system: - echo "> Compiling system" - {{NPM_DIR}}/.bin/gren make system/Build/Build.gren - node app - rm app - - - -# -# Dev tasks -# - -@dev: build - just watch & just server - - -@doc-tests: - echo "> Running documentation tests" - ( cd src && \ - find . -name "*.elm" -print0 | \ - xargs -0 -n 1 -I % sh -c 'elm-proofread -- % || exit 255; echo "\n\n"' \ - ) - - -@elm-format: - echo "> Running elm-format" - {{NPM_DIR}}/.bin/elm-format {{SRC_DIR}} --yes - - -@elm-housekeeping: elm-format elm-review - - -@elm-review: - echo "> Running elm-review" - {{ELM_REVIEW}} --fix-all - - -@quality: check-versions - echo "> Running es-lint" - {{NPM_DIR}}/.bin/eslint src/Javascript/**/* - echo "> Running elm-review" - {{ELM_REVIEW}} - - -@server: - echo "> Booting up web server on port 8000" - {{NPM_DIR}}/.bin/serve {{BUILD_DIR}} -p 8000 --no-request-logging - - -@test: doc-tests - - -@watch: - echo "> Watching" - just watch-css & just watch-elm & just watch-js & just watch-system - - -@watch-css: - watchexec -p -w {{SRC_DIR}}/Css -w {{SYSTEM_DIR}}/Css -- just css js - - -@watch-elm: - watchexec -p -w {{SRC_DIR}} -e elm -- just elm js css - - -@watch-js: - watchexec -p -w {{SRC_DIR}} -e js,ts -- just js - - -@watch-system: - watchexec -p --ignore *.elm --ignore *.js --ignore *.ts --ignore *.css --ignore src-tauri/** -- just system js diff --git a/README.md b/README.md index 3871550cd..8bbe3053a 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,49 @@ -Diffuse +# Diffuse -_A music player that connects to your cloud/distributed storage, -in the form of a static, serverless, web application._ +_Diffuse is a few things:_ -📍 Available at [diffuse.sh](https://diffuse.sh/) and for [download](https://github.com/icidasset/diffuse/releases). +- A collection of custom DOM elements (aka. framework-agnostic web components) that allow you to build an audio player, do audio metadata processing and browsing, list audio files and streams from various sources/APIs/servers, define how to save or sync user-data, etc. +- A HTML loader that loads "interfaces" from the local user-data cache, the default set from Diffuse, HTTP(S) URLs, or [AT Protocol](https://atproto.com) URIs. When you load an interface, it's supplied with optional "features" which are also pieces of HTML. These pieces are called "facets" and are loaded from the local user-data cache as well. +- A default configuration of the custom elements that's called "the foundation". This is used throughout the default set of facets. This foundation is configured so that the facets can communicate across tabs/frames; this is done by setting the element's `group` attribute. +- A collection of data schemas in the form of atproto [lexicons](https://atproto.com/specs/lexicon). These live in the `lexicons` directory. -
- +Putting all of this together to make cooperative and [malleable](https://www.inkandswitch.com/essay/malleable-software/) software. More information on the [website](https://elements.diffuse.sh/). +## Developer usage -### Integrations +You can either consume the Diffuse library via the [deployed instance](https://elements.diffuse.sh/elements/) (the listed elements link to Javascript files) or the [Javascript package](https://jsr.io/@toko/diffuse). From there you can use the custom elements as with any other custom DOM element, by writing HTML or creating a `Class` instance. -Music layer for music storage. -User layer for user-data storage. - -#### Music layer - -- [Amazon S3](https://aws.amazon.com/s3/) -- [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/) -- [Azure File Storage](https://azure.microsoft.com/en-us/services/storage/files/) -- [Dropbox](https://dropbox.com/) -- [IPFS](https://ipfs.io/) -- [WebDAV](https://en.wikipedia.org/wiki/WebDAV) - -#### User layer - -- [Dropbox](https://www.dropbox.com/) -- [IPFS](https://ipfs.io/) (using MFS) -- [RemoteStorage](https://remotestorage.io/) - - - ---- - - - -### Hosting on your own server - -Diffuse is a static web application, which means it's just HTML, CSS, and Javascript. No REST API, database, or anything backend-related involved. The app uses a hash (aka. fragment-based) routing system, so you don't need any special server rules for routing. You can download a pre-build web-only version of Diffuse on the [releases](https://github.com/icidasset/diffuse/releases) page. Diffuse uses service workers, so you may need HTTPS for it to work smoothly in certain browsers. - -I should also note that some source services use OAuth, so you'll need to use your own application credentials (eg. Dropbox). That said, if you're working locally, you can use `http://localhost:8000` or `http://127.0.0.1:44999` to use the default ones, that's what the old Electron app was using. - -In short: -- Diffuse is a static, serverless web application -- Routing is done using hashes/fragments (eg. `diffuse.sh/#/sources`) -- Download a web build on the [releases](https://github.com/icidasset/diffuse/releases) page -- Uses service workers (use HTTPS if possible) -- May need own OAuth application credentials for some source services +```html + + +``` +```js +import QueueEngine from "@toko/diffuse/components/engine/queue/element.js" ---- +const queue = new QueueEngine() +queue.setAttribute("group", "facets") +document.body.append(queue) +```` -### Building it yourself +## Build it yourself -This project can be built with [Node.js](https://nodejs.org/). +Install [Deno](https://docs.deno.com/runtime/getting_started/installation/). ```shell -# 🍱 - -# 1. Install dependencies -npm install - -# 2. Build -npx just build - -# 3. Start static-file server -npx just server - -# 4. Watch for changes (requires [watchexec](https://github.com/watchexec/watchexec/) to be installed) -npx just watch - -# Alternatively, to build, serve & watch: -npx just +deno run gen:defs:types +deno run build # or deno run serve ``` + +Diffuse is built with: +- [Deno](https://deno.com) +- Web components (custom elements) +- Web workers (also: shared + service workers) +- Signals (currently [alien-signals](https://github.com/stackblitz/alien-signals), but hopefully [TC39](https://github.com/tc39/proposal-signals) in the future) +- [`lit-html`](https://lit.dev/docs/libraries/standalone-templates/) +- [`music-metadata`](https://github.com/Borewit/music-metadata) +- [Lume](https://lume.land) & [ESBuild](https://esbuild.github.io) diff --git a/_config.ts b/_config.ts new file mode 100644 index 000000000..b4fe98384 --- /dev/null +++ b/_config.ts @@ -0,0 +1,487 @@ +import { dotenvRun } from "@dotenv-run/esbuild"; +import lume from "lume/mod.ts"; + +import brotli from "lume/plugins/brotli.ts"; +import esbuild from "lume/plugins/esbuild.ts"; +import postcss from "lume/plugins/postcss.ts"; +import sourceMaps from "lume/plugins/source_maps.ts"; + +import * as path from "@std/path"; +import { ensureDirSync } from "@std/fs/ensure-dir"; +import { walkSync } from "@std/fs/walk"; +import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill"; +import { wasmLoader } from "esbuild-plugin-wasm"; +import autoprefixer from "autoprefixer"; +import cssnano from "cssnano"; + +import { Uint8ArrayReader, Uint8ArrayWriter, ZipWriter } from "@zip-js/zip-js"; + +import { create as createCID } from "~/common/cid.js"; +import { generateManifest } from "./tasks/cem.js"; + +const site = lume({ + dest: "./dist", + src: "./src", + server: { + debugBar: false, + middlewares: [], + }, +}); + +export default site; + +//////////////////////////////////////////// +// JS +//////////////////////////////////////////// + +site.use(esbuild({ + extensions: [".js"], + options: { + alias: { + "@automerge/automerge": "https://esm.sh/@automerge/automerge@^3.2.3", + }, + bundle: true, + format: "esm", + minify: true, + external: ["@awesome.me/webawesome/*"], + platform: "browser", + plugins: [ + // @ts-ignore + dotenvRun({ + files: [".env"], + }), + // Force @atcute/uint8array to use the browser entry (dist/index.js) + // instead of the Node entry (dist/index.node.js) which imports from + // node:crypto. The @deno/loader Workspace defaults to platform "node", + // causing the "node" export condition to match before "default". + { + name: "atcute-uint8array-browser", + setup(build) { + build.onLoad( + { filter: /@atcute\+uint8array.*index\.node\.js$/ }, + async (args) => { + const browserPath = args.path.replace( + "index.node.js", + "index.js", + ); + const contents = await Deno.readTextFile(browserPath); + return { contents, loader: "js" }; + }, + ); + }, + }, + { + name: "atcute-tid-browser", + setup(build) { + build.onLoad( + { filter: /@atcute\+tid.*random-node\.js$/ }, + async (args) => { + const browserPath = args.path.replace( + "random-node.js", + "random-web.js", + ); + const contents = await Deno.readTextFile(browserPath); + return { contents, loader: "js" }; + }, + ); + }, + }, + { + name: "atcute-multibase-browser", + setup(build) { + build.onLoad( + { filter: /@atcute[+/]multibase.*-node\.js$/ }, + async (args) => { + const browserPath = args.path.replace( + "-node.js", + "-web.js", + ); + const contents = await Deno.readTextFile(browserPath); + return { contents, loader: "js" }; + }, + ); + }, + }, + // nanoid ships a browser entry (index.browser.js) but esbuild resolves + // the default condition (index.js) which uses Buffer.allocUnsafe. + { + name: "nanoid-browser", + setup(build) { + build.onLoad( + { filter: /nanoid\/index\.js$/ }, + async (args) => { + const browserPath = args.path.replace( + "index.js", + "index.browser.js", + ); + const contents = await Deno.readTextFile(browserPath); + return { contents, loader: "js" }; + }, + ); + }, + }, + nodeModulesPolyfillPlugin({ + fallback: "empty", + modules: [], + }), + wasmLoader(), + ], + splitting: true, + target: "esnext", + }, +})); + +site.add([".js"]); + +// *.inline.js files are inlined into their companion HTML at build/serve time. +// Exclude them from the regular build so esbuild doesn't try to bundle them. +site.ignore((p) => p.endsWith(".inline.js") || p.endsWith("SKILL.md")); + +//////////////////////////////////////////// +// CSS +//////////////////////////////////////////// + +site.use(postcss({ + plugins: [ + autoprefixer(), + cssnano({ + preset: "default", + }), + ], +})); + +site.add([".css"]); + +site.remoteFile( + "vendor/98.css", + import.meta.resolve("./node_modules/98.css/dist/98.css"), +); + +//////////////////////////////////////////// +// BINARY ASSETS +//////////////////////////////////////////// + +site.add("/favicons", "/"); +site.add("/fonts"); +site.add("/images"); +site.add("/testing"); +site.add([".woff2"]); + +site.remoteFile( + "vendor/ms_sans_serif.woff2", + import.meta.resolve( + "./node_modules/98.css/fonts/converted/ms_sans_serif.woff2", + ), +); + +site.remoteFile( + "vendor/ms_sans_serif_bold.woff2", + import.meta.resolve( + "./node_modules/98.css/fonts/converted/ms_sans_serif_bold.woff2", + ), +); + +site.remoteFile( + "fonts/98.css/ms_sans_serif.woff2", + import.meta.resolve( + "./node_modules/98.css/fonts/converted/ms_sans_serif.woff2", + ), +); + +site.remoteFile( + "fonts/98.css/ms_sans_serif_bold.woff2", + import.meta.resolve( + "./node_modules/98.css/fonts/converted/ms_sans_serif_bold.woff2", + ), +); + +//////////////////////////////////////////// +// DEFINITIONS +//////////////////////////////////////////// + +site.add("/definitions"); + +// HELPERS + +site.filter("facetURI", (text) => { + if (text.includes("://")) { + return text; + } else { + return `diffuse://${text}`; + } +}); + +site.filter("facetLoaderURL", (text) => { + let key = "path"; + + if (text.includes("://")) { + key = "uri"; + } + + return `l/?${key}=${encodeURIComponent(text)}`; +}); + +//////////////////////////////////////////// +// PHOSPHOR ICONS +//////////////////////////////////////////// + +function phosphor(path: string) { + site.remoteFile( + `vendor/@phosphor-icons/web/${path}`, + import.meta.resolve(`./node_modules/@phosphor-icons/web/src/${path}`), + ); + + site.add(`vendor/@phosphor-icons/web/${path}`); +} + +["bold", "duotone", "fill", "light", "regular", "light"].forEach((v) => { + const f = v === "regular" ? "" : `-${v[0].toUpperCase()}${v.slice(1)}`; + phosphor(`${v}/selection.json`); + phosphor(`${v}/style.css`); + phosphor(`${v}/Phosphor${f}.svg`); + phosphor(`${v}/Phosphor${f}.ttf`); + phosphor(`${v}/Phosphor${f}.woff`); + phosphor(`${v}/Phosphor${f}.woff2`); +}); + +//////////////////////////////////////////// +// WEB AWESOME +//////////////////////////////////////////// + +for ( + const f of walkSync("./node_modules/@awesome.me/webawesome/dist-cdn/", { + includeDirs: false, + }) +) { + const relativePath = f.path.replace( + /^node_modules\/@awesome\.me\/webawesome\/dist-cdn\//, + "", + ); + + const destPath = `vendor/@awesome.me/webawesome/${relativePath}`; + + site.remoteFile( + destPath, + import.meta.resolve( + `./node_modules/@awesome.me/webawesome/dist-cdn/${relativePath}`, + ), + ); + + site.copy(destPath); +} + +//////////////////////////////////////////// +// MISC +//////////////////////////////////////////// + +site.add([".html"]); +site.add([".json"]); +site.add([".webmanifest"]); + +site.script("copy-type-defs", () => { + for ( + const f of walkSync( + "./src/", + { includeDirs: false, exts: [".d.ts"] }, + ) + ) { + const dest = "dist/" + f.path.replace(/^src\//, ""); + const dir = path.dirname(dest); + ensureDirSync(dir); + Deno.copyFileSync(f.path, dest); + } +}); + +// SKILLS + +site.remoteFile( + "skills/diffuse-facet/docs/architecture.txt", + import.meta.resolve("./docs/ARCHITECTURE.md"), +); + +site.remoteFile( + "skills/diffuse-facet/docs/foundation.js", + import.meta.resolve("./src/common/foundation.js"), +); + +site.remoteFile( + "skills/diffuse-facet/example/index.html", + import.meta.resolve( + "./src/facets/themes/blur/artwork-controller/facet/index.html", + ), +); + +site.add("skills/diffuse-facet/docs/architecture.txt"); +site.add("skills/diffuse-facet/docs/foundation.js"); +site.add("skills/diffuse-facet/example/index.html"); +site.add("/definitions", "/skills/diffuse-facet/docs/definitions"); +site.copy("skills/diffuse-facet/SKILL.md"); +site.add("skills"); + +site.addEventListener("afterBuild", () => { + const destDirs = [ + "dist/definitions", + "dist/skills/diffuse-facet/docs/definitions", + ]; + for (const f of walkSync("./lexicons/", { includeDirs: false })) { + const rel = path.relative("./lexicons", f.path); + for (const destDir of destDirs) { + const dest = path.join(destDir, rel); + ensureDirSync(path.dirname(dest)); + Deno.copyFileSync(f.path, dest); + } + } +}); + +site.addEventListener("afterBuild", async () => { + const skillsDir = "dist/skills/diffuse-facet"; + const zipWriter = new ZipWriter(new Uint8ArrayWriter()); + + for (const entry of walkSync(skillsDir, { includeDirs: false })) { + if (entry.path.endsWith(".br")) continue; + await zipWriter.add( + "diffuse-facet/" + entry.path.slice(skillsDir.length + 1), + new Uint8ArrayReader(Deno.readFileSync(entry.path)), + ); + } + + Deno.writeFileSync("dist/skills/diffuse-facet.zip", await zipWriter.close()); +}); + +//////////////////////////////////////////// +// CUSTOM ELEMENTS MANIFEST +//////////////////////////////////////////// + +// Generate `dist/custom-elements.json` (+ brotli sidecar) as a build artifact. +// Runs in `afterBuild`, before `writeFileTree` below, so the manifest is +// included in the service-worker file tree and available offline. +site.addEventListener( + "afterBuild", + () => generateManifest(import.meta.dirname!), +); + +//////////////////////////////////////////// +// FILE TREE +//////////////////////////////////////////// + +async function buildFileTree( + dir: string, + prefix = "", +): Promise> { + const RAW = 0x55; + const tree: Record = {}; + + for (const entry of Deno.readDirSync(dir)) { + const entryPath = path.join(dir, entry.name); + const entryKey = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory) { + Object.assign(tree, await buildFileTree(entryPath, entryKey)); + } else { + const data = Deno.readFileSync(entryPath); + tree[entryKey] = await createCID(RAW, data); + } + } + + return tree; +} + +async function writeFileTree() { + const swDist = "./dist/service-worker.js"; + + // Remove stale brotli'd SW so Caddy never serves the old compressed version + // while the plain file has been updated. + try { + Deno.removeSync(`${swDist}.br`); + } catch { /* already gone */ } + + const tree = await buildFileTree("dist/"); + + // The SW is not included in its own embedded tree — the browser manages the + // SW lifecycle independently and never fetches it through the SW's handler. + delete tree["service-worker.js"]; + delete tree["service-worker.js.br"]; + + // Remove any .br sidecar that is older than its plain counterpart — this + // catches cases where brotli compression did not re-run after an asset + // changed (e.g. incremental afterUpdate builds), so the server falls back + // to the fresh plain file instead of serving stale compressed bytes. + for (const key of Object.keys(tree)) { + if (key.endsWith(".br")) continue; + const plainPath = `./dist/${key}`; + const brPath = `${plainPath}.br`; + try { + const plainMtime = Deno.statSync(plainPath).mtime; + const brMtime = Deno.statSync(brPath).mtime; + if (plainMtime && brMtime && brMtime < plainMtime) { + Deno.removeSync(brPath); + delete tree[`${key}.br`]; + } + } catch { /* .br sidecar doesn't exist — nothing to do */ } + } + + const sorted = Object.fromEntries( + Object.keys(tree).sort().map((k) => [k, tree[k]]), + ); + + // Inject the file tree into the compiled SW and stamp with a build ID so + // the browser always detects an update on each build. + // + // The placeholder "__FILE_TREE__" may survive as-is or may be folded by + // esbuild into `JSON.parse("__FILE_TREE__")` → `"__FILE_TREE__"`. Both + // patterns are replaced with the actual object literal so that `FILE_TREE` + // is always an object in the running SW, never a string. + const treeJson = JSON.stringify(sorted); + const buildId = crypto.randomUUID(); + const swSource = Deno.readTextFileSync(swDist) + .replace(/\n\/\/ @build \S+\n$/, "") + .replace('JSON.parse("__FILE_TREE__")', () => treeJson) + .replace('"__FILE_TREE__"', () => treeJson) + .replace('"__BUILD_ID__"', () => JSON.stringify(buildId)); + const swStamped = `${swSource}\n// @build ${buildId}\n`; + Deno.writeTextFileSync(swDist, swStamped); +} + +site.addEventListener("afterBuild", writeFileTree); +site.addEventListener("afterUpdate", writeFileTree); + +//////////////////////////////////////////// +// INLINE JS FOR FACETS +//////////////////////////////////////////// + +const SCRIPT_SRC_RE = + /`); +} + +//////////////////////////////////////////// +// COMPRESSION +//////////////////////////////////////////// + +site.use(brotli()); +site.use(sourceMaps()); diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 000000000..773a6b55e --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,351 @@ +{ + "name": "@toko/diffuse", + "version": "4.0.0-alpha", + "vendor": true, + "imports": { + "98.css": "npm:98.css@^0.1.21", + "@atcute/atproto": "npm:@atcute/atproto@^4.0.2", + "@atcute/car": "npm:@atcute/car@^6.0.1", + "@atcute/cbor": "npm:@atcute/cbor@^2.3.4", + "@atcute/cid": "npm:@atcute/cid@^2.4.1", + "@atcute/tid": "npm:@atcute/tid@^1.1.2", + "@atcute/client": "npm:@atcute/client@^5.1.0", + "@atcute/firehose": "npm:@atcute/firehose@^1.0.0", + "@atcute/repo": "npm:@atcute/repo@^1.0.1", + "@atcute/identity-resolver": "npm:@atcute/identity-resolver@^2.0.0", + "@atcute/lexicons": "npm:@atcute/lexicons@^2.0.1", + "@atcute/oauth-browser-client": "npm:@atcute/oauth-browser-client@^4.0.0", + "@atcute/uint8array": "npm:@atcute/uint8array@^1.1.2", + "@automerge/automerge": "npm:@automerge/automerge@^3.2.6", + "@awesome.me/webawesome": "npm:@awesome.me/webawesome@^3.9.0", + "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.9.6", + "@cloudradio/icy-parser": "jsr:@cloudradio/icy-parser@^1.0.3", + "@char/cbor": "jsr:@char/cbor@^0.1.4", + "@codemirror/autocomplete": "npm:@codemirror/autocomplete@^6.20.3", + "@codemirror/lang-css": "npm:@codemirror/lang-css@^6.3.1", + "@codemirror/lang-html": "npm:@codemirror/lang-html@^6.4.11", + "@codemirror/lang-javascript": "npm:@codemirror/lang-javascript@^6.2.5", + "@fcrozatier/htmlcrunch": "jsr:@fcrozatier/htmlcrunch@^1.5.1", + "@fortawesome/free-regular-svg-icons": "npm:@fortawesome/free-regular-svg-icons@^7.2.0", + "@fry69/deep-diff": "jsr:@fry69/deep-diff@^0.1.10", + "@mary/ds-queue": "jsr:@mary/ds-queue@^0.1.3", + "@noble/ciphers": "npm:@noble/ciphers@^2.2.0", + "@noble/hashes": "npm:@noble/hashes@^2.2.0", + "@okikio/transferables": "jsr:@okikio/transferables@^1.0.2", + "@orama/orama": "npm:@orama/orama@^3.1.18", + "@phosphor-icons/web": "npm:@phosphor-icons/web@^2.1.2", + "@std/html": "jsr:@std/html@^1.0.7", + "@std/semver": "jsr:@std/semver@^1.0.8", + "@std/xml": "jsr:@std/xml@^0.1.2", + "@vicary/debounce-microtask": "jsr:@vicary/debounce-microtask@^0.1.8", + "@paulmillr/qr": "jsr:@paulmillr/qr@^0.6.0", + "@zip-js/zip-js": "jsr:@zip-js/zip-js@^2.8.26", + "alien-signals": "npm:alien-signals@^3.2.1", + "bs58check": "npm:bs58check@^4.0.0", + "codemirror": "npm:codemirror@^6.0.2", + "fast-average-color": "npm:fast-average-color@^9.5.2", + "fast-uri": "./src/common/fast-uri.js", + "idb-keyval": "npm:idb-keyval@^6.2.5", + "kmenu": "npm:kmenu@^2.0.3", + "iso-base": "npm:iso-base@^4.4.0", + "lit-html": "npm:lit-html@^3.3.3", + "marked": "npm:marked@^18.0.5", + "query-string": "npm:query-string@^9.4.0", + "subsonic-api": "npm:subsonic-api@^3.3.1", + "temporal-polyfill": "npm:temporal-polyfill@^1.0.1", + "throttle-debounce": "npm:throttle-debounce@^5.0.2", + "xxh32": "npm:xxh32@^2.0.5", + "butterchurn": "npm:butterchurn@3.0.0-beta.5", + "butterchurn-presets": "npm:butterchurn-presets@3.0.0-beta.4", + + // music-metadata + "@tokenizer/http": "npm:@tokenizer/http@^0.9.2", + "@tokenizer/range": "npm:@tokenizer/range@^0.13.1", + "music-metadata": "npm:music-metadata@^11.13.0", + + // Build + "@atcute/lex-cli": "npm:@atcute/lex-cli@^3.1.0", + "@custom-elements-manifest/analyzer": "npm:@custom-elements-manifest/analyzer@^0.11.0", + "@dotenv-run/esbuild": "npm:@dotenv-run/esbuild@^1.5.2", + "@std/fs": "jsr:@std/fs@^1.0.24", + "@std/path": "jsr:@std/path@^1.1.5", + "@types/wicg-file-system-access": "npm:@types/wicg-file-system-access@^2023.10.7", + "autoprefixer": "npm:autoprefixer@^10.5.2", + "cssnano": "npm:cssnano@^8.0.2", + "esbuild-plugins-node-modules-polyfill": "npm:esbuild-plugins-node-modules-polyfill@^1.8.2", + "esbuild-plugin-wasm": "npm:esbuild-plugin-wasm@^1.1.0", + "lume/": "https://cdn.jsdelivr.net/gh/lumeland/lume@3.2.4/", + "lume/jsx-runtime": "https://cdn.jsdelivr.net/gh/oscarotero/ssx@0.1.14/jsx-runtime.ts", + + // Tests + "@astral/astral": "jsr:@astral/astral@0.5.6", + "@std/expect": "jsr:@std/expect@1.0.19", + "@std/testing/bdd": "jsr:@std/testing@1.0.19/bdd", + + // Paths + "~/": "./src/", + + "@specs/": "./specs/", + "@tests/": "./tests/", + }, + "exports": { + // .js + "./common/foundation.js": "./src/common/foundation.js", + "./common/facets/utils.js": "./src/common/facets/utils.js", + "./common/element.js": "./src/common/element.js", + "./common/signal.js": "./src/common/signal.js", + "./common/worker.js": "./src/common/worker.js", + "./common/worker/rpc.js": "./src/common/worker/rpc.js", + "./common/utils.js": "./src/common/utils.js", + "./components/artwork/audio-metadata/element.js": "./src/components/artwork/audio-metadata/element.js", + "./components/artwork/audio-metadata/worker.js": "./src/components/artwork/audio-metadata/worker.js", + "./components/artwork/input/element.js": "./src/components/artwork/input/element.js", + "./components/artwork/input/worker.js": "./src/components/artwork/input/worker.js", + "./components/artwork/last.fm/element.js": "./src/components/artwork/last.fm/element.js", + "./components/artwork/last.fm/worker.js": "./src/components/artwork/last.fm/worker.js", + "./components/artwork/musicbrainz/element.js": "./src/components/artwork/musicbrainz/element.js", + "./components/artwork/musicbrainz/worker.js": "./src/components/artwork/musicbrainz/worker.js", + "./components/configurator/artwork/element.js": "./src/components/configurator/artwork/element.js", + "./components/configurator/artwork/worker.js": "./src/components/configurator/artwork/worker.js", + "./components/configurator/input/element.js": "./src/components/configurator/input/element.js", + "./components/configurator/input/worker.js": "./src/components/configurator/input/worker.js", + "./components/configurator/metadata/element.js": "./src/components/configurator/metadata/element.js", + "./components/configurator/metadata/worker.js": "./src/components/configurator/metadata/worker.js", + "./components/configurator/output/element.js": "./src/components/configurator/output/element.js", + "./components/configurator/scrobbles/element.js": "./src/components/configurator/scrobbles/element.js", + "./components/configurator/upload/element.js": "./src/components/configurator/upload/element.js", + "./components/configurator/upload/worker.js": "./src/components/configurator/upload/worker.js", + "./components/engine/audio/element.js": "./src/components/engine/audio/element.js", + "./components/engine/queue/element.js": "./src/components/engine/queue/element.js", + "./components/engine/queue/worker.js": "./src/components/engine/queue/worker.js", + "./components/engine/repeat-shuffle/element.js": "./src/components/engine/repeat-shuffle/element.js", + "./components/engine/scope/element.js": "./src/components/engine/scope/element.js", + "./components/input/common.js": "./src/components/input/common.js", + "./components/input/dropbox/common.js": "./src/components/input/dropbox/common.js", + "./components/input/dropbox/constants.js": "./src/components/input/dropbox/constants.js", + "./components/input/dropbox/element.js": "./src/components/input/dropbox/element.js", + "./components/input/dropbox/worker.js": "./src/components/input/dropbox/worker.js", + "./components/input/ephemeral-cache/constants.js": "./src/components/input/ephemeral-cache/constants.js", + "./components/input/ephemeral-cache/element.js": "./src/components/input/ephemeral-cache/element.js", + "./components/input/ephemeral-cache/worker.js": "./src/components/input/ephemeral-cache/worker.js", + "./components/input/https/common.js": "./src/components/input/https/common.js", + "./components/input/https/constants.js": "./src/components/input/https/constants.js", + "./components/input/https/element.js": "./src/components/input/https/element.js", + "./components/input/https/worker.js": "./src/components/input/https/worker.js", + "./components/input/icecast/common.js": "./src/components/input/icecast/common.js", + "./components/input/icecast/constants.js": "./src/components/input/icecast/constants.js", + "./components/input/icecast/element.js": "./src/components/input/icecast/element.js", + "./components/input/icecast/worker.js": "./src/components/input/icecast/worker.js", + "./components/input/local/common.js": "./src/components/input/local/common.js", + "./components/input/local/constants.js": "./src/components/input/local/constants.js", + "./components/input/local/element.js": "./src/components/input/local/element.js", + "./components/input/local/worker.js": "./src/components/input/local/worker.js", + "./components/input/opensubsonic/common.js": "./src/components/input/opensubsonic/common.js", + "./components/input/opensubsonic/constants.js": "./src/components/input/opensubsonic/constants.js", + "./components/input/opensubsonic/element.js": "./src/components/input/opensubsonic/element.js", + "./components/input/opensubsonic/worker.js": "./src/components/input/opensubsonic/worker.js", + "./components/input/s3/common.js": "./src/components/input/s3/common.js", + "./components/input/s3/constants.js": "./src/components/input/s3/constants.js", + "./components/input/s3/element.js": "./src/components/input/s3/element.js", + "./components/input/s3/worker.js": "./src/components/input/s3/worker.js", + "./components/input/webdav/constants.js": "./src/components/input/webdav/constants.js", + "./components/input/webdav/element.js": "./src/components/input/webdav/element.js", + "./components/input/webdav/worker.js": "./src/components/input/webdav/worker.js", + "./components/metadata/audio-file/element.js": "./src/components/metadata/audio-file/element.js", + "./components/metadata/audio-file/worker.js": "./src/components/metadata/audio-file/worker.js", + "./components/metadata/common.js": "./src/components/metadata/common.js", + "./components/metadata/spectrogram/element.js": "./src/components/metadata/spectrogram/element.js", + "./components/metadata/spectrogram/fft.js": "./src/components/metadata/spectrogram/fft.js", + "./components/metadata/spectrogram/worker.js": "./src/components/metadata/spectrogram/worker.js", + "./components/orchestrator/artwork/element.js": "./src/components/orchestrator/artwork/element.js", + "./components/orchestrator/artwork/worker.js": "./src/components/orchestrator/artwork/worker.js", + "./components/orchestrator/auto-queue/element.js": "./src/components/orchestrator/auto-queue/element.js", + "./components/orchestrator/controller/element.js": "./src/components/orchestrator/controller/element.js", + "./components/orchestrator/cover-groups/element.js": "./src/components/orchestrator/cover-groups/element.js", + "./components/orchestrator/favourites/element.js": "./src/components/orchestrator/favourites/element.js", + "./components/orchestrator/media-session/element.js": "./src/components/orchestrator/media-session/element.js", + "./components/orchestrator/output/element.js": "./src/components/orchestrator/output/element.js", + "./components/orchestrator/path-collections/element.js": "./src/components/orchestrator/path-collections/element.js", + "./components/orchestrator/process-tracks/element.js": "./src/components/orchestrator/process-tracks/element.js", + "./components/orchestrator/process-tracks/worker.js": "./src/components/orchestrator/process-tracks/worker.js", + "./components/orchestrator/queue-audio/element.js": "./src/components/orchestrator/queue-audio/element.js", + "./components/orchestrator/scoped-tracks/element.js": "./src/components/orchestrator/scoped-tracks/element.js", + "./components/orchestrator/scoped-tracks/worker.js": "./src/components/orchestrator/scoped-tracks/worker.js", + "./components/orchestrator/scrobble-audio/element.js": "./src/components/orchestrator/scrobble-audio/element.js", + "./components/orchestrator/sources/element.js": "./src/components/orchestrator/sources/element.js", + "./components/orchestrator/spectrogram-audio/element.js": "./src/components/orchestrator/spectrogram-audio/element.js", + "./components/output/bytes/s3/constants.js": "./src/components/output/bytes/s3/constants.js", + "./components/output/bytes/s3/element.js": "./src/components/output/bytes/s3/element.js", + "./components/output/bytes/s3/worker.js": "./src/components/output/bytes/s3/worker.js", + "./components/output/bytes/dropbox/common.js": "./src/components/output/bytes/dropbox/common.js", + "./components/output/bytes/dropbox/constants.js": "./src/components/output/bytes/dropbox/constants.js", + "./components/output/bytes/dropbox/element.js": "./src/components/output/bytes/dropbox/element.js", + "./components/output/bytes/dropbox/worker.js": "./src/components/output/bytes/dropbox/worker.js", + "./components/output/common.js": "./src/components/output/common.js", + "./components/output/polymorphic/indexed-db/constants.js": "./src/components/output/polymorphic/indexed-db/constants.js", + "./components/output/polymorphic/indexed-db/element.js": "./src/components/output/polymorphic/indexed-db/element.js", + "./components/output/polymorphic/indexed-db/worker.js": "./src/components/output/polymorphic/indexed-db/worker.js", + "./components/output/raw/atproto-passkey/element.js": "./src/components/output/raw/atproto-passkey/element.js", + "./components/supplement/last.fm/element.js": "./src/components/supplement/last.fm/element.js", + "./components/supplement/listenbrainz/element.js": "./src/components/supplement/listenbrainz/element.js", + "./components/supplement/rocksky/element.js": "./src/components/supplement/rocksky/element.js", + "./components/supplement/session.js": "./src/components/supplement/session.js", + "./components/transformer/output/base.js": "./src/components/transformer/output/base.js", + "./components/transformer/output/bytes/automerge/constants.js": "./src/components/transformer/output/bytes/automerge/constants.js", + "./components/transformer/output/bytes/automerge/element.js": "./src/components/transformer/output/bytes/automerge/element.js", + "./components/transformer/output/bytes/automerge/utils.js": "./src/components/transformer/output/bytes/automerge/utils.js", + "./components/transformer/output/bytes/dasl-sync/constants.js": "./src/components/transformer/output/bytes/dasl-sync/constants.js", + "./components/transformer/output/bytes/dasl-sync/element.js": "./src/components/transformer/output/bytes/dasl-sync/element.js", + "./components/transformer/output/bytes/json/element.js": "./src/components/transformer/output/bytes/json/element.js", + "./components/transformer/output/raw/atproto-sync/element.js": "./src/components/transformer/output/raw/atproto-sync/element.js", + "./components/transformer/output/refiner/default/element.js": "./src/components/transformer/output/refiner/default/element.js", + "./components/transformer/output/refiner/initial-contents/element.js": "./src/components/transformer/output/refiner/initial-contents/element.js", + "./components/transformer/output/refiner/passkey-encryption/element.js": "./src/components/transformer/output/refiner/passkey-encryption/element.js", + "./components/transformer/output/string/json/element.js": "./src/components/transformer/output/string/json/element.js", + "./components/upload/dropbox/common.js": "./src/components/upload/dropbox/common.js", + "./components/upload/dropbox/element.js": "./src/components/upload/dropbox/element.js", + "./components/upload/dropbox/worker.js": "./src/components/upload/dropbox/worker.js", + + // .d.ts + "./common/element.d.ts": "./src/common/element.d.ts", + "./common/signal.d.ts": "./src/common/signal.d.ts", + "./common/worker.d.ts": "./src/common/worker.d.ts", + "./common/fast-uri.js": "./src/common/fast-uri.js", + "./components/artwork/types.d.ts": "./specs/components/artwork/types.d.ts", + "./components/configurator/artwork/types.d.ts": "./specs/components/configurator/artwork/types.d.ts", + "./components/configurator/input/types.d.ts": "./specs/components/configurator/input/types.d.ts", + "./components/configurator/metadata/types.d.ts": "./specs/components/configurator/metadata/types.d.ts", + "./components/configurator/output/types.d.ts": "./specs/components/configurator/output/types.d.ts", + "./components/configurator/upload/types.d.ts": "./specs/components/configurator/upload/types.d.ts", + "./components/engine/audio/types.d.ts": "./specs/components/engine/audio/types.d.ts", + "./components/engine/queue/types.d.ts": "./specs/components/engine/queue/types.d.ts", + "./components/input/opensubsonic/types.d.ts": "./specs/components/input/opensubsonic/types.d.ts", + "./components/input/s3/types.d.ts": "./specs/components/input/s3/types.d.ts", + "./components/input/types.d.ts": "./specs/components/input/types.d.ts", + "./components/input/webdav/types.d.ts": "./specs/components/input/webdav/types.d.ts", + "./components/metadata/audio-file/types.d.ts": "./specs/components/metadata/audio-file/types.d.ts", + "./components/metadata/spectrogram/types.d.ts": "./specs/components/metadata/spectrogram/types.d.ts", + "./components/metadata/types.d.ts": "./specs/components/metadata/types.d.ts", + "./components/orchestrator/artwork/types.d.ts": "./specs/components/orchestrator/artwork/types.d.ts", + "./components/orchestrator/favourites/types.d.ts": "./specs/components/orchestrator/favourites/types.d.ts", + "./components/orchestrator/process-tracks/types.d.ts": "./specs/components/orchestrator/process-tracks/types.d.ts", + "./components/orchestrator/scoped-tracks/types.d.ts": "./specs/components/orchestrator/scoped-tracks/types.d.ts", + "./components/output/bytes/s3/types.d.ts": "./specs/components/output/bytes/s3/types.d.ts", + "./components/output/bytes/dropbox/types.d.ts": "./specs/components/output/bytes/dropbox/types.d.ts", + "./components/output/polymorphic/indexed-db/types.d.ts": "./specs/components/output/polymorphic/indexed-db/types.d.ts", + "./components/output/raw/atproto-passkey/types.d.ts": "./specs/components/output/raw/atproto-passkey/types.d.ts", + "./components/output/types.d.ts": "./specs/components/output/types.d.ts", + "./components/supplement/types.d.ts": "./specs/components/supplement/types.d.ts", + "./components/transformer/output/bytes/automerge/types.d.ts": "./specs/components/transformer/output/bytes/automerge/types.d.ts", + "./components/transformer/output/bytes/dasl-sync/types.d.ts": "./specs/components/transformer/output/bytes/dasl-sync/types.d.ts", + "./components/upload/types.d.ts": "./specs/components/upload/types.d.ts", + "./definitions/types.d.ts": "./src/definitions/types.d.ts", + + // .ts + "./definitions/types/sh/diffuse/output/facet.ts": "./src/definitions/types/sh/diffuse/output/facet.ts", + "./definitions/types/sh/diffuse/output/playlistItem.ts": "./src/definitions/types/sh/diffuse/output/playlistItem.ts", + "./definitions/types/sh/diffuse/output/theme.ts": "./src/definitions/types/sh/diffuse/output/theme.ts", + "./definitions/types/sh/diffuse/output/track.ts": "./src/definitions/types/sh/diffuse/output/track.ts", + }, + "tasks": { + "build": { + "description": "Build the site for production", + "command": "deno task lume", + }, + "gen:defs:types": { + "description": "Generate definition Typescript types", + "command": "deno run -A npm:@atcute/lex-cli generate -c ./lexicon.config.js && deno run -A ./tasks/replace-gen-import-extensions.ts", + }, + "gen:manifest": { + "description": "Generate the custom-elements.json manifest", + "command": "deno run -A ./tasks/gen-manifest.js", + }, + "lume": { + "description": "Run Lume command", + "command": "deno run -P=lume --allow-write --allow-read lume/cli.ts", + }, + "serve": { + "description": "Run and serve the site for development", + "command": "static-web-server -p 3000 --root dist/", + }, + "test:doc": { + "description": "Run documentation tests", + "command": "deno test -A --doc --ignore=README.md --ignore=src/skills src", + }, + "test:types": { + "description": "Make sure everything type checks", + "command": "deno check specs && deno check src", + }, + "test:unit": { + "description": "Run unit tests (documentation tests)", + "command": "deno task test:doc", + }, + "test:integration": { + "description": "Run integration tests", + "command": "deno test tests -A --ignore=README.md", + }, + "test:integration:coverage": { + "description": "Run integration tests with code coverage", + "command": "DENO_COVERAGE_DIR=./cov deno test tests -A --coverage=./cov --ignore=README.md", + }, + "coverage": { + "description": "Generate coverage report from collected data", + "command": "deno coverage ./cov --lcov --output=./cov.lcov", + }, + "coverage:html": { + "description": "Generate HTML coverage report", + "command": "deno coverage ./cov --html", + }, + "vit:ship:diffuse-facet": { + "description": "Build and publish the diffuse-facet skill to vit", + "command": "deno task build && deno x vit ship --skill dist/skills/diffuse-facet/", + }, + }, + "compilerOptions": { + "checkJs": true, + "lib": ["deno.ns", "dom", "esnext"], + "types": ["lume/types.ts", "@types/wicg-file-system-access"], + "jsx": "react-jsx", + "jsxImportSource": "lume", + }, + "unstable": ["temporal", "fmt-component"], + "lint": { + "plugins": ["https://cdn.jsdelivr.net/gh/lumeland/lume@3.1.4/lint.ts"], + "rules": { + "exclude": ["no-import-prefix"], + }, + }, + "publish": { + "exclude": ["!src/definitions/types"], + "include": [ + "LICENSE", + "src/common/**/*.js", + "src/common/**/*.d.ts", + "src/components/**/*.js", + "specs/components/**/*.d.ts", + "src/definitions/**/*.json", + "src/definitions/**/*.ts", + ], + }, + "permissions": { + "lume": { + "read": true, + "write": ["./"], + "import": ["cdn.jsdelivr.net:443", "jsr.io:443", "deno.land:443", "esm.sh:443"], + "net": [ + "0.0.0.0", + "cdn.jsdelivr.net:443", + "data.jsdelivr.com:443", + "jsr.io:443", + "deno.land:443", + "esm.sh:443", + "registry.npmjs.org:443", + ], + "env": true, + "run": true, + "ffi": true, + "sys": true, + }, + }, + "lock": false, +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 000000000..395a73cea --- /dev/null +++ b/deno.lock @@ -0,0 +1,2149 @@ +{ + "version": "5", + "specifiers": { + "jsr:@bradenmacdonald/s3-lite-client@~0.9.4": "0.9.4", + "jsr:@deno/loader@0.3.6": "0.3.6", + "jsr:@fry69/deep-diff@~0.1.10": "0.1.10", + "jsr:@kunkun/kkrpc@0.6": "0.6.0", + "jsr:@mary/ds-queue@~0.1.3": "0.1.3", + "jsr:@mys/m-rpc@~0.12.2": "0.12.2", + "jsr:@mys/worker-fn@^3.2.1": "3.2.1", + "jsr:@okikio/transferables@^1.0.2": "1.0.2", + "jsr:@orama/orama@^2.0.6": "2.0.6", + "jsr:@std/cli@1.0.22": "1.0.22", + "jsr:@std/cli@^1.0.21": "1.0.22", + "jsr:@std/collections@^1.1.3": "1.1.3", + "jsr:@std/crypto@1.0.5": "1.0.5", + "jsr:@std/encoding@1.0.10": "1.0.10", + "jsr:@std/encoding@^1.0.10": "1.0.10", + "jsr:@std/fmt@1.0.8": "1.0.8", + "jsr:@std/fmt@^1.0.8": "1.0.8", + "jsr:@std/front-matter@1.0.9": "1.0.9", + "jsr:@std/fs@1.0.19": "1.0.19", + "jsr:@std/fs@^1.0.19": "1.0.19", + "jsr:@std/html@^1.0.4": "1.0.5", + "jsr:@std/http@1.0.20": "1.0.20", + "jsr:@std/internal@^1.0.10": "1.0.12", + "jsr:@std/internal@^1.0.9": "1.0.12", + "jsr:@std/jsonc@1.0.2": "1.0.2", + "jsr:@std/media-types@^1.1.0": "1.1.0", + "jsr:@std/net@^1.0.4": "1.0.6", + "jsr:@std/path@1.1.2": "1.1.2", + "jsr:@std/path@^1.1.1": "1.1.2", + "jsr:@std/path@^1.1.2": "1.1.2", + "jsr:@std/semver@1.0.5": "1.0.5", + "jsr:@std/streams@^1.0.10": "1.0.13", + "jsr:@std/toml@1.0.10": "1.0.10", + "jsr:@std/toml@^1.0.3": "1.0.10", + "jsr:@std/yaml@1.0.9": "1.0.9", + "jsr:@std/yaml@^1.0.5": "1.0.9", + "jsr:@vicary/debounce-microtask@~0.1.8": "0.1.8", + "npm:98.css@~0.1.21": "0.1.21", + "npm:@atcute/lex-cli@*": "2.3.1", + "npm:@atcute/lex-cli@^2.3.1": "2.3.1", + "npm:@atcute/lexicons@^1.2.2": "1.2.2", + "npm:@js-temporal/polyfill@~0.5.1": "0.5.1", + "npm:@phosphor-icons/web@^2.1.2": "2.1.2", + "npm:@tauri-apps/plugin-shell@^2.2.0": "2.3.3", + "npm:alien-signals@3": "3.0.3", + "npm:autoprefixer@10.4.21": "10.4.21_postcss@8.5.6", + "npm:esbuild-plugins-node-modules-polyfill@^1.7.1": "1.7.1_esbuild@0.25.12", + "npm:fast-average-color@^9.5.0": "9.5.0", + "npm:idb-keyval@^6.2.2": "6.2.2", + "npm:lightningcss-wasm@1.30.1": "1.30.1", + "npm:lit-html@^3.3.1": "3.3.1", + "npm:markdown-it-attrs@4.3.1": "4.3.1_markdown-it@14.1.0", + "npm:markdown-it-deflist@3.0.0": "3.0.0", + "npm:markdown-it@14.1.0": "14.1.0", + "npm:morphdom@^2.7.7": "2.7.7", + "npm:postcss-import@16.1.1": "16.1.1_postcss@8.5.6", + "npm:postcss@8.5.6": "8.5.6", + "npm:purgecss-from-html@7.0.2": "7.0.2", + "npm:purgecss@7.0.2": "7.0.2", + "npm:query-string@^9.3.1": "9.3.1", + "npm:socket.io-client@^4.8.1": "4.8.1", + "npm:subsonic-api@^3.2.0": "3.2.0", + "npm:superjson@^2.2.2": "2.2.5", + "npm:throttle-debounce@^5.0.2": "5.0.2", + "npm:uint8arrays@^5.1.0": "5.1.0", + "npm:uri-js@^4.4.1": "4.4.1", + "npm:webamp@^2.2.0": "2.2.0_redux@5.0.1_react@19.2.0_react-dom@19.2.0__react@19.2.0", + "npm:xxh32@^2.0.5": "2.0.5" + }, + "jsr": { + "@bradenmacdonald/s3-lite-client@0.9.4": { + "integrity": "f52e31c7efdaeb1ccdf65a1db995b5920d635717c96d45dcf9450c3cc47ecaaf" + }, + "@deno/loader@0.3.6": { + "integrity": "98f08d837c18ece5ba15122264fb29580967407c34e6552e152b8f453a60c2be" + }, + "@fry69/deep-diff@0.1.10": { + "integrity": "cdd88fefaef1ac896a038a5f3c0895038d8c725e61bac50489c455156e0275f5" + }, + "@kunkun/kkrpc@0.6.0": { + "integrity": "44674738cac0712740ee9e2a57048bd53e1086829a2c4f21e15c03333793a19f", + "dependencies": [ + "npm:@tauri-apps/plugin-shell", + "npm:socket.io-client", + "npm:superjson" + ] + }, + "@mary/ds-queue@0.1.3": { + "integrity": "a743caa397b924cb08b0bbdffc526eb1ea2d3fc9e675da6edc137c437fc93c76" + }, + "@mys/m-rpc@0.12.2": { + "integrity": "36599d3d4708db9f5c0f7da35a17b7e7da1fafddb69de6cfcdc6afe94cd4f084", + "dependencies": [ + "jsr:@okikio/transferables" + ] + }, + "@mys/worker-fn@3.2.1": { + "integrity": "330960f21041edd20fa9c5f78b136f62e3781e35797ac635534f003545be76cd", + "dependencies": [ + "jsr:@mys/m-rpc" + ] + }, + "@okikio/transferables@1.0.2": { + "integrity": "46a80015a1c4672b0b246e38838b3ea1e2edc6c775a235184a2f8eb49a8314f7" + }, + "@orama/orama@2.0.6": { + "integrity": "0221bbd9a638e42772fc6e082cf057755f83a8cea919c85fa0baa9bb027ab078" + }, + "@std/cli@1.0.22": { + "integrity": "50d1e4f87887cb8a8afa29b88505ab5081188f5cad3985460c3b471fa49ff21a" + }, + "@std/collections@1.1.3": { + "integrity": "bf8b0818886df6a32b64c7d3b037a425111f28278d69fd0995aeb62777c986b0" + }, + "@std/crypto@1.0.5": { + "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" + }, + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" + }, + "@std/fmt@1.0.8": { + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" + }, + "@std/front-matter@1.0.9": { + "integrity": "ee6201d06674cbef137dda2252f62477450b48249e7d8d9ab57a30f85ff6f051", + "dependencies": [ + "jsr:@std/toml@^1.0.3", + "jsr:@std/yaml@^1.0.5" + ] + }, + "@std/fs@1.0.19": { + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06", + "dependencies": [ + "jsr:@std/internal@^1.0.9", + "jsr:@std/path@^1.1.1" + ] + }, + "@std/html@1.0.5": { + "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" + }, + "@std/http@1.0.20": { + "integrity": "b5cc33fc001bccce65ed4c51815668c9891c69ccd908295997e983d8f56070a1", + "dependencies": [ + "jsr:@std/cli@^1.0.21", + "jsr:@std/encoding@^1.0.10", + "jsr:@std/fmt@^1.0.8", + "jsr:@std/fs@^1.0.19", + "jsr:@std/html", + "jsr:@std/media-types", + "jsr:@std/net", + "jsr:@std/path@^1.1.1", + "jsr:@std/streams" + ] + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + }, + "@std/jsonc@1.0.2": { + "integrity": "909605dae3af22bd75b1cbda8d64a32cf1fd2cf6efa3f9e224aba6d22c0f44c7" + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/net@1.0.6": { + "integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c" + }, + "@std/path@1.1.2": { + "integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038", + "dependencies": [ + "jsr:@std/internal@^1.0.10" + ] + }, + "@std/semver@1.0.5": { + "integrity": "529f79e83705714c105ad0ba55bec0f9da0f24d2f726b6cc1c15e505cc2c0624" + }, + "@std/streams@1.0.13": { + "integrity": "772d208cd0d3e5dac7c1d9e6cdb25842846d136eea4a41a62e44ed4ab0c8dd9e" + }, + "@std/toml@1.0.10": { + "integrity": "87b2b7ff95afe7209a868732eb013a2707be29a15229f5b57bb13eededff4655", + "dependencies": [ + "jsr:@std/collections" + ] + }, + "@std/yaml@1.0.9": { + "integrity": "6bad3dc766dd85b4b37eabcba81b6aa4eac7a392792ae29abcfb0f90602d55bb" + }, + "@vicary/debounce-microtask@0.1.8": { + "integrity": "fe180e0c599903ccf7a93e719ea986c48affc1ff78951a1bc0ccb874aa30fd0e" + } + }, + "npm": { + "98.css@0.1.21": { + "integrity": "sha512-ddk5qtUWyapM0Bzd5jwGExoE5fdSEGrP+F5VbYjyZLf2c9UVmn6w2NPTvCsoD4BWdGsjdLjlkQGhWwWTJcYQJQ==" + }, + "@assemblyscript/loader@0.17.14": { + "integrity": "sha512-+PVTOfla/0XMLRTQLJFPg4u40XcdTfon6GGea70hBGi8Pd7ZymIXyVUR+vK8wt5Jb4MVKTKPIz43Myyebw5mZA==" + }, + "@atcute/lex-cli@2.3.1": { + "integrity": "sha512-HrHD91CFSFd/p0UFe3akFA1HXiboQwd5LbYiU0srKdLxGX+NLTX/EdCdhbLV6M7LsXdmxk7PB6BMcprsX4rbvg==", + "dependencies": [ + "@atcute/lexicon-doc", + "@badrap/valita", + "@optique/core", + "@optique/run", + "picocolors", + "prettier" + ], + "bin": true + }, + "@atcute/lexicon-doc@1.1.4": { + "integrity": "sha512-OL0fsXtbnN/KwCq/L3nWGvOCdSHV0NWTatgLUIPt+T9AhcziFNaXAbbjvVHdflr3ZaLh3ksleHK0J789UBhlWQ==", + "dependencies": [ + "@badrap/valita" + ] + }, + "@atcute/lexicons@1.2.2": { + "integrity": "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA==", + "dependencies": [ + "@standard-schema/spec", + "esm-env" + ] + }, + "@babel/runtime@7.28.4": { + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==" + }, + "@badrap/valita@0.4.6": { + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" + }, + "@borewit/text-codec@0.1.1": { + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==" + }, + "@borewit/text-codec@0.2.0": { + "integrity": "sha512-X999CKBxGwX8wW+4gFibsbiNdwqmdQEXmUejIWaIqdrHBgS5ARIOOeyiQbHjP9G58xVEPcuvP6VwwH3A0OFTOA==" + }, + "@esbuild/aix-ppc64@0.25.12": { + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.25.12": { + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.25.12": { + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.25.12": { + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.25.12": { + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.25.12": { + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.25.12": { + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.25.12": { + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.25.12": { + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.25.12": { + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.25.12": { + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.25.12": { + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.25.12": { + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.25.12": { + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.25.12": { + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.25.12": { + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.25.12": { + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-arm64@0.25.12": { + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-x64@0.25.12": { + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-arm64@0.25.12": { + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-x64@0.25.12": { + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openharmony-arm64@0.25.12": { + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/sunos-x64@0.25.12": { + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.25.12": { + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.25.12": { + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.25.12": { + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@isaacs/balanced-match@4.0.1": { + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==" + }, + "@isaacs/brace-expansion@5.0.0": { + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dependencies": [ + "@isaacs/balanced-match" + ] + }, + "@isaacs/cliui@8.0.2": { + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": [ + "string-width@5.1.2", + "string-width-cjs@npm:string-width@4.2.3", + "strip-ansi@7.1.2", + "strip-ansi-cjs@npm:strip-ansi@6.0.1", + "wrap-ansi@8.1.0", + "wrap-ansi-cjs@npm:wrap-ansi@7.0.0" + ] + }, + "@js-temporal/polyfill@0.5.1": { + "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", + "dependencies": [ + "jsbi" + ] + }, + "@jspm/core@2.1.0": { + "integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==" + }, + "@optique/core@0.6.2": { + "integrity": "sha512-HTxIHJ8xLOSZotiU6Zc5BCJv+SJ8DMYmuiQM+7tjF7RolJn/pdZNe7M78G3+DgXL9lIf82l8aGcilmgVYRQnGQ==" + }, + "@optique/run@0.6.2": { + "integrity": "sha512-ERksB5bHozwEUVlTPToIc8UjZZBOgLeBhFZYh2lgldUbNDt7LItzgcErsPq5au5i5IBmmyCti4+2A3x+MRI4Xw==", + "dependencies": [ + "@optique/core" + ] + }, + "@phosphor-icons/web@2.1.2": { + "integrity": "sha512-rPAR9o/bEcp4Cw4DEeZHXf+nlGCMNGkNDRizYHM47NLxz9vvEHp/Tt6FMK1NcWadzw/pFDPnRBGi/ofRya958A==" + }, + "@redux-devtools/extension@3.3.0_redux@5.0.1": { + "integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==", + "dependencies": [ + "@babel/runtime", + "immutable", + "redux" + ] + }, + "@sentry/browser@5.9.1": { + "integrity": "sha512-7AOabwp9yAH9h6Xe6TfDwlLxHbUSWs+SPWHI7bPlht2yDSAqkXYGSzRr5X0XQJX9oBQdx2cEPMqHyJrbNaP/og==", + "dependencies": [ + "@sentry/core", + "@sentry/types", + "@sentry/utils", + "tslib" + ] + }, + "@sentry/core@5.8.0": { + "integrity": "sha512-aAh2KLidIXJVGrxmHSVq2eVKbu7tZiYn5ylW6yzJXFetS5z4MA+JYaSBaG2inVYDEEqqMIkb17TyWxxziUDieg==", + "dependencies": [ + "@sentry/hub", + "@sentry/minimal", + "@sentry/types", + "@sentry/utils", + "tslib" + ] + }, + "@sentry/hub@5.8.0": { + "integrity": "sha512-VdApn1ZCNwH1wwQwoO6pu53PM/qgHG+DQege0hbByluImpLBhAj9w50nXnF/8KzV4UoMIVbzCb6jXzMRmqqp9A==", + "dependencies": [ + "@sentry/types", + "@sentry/utils", + "tslib" + ] + }, + "@sentry/minimal@5.8.0": { + "integrity": "sha512-MIlFOgd+JvAUrBBmq7vr9ovRH1HvckhnwzHdoUPpKRBN+rQgTyZy1o6+kA2fASCbrRqFCP+Zk7EHMACKg8DpIw==", + "dependencies": [ + "@sentry/hub", + "@sentry/types", + "tslib" + ] + }, + "@sentry/types@5.7.1": { + "integrity": "sha512-tbUnTYlSliXvnou5D4C8Zr+7/wJrHLbpYX1YkLXuIJRU0NSi81bHMroAuHWILcQKWhVjaV/HZzr7Y/hhWtbXVQ==" + }, + "@sentry/utils@5.8.0": { + "integrity": "sha512-KDxUvBSYi0/dHMdunbxAxD3389pcQioLtcO6CI6zt/nJXeVFolix66cRraeQvqupdLhvOk/el649W4fCPayTHw==", + "dependencies": [ + "@sentry/types", + "tslib" + ] + }, + "@socket.io/component-emitter@3.1.2": { + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "@standard-schema/spec@1.0.0": { + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, + "@tauri-apps/api@2.9.0": { + "integrity": "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==" + }, + "@tauri-apps/plugin-shell@2.3.3": { + "integrity": "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==", + "dependencies": [ + "@tauri-apps/api" + ] + }, + "@tokenizer/inflate@0.2.7": { + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "dependencies": [ + "debug@4.4.3", + "fflate", + "token-types@6.1.1" + ] + }, + "@tokenizer/token@0.3.0": { + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "@types/hoist-non-react-statics@3.3.7_@types+react@19.2.2": { + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "dependencies": [ + "@types/react", + "hoist-non-react-statics" + ] + }, + "@types/react@19.2.2": { + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dependencies": [ + "csstype" + ] + }, + "@types/trusted-types@2.0.7": { + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "@types/use-sync-external-store@0.0.3": { + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "acorn@8.15.0": { + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": true + }, + "alien-signals@3.0.3": { + "integrity": "sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==" + }, + "ani-cursor@0.0.5": { + "integrity": "sha512-gGxst72lG9TOwEfbVpX9vHhzUGw+4Ee2XB6AfYq5JP+bxBtpAjgnTBepCVxYF5t1TPrWHN23nWqLTflJOA3/ag==", + "dependencies": [ + "byte-data", + "riff-file" + ] + }, + "ansi-regex@5.0.1": { + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-regex@6.2.2": { + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==" + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "ansi-styles@6.2.3": { + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==" + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "assert@1.5.1": { + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", + "dependencies": [ + "object.assign", + "util" + ] + }, + "autoprefixer@10.4.21_postcss@8.5.6": { + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dependencies": [ + "browserslist", + "caniuse-lite", + "fraction.js", + "normalize-range", + "picocolors", + "postcss", + "postcss-value-parser" + ], + "bin": true + }, + "babel-runtime@6.26.0": { + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dependencies": [ + "core-js", + "regenerator-runtime" + ] + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "baseline-browser-mapping@2.8.25": { + "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "bin": true + }, + "browserslist@4.27.0": { + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dependencies": [ + "baseline-browser-mapping", + "caniuse-lite", + "electron-to-chromium", + "node-releases", + "update-browserslist-db" + ], + "bin": true + }, + "buffer@5.7.1": { + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dependencies": [ + "base64-js", + "ieee754" + ] + }, + "butterchurn-presets@3.0.0-beta.4": { + "integrity": "sha512-TbQLUPvGOYMZAtWKoCmBtludh9aQZ6NaMGQU4lvPeadBPy3Du3yNmwBjlTMLP5c5mRWElxQPjTL1PtR7FZK3OQ==", + "dependencies": [ + "@babel/runtime" + ] + }, + "butterchurn@3.0.0-beta.5": { + "integrity": "sha512-BStK4OAbBb9Pvt8PuQlS4WVmYBwU1KuDMRHF1V89QjoIFauAqq7tpV4EpYXj7K563r5daLrMX+2y5DBhZZ9Xig==", + "dependencies": [ + "@assemblyscript/loader", + "ecma-proposal-math-extensions", + "eel-wasm" + ] + }, + "byte-data@18.1.1": { + "integrity": "sha512-Kv/B0r7adgnCcrs/y703sac2XFLdHW5kPfis1j8+Ij/hmEcWhBKf+1pNTv+vsNqXb207Uiyri8bpnogNxR/4Lg==", + "dependencies": [ + "endianness", + "ieee754-buffer", + "utf8-buffer" + ] + }, + "call-bind-apply-helpers@1.0.2": { + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": [ + "es-errors", + "function-bind" + ] + }, + "call-bind@1.0.8": { + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": [ + "call-bind-apply-helpers", + "es-define-property", + "get-intrinsic", + "set-function-length" + ] + }, + "call-bound@1.0.4": { + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": [ + "call-bind-apply-helpers", + "get-intrinsic" + ] + }, + "caniuse-lite@1.0.30001754": { + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==" + }, + "chainsaw@0.0.9": { + "integrity": "sha512-nG8PYH+/4xB+8zkV4G844EtfvZ5tTiLFoX3dZ4nhF4t3OCKIb9UvaFyNmeZO2zOSmRWzBoTD+napN6hiL+EgcA==", + "dependencies": [ + "traverse" + ] + }, + "classnames@2.5.1": { + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "commander@12.1.0": { + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==" + }, + "confbox@0.1.8": { + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" + }, + "confbox@0.2.2": { + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==" + }, + "content-type@1.0.5": { + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "copy-anything@4.0.5": { + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dependencies": [ + "is-what" + ] + }, + "core-js@2.6.12": { + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": true, + "scripts": true + }, + "core-util-is@1.0.3": { + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, + "cssesc@3.0.0": { + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": true + }, + "csstype@3.1.3": { + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "debug@4.3.7": { + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": [ + "ms" + ] + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "decode-uri-component@0.4.1": { + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==" + }, + "define-data-property@1.1.4": { + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": [ + "es-define-property", + "es-errors", + "gopd" + ] + }, + "define-properties@1.2.1": { + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": [ + "define-data-property", + "has-property-descriptors", + "object-keys" + ] + }, + "domelementtype@2.3.0": { + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler@5.0.3": { + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": [ + "domelementtype" + ] + }, + "dunder-proto@1.0.1": { + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": [ + "call-bind-apply-helpers", + "es-errors", + "gopd" + ] + }, + "eastasianwidth@0.2.0": { + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "ecma-proposal-math-extensions@0.0.2": { + "integrity": "sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==" + }, + "eel-wasm@0.0.16": { + "integrity": "sha512-1tkId7I7E1Vs4fXTRsH83Sjn2S/AbzrVQKLBRGys6NLc3eVH4NBffJsdEeLHOWWUgQpVXBEP3CV/srUZNIuBnw==" + }, + "electron-to-chromium@1.5.248": { + "integrity": "sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==" + }, + "emoji-regex@8.0.0": { + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "emoji-regex@9.2.2": { + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "endianness@8.0.2": { + "integrity": "sha512-IU+77+jJ7lpw2qZ3NUuqBZFy3GuioNgXUdsL1L9tooDNTaw0TgOnwNuc+8Ns+haDaTifK97QLzmOANJtI/rGvw==" + }, + "engine.io-client@6.6.3": { + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "dependencies": [ + "@socket.io/component-emitter", + "debug@4.3.7", + "engine.io-parser", + "ws", + "xmlhttprequest-ssl" + ] + }, + "engine.io-parser@5.2.3": { + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==" + }, + "entities@4.5.0": { + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "entities@6.0.1": { + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==" + }, + "es-define-property@1.0.1": { + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors@1.3.0": { + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms@1.1.1": { + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": [ + "es-errors" + ] + }, + "esbuild-plugins-node-modules-polyfill@1.7.1_esbuild@0.25.12": { + "integrity": "sha512-IEaUhaS1RukGGcatDzqJmR+AzUWJ2upTJZP2i7FzR37Iw5Lk0LReCTnWnPMWnGG9lO4MWTGKEGGLWEOPegTRJA==", + "dependencies": [ + "@jspm/core", + "esbuild", + "local-pkg", + "resolve.exports" + ] + }, + "esbuild@0.25.12": { + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ], + "scripts": true, + "bin": true + }, + "escalade@3.2.0": { + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "esm-env@1.2.2": { + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" + }, + "exsolve@1.0.8": { + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==" + }, + "fast-average-color@9.5.0": { + "integrity": "sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg==" + }, + "fflate@0.8.2": { + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, + "file-type@11.1.0": { + "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==" + }, + "file-type@21.0.0": { + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "dependencies": [ + "@tokenizer/inflate", + "strtok3@10.3.4", + "token-types@6.1.1", + "uint8array-extras" + ] + }, + "filter-obj@5.1.0": { + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==" + }, + "foreground-child@3.3.1": { + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": [ + "cross-spawn", + "signal-exit" + ] + }, + "fraction.js@4.3.7": { + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" + }, + "fscreen@1.2.0": { + "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" + }, + "function-bind@1.1.2": { + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic@1.3.0": { + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": [ + "call-bind-apply-helpers", + "es-define-property", + "es-errors", + "es-object-atoms", + "function-bind", + "get-proto", + "gopd", + "has-symbols", + "hasown", + "math-intrinsics" + ] + }, + "get-proto@1.0.1": { + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": [ + "dunder-proto", + "es-object-atoms" + ] + }, + "glob@11.0.3": { + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dependencies": [ + "foreground-child", + "jackspeak", + "minimatch", + "minipass", + "package-json-from-dist", + "path-scurry" + ], + "bin": true + }, + "glsl-optimizer-js@0.0.2": { + "integrity": "sha512-SMkVILyc1LeBEBgiHOe+4Bh8MEqxLNyAns0NfgmxJTxZZdj7oCoZt+n846rbdB8OLGsg16f5C9nmhi9XEuM8SQ==" + }, + "gopd@1.2.0": { + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "has-property-descriptors@1.0.2": { + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": [ + "es-define-property" + ] + }, + "has-symbols@1.1.0": { + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "hashish@0.0.4": { + "integrity": "sha512-xyD4XgslstNAs72ENaoFvgMwtv8xhiDtC2AtzCG+8yF7W/Knxxm9BX+e2s25mm+HxMKh0rBmXVOEGF3zNImXvA==", + "dependencies": [ + "traverse" + ] + }, + "hasown@2.0.2": { + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": [ + "function-bind" + ] + }, + "hoist-non-react-statics@3.3.2": { + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": [ + "react-is@16.13.1" + ] + }, + "idb-keyval@6.2.2": { + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==" + }, + "ieee754-buffer@2.0.0": { + "integrity": "sha512-AXUAT0nMEi7h1Is8HXGXof3eejl/GabZFKSj8Ym6kVRUSwrAb52EkAXywiCQYSHGQMRn7lvfY7vhPMjVc+Kybg==" + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "immediate@3.0.6": { + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "immutable@4.3.7": { + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" + }, + "inherits@2.0.3": { + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "invariant@2.2.4": { + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": [ + "loose-envify" + ] + }, + "is-core-module@2.16.1": { + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": [ + "hasown" + ] + }, + "is-fullwidth-code-point@3.0.0": { + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-typedarray@1.0.0": { + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "is-what@5.5.0": { + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==" + }, + "isarray@1.0.0": { + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jackspeak@4.1.1": { + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dependencies": [ + "@isaacs/cliui" + ] + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsbi@4.3.2": { + "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==" + }, + "jszip@3.10.1": { + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": [ + "lie", + "pako", + "readable-stream@2.3.8", + "setimmediate" + ] + }, + "lie@3.3.0": { + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": [ + "immediate" + ] + }, + "lightningcss-wasm@1.30.1": { + "integrity": "sha512-KJTnKEn0REV6DoJzxG0m5EKVEFA1CVE1isDYpXjsuqWXwLKFPJtA9Z9BSzPZJwAZFN2KaUzy+IWGP59p5bm2sA==" + }, + "linkify-it@5.0.0": { + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": [ + "uc.micro" + ] + }, + "lit-html@3.3.1": { + "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", + "dependencies": [ + "@types/trusted-types" + ] + }, + "local-pkg@1.1.2": { + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dependencies": [ + "mlly", + "pkg-types@2.3.0", + "quansync" + ] + }, + "lodash@4.17.21": { + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "loose-envify@1.4.0": { + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": [ + "js-tokens" + ], + "bin": true + }, + "lru-cache@11.2.2": { + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==" + }, + "markdown-it-attrs@4.3.1_markdown-it@14.1.0": { + "integrity": "sha512-/ko6cba+H6gdZ0DOw7BbNMZtfuJTRp9g/IrGIuz8lYc/EfnmWRpaR3CFPnNbVz0LDvF8Gf1hFGPqrQqq7De0rg==", + "dependencies": [ + "markdown-it" + ] + }, + "markdown-it-deflist@3.0.0": { + "integrity": "sha512-OxPmQ/keJZwbubjiQWOvKLHwpV2wZ5I3Smc81OjhwbfJsjdRrvD5aLTQxmZzzePeO0kbGzAo3Krk4QLgA8PWLg==" + }, + "markdown-it@14.1.0": { + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": [ + "argparse", + "entities@4.5.0", + "linkify-it", + "mdurl", + "punycode.js", + "uc.micro" + ], + "bin": true + }, + "math-intrinsics@1.1.0": { + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "mdurl@2.0.0": { + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "media-typer@0.3.0": { + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "media-typer@1.1.0": { + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "milkdrop-eel-parser@0.0.4": { + "integrity": "sha512-4PsOdTMDB7GM3UFzqXQQXf8MBeoolOhsBLMlhug+IIMZ+yNkvqLbdqDbrueGZc8P8tLRJP8pbAxna1yjFr06HQ==" + }, + "milkdrop-preset-converter-aws@0.1.6": { + "integrity": "sha512-nr89LRZYgdrDn17vGQCvUK/LM9d90mywElL7zlzXBTgkxWAs/Kamn1Yl9676ugt4L4BAGo6PTEipIqeYXFSM7g==", + "dependencies": [ + "babel-runtime", + "glsl-optimizer-js", + "milkdrop-eel-parser", + "milkdrop-preset-utils" + ] + }, + "milkdrop-preset-utils@0.1.0": { + "integrity": "sha512-yK5y03SN8INC+ssLLYGGsaAHgNxXEUK6PQVV44rg9OAA27F2aPM0tA5uGsDdASH9sgPaAaRVMV5NoEvEkh66Sw==", + "dependencies": [ + "babel-runtime", + "lodash" + ] + }, + "minimatch@10.1.1": { + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dependencies": [ + "@isaacs/brace-expansion" + ] + }, + "minipass@7.1.2": { + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "mlly@1.8.0": { + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dependencies": [ + "acorn", + "pathe", + "pkg-types@1.3.1", + "ufo" + ] + }, + "morphdom@2.7.7": { + "integrity": "sha512-04GmsiBcalrSCNmzfo+UjU8tt3PhZJKzcOy+r1FlGA7/zri8wre3I1WkYN9PT3sIeIKfW9bpyElA+VzOg2E24g==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "multiformats@13.4.1": { + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==" + }, + "music-metadata-browser@0.6.6": { + "integrity": "sha512-14KFz4HR6rM6RATcLtJoBDRbehU/dKdVzElCdeI8CjP7Un2HtSf0WiT7f7Lz+XNkcBMZUjthmC6Wy4+NNayCRw==", + "dependencies": [ + "assert", + "buffer", + "debug@4.4.3", + "music-metadata@3.8.0", + "readable-stream@3.6.2", + "remove", + "typedarray-to-buffer" + ], + "deprecated": true + }, + "music-metadata@11.9.0": { + "integrity": "sha512-J7VqD8FY6KRcm75Fzj86FPsckiD/EdvO5OS3P+JiMf/2krP3TcAseZYfkic6eFeJ0iBhhzcdxgfu8hLW95aXXw==", + "dependencies": [ + "@borewit/text-codec@0.2.0", + "@tokenizer/token", + "content-type", + "debug@4.4.3", + "file-type@21.0.0", + "media-typer@1.1.0", + "strtok3@10.3.4", + "token-types@6.1.1", + "uint8array-extras" + ] + }, + "music-metadata@3.8.0": { + "integrity": "sha512-aIADbp3uCS+ANr4nnFEHzTzMy81OT7PR7WBMW73SJ28Y7P94nnEugmTOj1ICP2JmxBBDlo+MeYVgiPnxVN69tg==", + "dependencies": [ + "debug@4.4.3", + "file-type@11.1.0", + "media-typer@0.3.0", + "strtok3@2.3.0", + "token-types@1.3.2" + ] + }, + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "bin": true + }, + "node-releases@2.0.27": { + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" + }, + "normalize-range@0.1.2": { + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" + }, + "object-keys@1.1.1": { + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign@4.1.7": { + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dependencies": [ + "call-bind", + "call-bound", + "define-properties", + "es-object-atoms", + "has-symbols", + "object-keys" + ] + }, + "package-json-from-dist@1.0.1": { + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "pako@1.0.11": { + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parse5-htmlparser2-tree-adapter@7.1.0": { + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dependencies": [ + "domhandler", + "parse5" + ] + }, + "parse5@7.3.0": { + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dependencies": [ + "entities@6.0.1" + ] + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-parse@1.0.7": { + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-scurry@2.0.1": { + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dependencies": [ + "lru-cache", + "minipass" + ] + }, + "pathe@2.0.3": { + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "pify@2.3.0": { + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + }, + "pkg-types@1.3.1": { + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dependencies": [ + "confbox@0.1.8", + "mlly", + "pathe" + ] + }, + "pkg-types@2.3.0": { + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dependencies": [ + "confbox@0.2.2", + "exsolve", + "pathe" + ] + }, + "postcss-import@16.1.1_postcss@8.5.6": { + "integrity": "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==", + "dependencies": [ + "postcss", + "postcss-value-parser", + "read-cache", + "resolve" + ] + }, + "postcss-selector-parser@6.1.2": { + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss-value-parser@4.2.0": { + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "postcss@8.5.6": { + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "prettier@3.6.2": { + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "bin": true + }, + "process-nextick-args@2.0.1": { + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "punycode.js@2.3.1": { + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "purgecss-from-html@7.0.2": { + "integrity": "sha512-eJOLW9wIt30qvruvz+FCBmaW5XLt+bx0VCGKn+ZhEDzj69e834kt4pIWhIn0APFfwYu4t9x5rSkjyAvbI77xqg==", + "dependencies": [ + "parse5", + "parse5-htmlparser2-tree-adapter" + ] + }, + "purgecss@7.0.2": { + "integrity": "sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ==", + "dependencies": [ + "commander", + "glob", + "postcss", + "postcss-selector-parser" + ], + "bin": true + }, + "quansync@0.2.11": { + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==" + }, + "query-string@9.3.1": { + "integrity": "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==", + "dependencies": [ + "decode-uri-component", + "filter-obj", + "split-on-first" + ] + }, + "react-dom@19.2.0_react@19.2.0": { + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "dependencies": [ + "react", + "scheduler" + ] + }, + "react-is@16.13.1": { + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-is@18.3.1": { + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "react-redux@8.1.3_react@19.2.0_react-dom@19.2.0__react@19.2.0_redux@5.0.1": { + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "dependencies": [ + "@babel/runtime", + "@types/hoist-non-react-statics", + "@types/use-sync-external-store", + "hoist-non-react-statics", + "react", + "react-dom", + "react-is@18.3.1", + "redux", + "use-sync-external-store" + ], + "optionalPeers": [ + "react-dom", + "redux" + ] + }, + "react@19.2.0": { + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==" + }, + "read-cache@1.0.0": { + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": [ + "pify" + ] + }, + "readable-stream@2.3.8": { + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": [ + "core-util-is", + "inherits", + "isarray", + "process-nextick-args", + "safe-buffer", + "string_decoder", + "util-deprecate" + ] + }, + "readable-stream@3.6.2": { + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": [ + "inherits", + "string_decoder", + "util-deprecate" + ] + }, + "redux-sentry-middleware@0.1.8": { + "integrity": "sha512-xubpYH9RgE31tZUESeRW5agwQa19Yd6Gy+4iO09raW/2TITPO5fhJdXpVwJfpGMbIYhEmHFqE2wD5Lnz7YtAeA==" + }, + "redux-thunk@2.4.2_redux@5.0.1": { + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "dependencies": [ + "redux" + ] + }, + "redux@5.0.1": { + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "regenerator-runtime@0.11.1": { + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "remove@0.1.5": { + "integrity": "sha512-AJMA9oWvJzdTjwIGwSQZsjGQiRx73YTmiOWmfCp1fpLa/D4n7jKcpoA+CZiVLJqKcEKUuh1Suq80c5wF+L/qVQ==", + "dependencies": [ + "seq" + ] + }, + "reselect@3.0.1": { + "integrity": "sha512-b/6tFZCmRhtBMa4xGqiiRp9jh9Aqi2A687Lo265cN0/QohJQEBPiQ52f4QB6i0eF3yp3hmLL21LSGBcML2dlxA==" + }, + "resolve.exports@2.0.3": { + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==" + }, + "resolve@1.22.11": { + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dependencies": [ + "is-core-module", + "path-parse", + "supports-preserve-symlinks-flag" + ], + "bin": true + }, + "riff-file@1.0.3": { + "integrity": "sha512-Vv8wwGr0BCks7VMI3Lv0houZee4DaHFjjTT0LMhMJKio2YmLncLeIVpK63ydSverngNk8XQPU3fbeP3bWgSIig==", + "dependencies": [ + "byte-data" + ] + }, + "safe-buffer@5.1.2": { + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "scheduler@0.27.0": { + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "seq@0.3.5": { + "integrity": "sha512-sisY2Ln1fj43KBkRtXkesnRHYNdswIkIibvNe/0UKm2GZxjMbqmccpiatoKr/k2qX5VKiLU8xm+tz/74LAho4g==", + "dependencies": [ + "chainsaw", + "hashish" + ] + }, + "set-function-length@1.2.2": { + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": [ + "define-data-property", + "es-errors", + "function-bind", + "get-intrinsic", + "gopd", + "has-property-descriptors" + ] + }, + "setimmediate@1.0.5": { + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit@4.1.0": { + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, + "socket.io-client@4.8.1": { + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "dependencies": [ + "@socket.io/component-emitter", + "debug@4.3.7", + "engine.io-client", + "socket.io-parser" + ] + }, + "socket.io-parser@4.2.4": { + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": [ + "@socket.io/component-emitter", + "debug@4.3.7" + ] + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "split-on-first@3.0.0": { + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==" + }, + "string-width@4.2.3": { + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": [ + "emoji-regex@8.0.0", + "is-fullwidth-code-point", + "strip-ansi@6.0.1" + ] + }, + "string-width@5.1.2": { + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": [ + "eastasianwidth", + "emoji-regex@9.2.2", + "strip-ansi@7.1.2" + ] + }, + "string_decoder@1.1.1": { + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": [ + "safe-buffer" + ] + }, + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": [ + "ansi-regex@5.0.1" + ] + }, + "strip-ansi@7.1.2": { + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": [ + "ansi-regex@6.2.2" + ] + }, + "strtok3@10.3.4": { + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "dependencies": [ + "@tokenizer/token" + ] + }, + "strtok3@2.3.0": { + "integrity": "sha512-AA67/1atBh7X0fUTDevjW89by2ZkY9RZAnkwusx5Yc1COYf0ruUbpYOOIs03SnRA1CF9K3+BtRXKOEtKhAXVaQ==", + "dependencies": [ + "debug@4.4.3", + "then-read-stream", + "token-types@1.3.2" + ] + }, + "subsonic-api@3.2.0": { + "integrity": "sha512-BADBQ2hONdLb3agCiSDzNzTIFLWJAuxJTUJvC2zDFvXUVfnK3yy7r8xFu3NkrQl8p5UVI7q8Qfm62N1lFxWbww==" + }, + "superjson@2.2.5": { + "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==", + "dependencies": [ + "copy-anything" + ] + }, + "supports-preserve-symlinks-flag@1.0.0": { + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "then-read-stream@1.5.1": { + "integrity": "sha512-I+iiemYWhp1ysJQEioqpEICgvHlqHS5WrQGZkboFLs7Jm350Kvq4cN3qRCzHpETUuq5+NsdrdWEg6M0NFxtwtQ==", + "deprecated": true + }, + "throttle-debounce@5.0.2": { + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==" + }, + "tinyqueue@1.2.3": { + "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==" + }, + "token-types@1.3.2": { + "integrity": "sha512-LemYprKRfZPUiwVEMIL8fIP/cvZBpMds1PklsyoQyLZdKk7SQlldNGzw4TTrg2MnWLGSkMM6gUa1EW0h1d72fg==", + "dependencies": [ + "ieee754" + ] + }, + "token-types@6.1.1": { + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "dependencies": [ + "@borewit/text-codec@0.1.1", + "@tokenizer/token", + "ieee754" + ] + }, + "traverse@0.3.9": { + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" + }, + "tslib@1.14.1": { + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "typedarray-to-buffer@3.1.5": { + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": [ + "is-typedarray" + ] + }, + "uc.micro@2.1.0": { + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "ufo@1.6.1": { + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==" + }, + "uint8array-extras@1.5.0": { + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==" + }, + "uint8arrays@5.1.0": { + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "dependencies": [ + "multiformats" + ] + }, + "update-browserslist-db@1.1.4_browserslist@4.27.0": { + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dependencies": [ + "browserslist", + "escalade", + "picocolors" + ], + "bin": true + }, + "uri-js@4.4.1": { + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": [ + "punycode" + ] + }, + "use-sync-external-store@1.6.0_react@19.2.0": { + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dependencies": [ + "react" + ] + }, + "utf8-buffer@1.0.0": { + "integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==" + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "util@0.10.4": { + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": [ + "inherits" + ] + }, + "webamp@2.2.0_redux@5.0.1_react@19.2.0_react-dom@19.2.0__react@19.2.0": { + "integrity": "sha512-XzKr65Z4d+4rxA1J//aPkZRqvPS0aqAxpryNKaWt/EDQ4uCJadxjr966QElagH+iZxWMCDekW5dV/dTx5b+WPQ==", + "dependencies": [ + "@redux-devtools/extension", + "@sentry/browser", + "ani-cursor", + "butterchurn", + "butterchurn-presets", + "classnames", + "fscreen", + "invariant", + "jszip", + "lodash", + "milkdrop-preset-converter-aws", + "music-metadata@11.9.0", + "music-metadata-browser", + "react", + "react-dom", + "react-redux", + "redux", + "redux-sentry-middleware", + "redux-thunk", + "reselect", + "strtok3@10.3.4", + "tinyqueue", + "winamp-eqf" + ] + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ], + "bin": true + }, + "winamp-eqf@1.0.0": { + "integrity": "sha512-yUIb4+lTYBKP4L6nPXdDj1CQBXlJ+/PrNAkT1VbTAgeFjX8lPxAthsUE5NxQP4s8SO4YMJemsrErZ49Bh+/Veg==" + }, + "wrap-ansi@7.0.0": { + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": [ + "ansi-styles@4.3.0", + "string-width@4.2.3", + "strip-ansi@6.0.1" + ] + }, + "wrap-ansi@8.1.0": { + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": [ + "ansi-styles@6.2.3", + "string-width@5.1.2", + "strip-ansi@7.1.2" + ] + }, + "ws@8.17.1": { + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==" + }, + "xmlhttprequest-ssl@2.1.2": { + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==" + }, + "xxh32@2.0.5": { + "integrity": "sha512-glQIaPvLHV4xG2Sn0E4mZWY25JT34+XcG4e2c8OMIH2SXxVrm6MmJ8miCsqGBLtf+rn2YcaeS11vq/66vkXGUQ==" + } + }, + "redirects": { + "https://esm.sh/@borewit/text-codec@^0.1.0?target=denonext": "https://esm.sh/@borewit/text-codec@0.1.1?target=denonext", + "https://esm.sh/@borewit/text-codec@^0.2.0?target=denonext": "https://esm.sh/@borewit/text-codec@0.2.0?target=denonext", + "https://esm.sh/@tokenizer/inflate@^0.2.7?target=denonext": "https://esm.sh/@tokenizer/inflate@0.2.7?target=denonext", + "https://esm.sh/@tokenizer/range@^0.12.0?target=denonext": "https://esm.sh/@tokenizer/range@0.12.0?target=denonext", + "https://esm.sh/content-type@^1.0.5?target=denonext": "https://esm.sh/content-type@1.0.5?target=denonext", + "https://esm.sh/debug@^4.3.7?target=denonext": "https://esm.sh/debug@4.4.3?target=denonext", + "https://esm.sh/debug@^4.4.0?target=denonext": "https://esm.sh/debug@4.4.3?target=denonext", + "https://esm.sh/debug@^4.4.3?target=denonext": "https://esm.sh/debug@4.4.3?target=denonext", + "https://esm.sh/fflate@^0.8.2?target=denonext": "https://esm.sh/fflate@0.8.2?target=denonext", + "https://esm.sh/file-type@^21.0.0?target=denonext": "https://esm.sh/file-type@21.0.0?target=denonext", + "https://esm.sh/ieee754@^1.2.1?target=denonext": "https://esm.sh/ieee754@1.2.1?target=denonext", + "https://esm.sh/media-typer@^1.1.0?target=denonext": "https://esm.sh/media-typer@1.1.0?target=denonext", + "https://esm.sh/ms@^2.1.3?target=denonext": "https://esm.sh/ms@2.1.3?target=denonext", + "https://esm.sh/peek-readable@^5.3.1?target=denonext": "https://esm.sh/peek-readable@5.4.2?target=denonext", + "https://esm.sh/strtok3@^10.2.0?target=denonext": "https://esm.sh/strtok3@10.3.4?target=denonext", + "https://esm.sh/strtok3@^10.2.2/core?target=denonext": "https://esm.sh/strtok3@10.3.4/core?target=denonext", + "https://esm.sh/strtok3@^10.3.4?target=denonext": "https://esm.sh/strtok3@10.3.4?target=denonext", + "https://esm.sh/strtok3@^9.1.1?target=denonext": "https://esm.sh/strtok3@9.1.1?target=denonext", + "https://esm.sh/supports-color?target=denonext": "https://esm.sh/supports-color@10.2.2?target=denonext", + "https://esm.sh/token-types@^6.0.0?target=denonext": "https://esm.sh/token-types@6.1.1?target=denonext", + "https://esm.sh/token-types@^6.1.1?target=denonext": "https://esm.sh/token-types@6.1.1?target=denonext", + "https://esm.sh/uint8array-extras@^1.4.0?target=denonext": "https://esm.sh/uint8array-extras@1.5.0?target=denonext", + "https://esm.sh/uint8array-extras@^1.5.0?target=denonext": "https://esm.sh/uint8array-extras@1.5.0?target=denonext" + }, + "remote": { + "https://deno.land/std@0.170.0/_util/asserts.ts": "d0844e9b62510f89ce1f9878b046f6a57bf88f208a10304aab50efcb48365272", + "https://deno.land/std@0.170.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", + "https://deno.land/std@0.170.0/encoding/base64.ts": "8605e018e49211efc767686f6f687827d7f5fd5217163e981d8d693105640d7a", + "https://deno.land/std@0.170.0/fmt/colors.ts": "03ad95e543d2808bc43c17a3dd29d25b43d0f16287fe562a0be89bf632454a12", + "https://deno.land/std@0.170.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.170.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.170.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", + "https://deno.land/std@0.170.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.170.0/path/glob.ts": "81cc6c72be002cd546c7a22d1f263f82f63f37fe0035d9726aa96fc8f6e4afa1", + "https://deno.land/std@0.170.0/path/mod.ts": "cf7cec7ac11b7048bb66af8ae03513e66595c279c65cfa12bfc07d9599608b78", + "https://deno.land/std@0.170.0/path/posix.ts": "b859684bc4d80edfd4cad0a82371b50c716330bed51143d6dcdbe59e6278b30c", + "https://deno.land/std@0.170.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.170.0/path/win32.ts": "7cebd2bda6657371adc00061a1d23fdd87bcdf64b4843bb148b0b24c11b40f69", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/cli/parse_args.ts": "5250832fb7c544d9111e8a41ad272c016f5a53f975ef84d5a9fe5fcb70566ece", + "https://deno.land/std@0.224.0/cli/spinner.ts": "cf873605771270b4324cc063b5031ab250d8efee8799e45e1a3bfdd333ff721d", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", + "https://deno.land/std@0.224.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412", + "https://deno.land/std@0.224.0/fs/_get_file_info_type.ts": "da7bec18a7661dba360a1db475b826b18977582ce6fc9b25f3d4ee0403fe8cbd", + "https://deno.land/std@0.224.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e", + "https://deno.land/std@0.224.0/fs/ensure_dir.ts": "51a6279016c65d2985f8803c848e2888e206d1b510686a509fa7cc34ce59d29f", + "https://deno.land/std@0.224.0/fs/expand_glob.ts": "2e428d90acc6676b2aa7b5c78ef48f30641b13f1fe658e7976c9064fb4b05309", + "https://deno.land/std@0.224.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303", + "https://deno.land/std@0.224.0/jsonc/parse.ts": "06fbe10f0bb0cba684f7902bf7de5126b16eb0e5a82220c98a4b86675c7f9cff", + "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.224.0/semver/_shared.ts": "5c53a675225cba9ad74ae2e17c124e333728fc2b551a13e8a32b99433b90c1c2", + "https://deno.land/std@0.224.0/semver/compare.ts": "7b5610c25ded57dc4aa41034ee02857d1a6fff609ab183afea17849284f86236", + "https://deno.land/std@0.224.0/semver/format.ts": "a4492b55a10210a10b9307491c0ec7f0c2475cc82af33de1c2565a15083b83df", + "https://deno.land/std@0.224.0/semver/less_than.ts": "890eb36e6294d245934a33dbe6818164c4ec6fddf3aa585a590031393f781719", + "https://deno.land/std@0.224.0/semver/parse.ts": "94c09f3486643853e7438e64f2c6741462fbeb84cf141ad5bfe88b73ec4cb0f3", + "https://deno.land/std@0.224.0/semver/types.ts": "9286e72b160e25856608f4bc5f08f8f5ccba54e6cdfc9aba8adee68a355c4b36", + "https://deno.land/x/cliffy@v0.25.7/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", + "https://deno.land/x/cliffy@v0.25.7/ansi/ansi.ts": "7f43d07d31dd7c24b721bb434c39cbb5132029fa4be3dd8938873065f65e5810", + "https://deno.land/x/cliffy@v0.25.7/ansi/ansi_escapes.ts": "885f61f343223f27b8ec69cc138a54bea30542924eacd0f290cd84edcf691387", + "https://deno.land/x/cliffy@v0.25.7/ansi/chain.ts": "31fb9fcbf72fed9f3eb9b9487270d2042ccd46a612d07dd5271b1a80ae2140a0", + "https://deno.land/x/cliffy@v0.25.7/ansi/colors.ts": "5f71993af5bd1aa0a795b15f41692d556d7c89584a601fed75997df844b832c9", + "https://deno.land/x/cliffy@v0.25.7/ansi/cursor_position.ts": "d537491e31d9c254b208277448eff92ff7f55978c4928dea363df92c0df0813f", + "https://deno.land/x/cliffy@v0.25.7/ansi/deps.ts": "0f35cb7e91868ce81561f6a77426ea8bc55dc15e13f84c7352f211023af79053", + "https://deno.land/x/cliffy@v0.25.7/ansi/mod.ts": "bb4e6588e6704949766205709463c8c33b30fec66c0b1846bc84a3db04a4e075", + "https://deno.land/x/cliffy@v0.25.7/ansi/tty.ts": "8fb064c17ead6cdf00c2d3bc87a9fd17b1167f2daa575c42b516f38bdb604673", + "https://deno.land/x/cliffy@v0.25.7/command/_errors.ts": "a9bd23dc816b32ec96c9b8f3057218241778d8c40333b43341138191450965e5", + "https://deno.land/x/cliffy@v0.25.7/command/_utils.ts": "9ab3d69fabab6c335b881b8a5229cbd5db0c68f630a1c307aff988b6396d9baf", + "https://deno.land/x/cliffy@v0.25.7/command/command.ts": "a2b83c612acd65c69116f70dec872f6da383699b83874b70fcf38cddf790443f", + "https://deno.land/x/cliffy@v0.25.7/command/completions/_bash_completions_generator.ts": "43b4abb543d4dc60233620d51e69d82d3b7c44e274e723681e0dce2a124f69f9", + "https://deno.land/x/cliffy@v0.25.7/command/completions/_fish_completions_generator.ts": "d0289985f5cf0bd288c05273bfa286b24c27feb40822eb7fd9d7fee64e6580e8", + "https://deno.land/x/cliffy@v0.25.7/command/completions/_zsh_completions_generator.ts": "14461eb274954fea4953ee75938821f721da7da607dc49bcc7db1e3f33a207bd", + "https://deno.land/x/cliffy@v0.25.7/command/completions/bash.ts": "053aa2006ec327ccecacb00ba28e5eb836300e5c1bec1b3cfaee9ddcf8189756", + "https://deno.land/x/cliffy@v0.25.7/command/completions/complete.ts": "58df61caa5e6220ff2768636a69337923ad9d4b8c1932aeb27165081c4d07d8b", + "https://deno.land/x/cliffy@v0.25.7/command/completions/fish.ts": "9938beaa6458c6cf9e2eeda46a09e8cd362d4f8c6c9efe87d3cd8ca7477402a5", + "https://deno.land/x/cliffy@v0.25.7/command/completions/mod.ts": "aeef7ec8e319bb157c39a4bab8030c9fe8fa327b4c1e94c9c1025077b45b40c0", + "https://deno.land/x/cliffy@v0.25.7/command/completions/zsh.ts": "8b04ab244a0b582f7927d405e17b38602428eeb347a9968a657e7ea9f40e721a", + "https://deno.land/x/cliffy@v0.25.7/command/deprecated.ts": "bbe6670f1d645b773d04b725b8b8e7814c862c9f1afba460c4d599ffe9d4983c", + "https://deno.land/x/cliffy@v0.25.7/command/deps.ts": "275b964ce173770bae65f6b8ebe9d2fd557dc10292cdd1ed3db1735f0d77fa1d", + "https://deno.land/x/cliffy@v0.25.7/command/help/_help_generator.ts": "f7c349cb2ddb737e70dc1f89bcb1943ca9017a53506be0d4138e0aadb9970a49", + "https://deno.land/x/cliffy@v0.25.7/command/help/mod.ts": "09d74d3eb42d21285407cda688074c29595d9c927b69aedf9d05ff3f215820d3", + "https://deno.land/x/cliffy@v0.25.7/command/mod.ts": "d0a32df6b14028e43bb2d41fa87d24bc00f9662a44e5a177b3db02f93e473209", + "https://deno.land/x/cliffy@v0.25.7/command/type.ts": "24e88e3085e1574662b856ccce70d589959648817135d4469fab67b9cce1b364", + "https://deno.land/x/cliffy@v0.25.7/command/types.ts": "ae02eec0ed7a769f7dba2dd5d3a931a61724b3021271b1b565cf189d9adfd4a0", + "https://deno.land/x/cliffy@v0.25.7/command/types/action_list.ts": "33c98d449617c7a563a535c9ceb3741bde9f6363353fd492f90a74570c611c27", + "https://deno.land/x/cliffy@v0.25.7/command/types/boolean.ts": "3879ec16092b4b5b1a0acb8675f8c9250c0b8a972e1e4c7adfba8335bd2263ed", + "https://deno.land/x/cliffy@v0.25.7/command/types/child_command.ts": "f1fca390c7fbfa7a713ca15ef55c2c7656bcbb394d50e8ef54085bdf6dc22559", + "https://deno.land/x/cliffy@v0.25.7/command/types/command.ts": "325d0382e383b725fd8d0ef34ebaeae082c5b76a1f6f2e843fee5dbb1a4fe3ac", + "https://deno.land/x/cliffy@v0.25.7/command/types/enum.ts": "2178345972adf7129a47e5f02856ca3e6852a91442a1c78307dffb8a6a3c6c9f", + "https://deno.land/x/cliffy@v0.25.7/command/types/file.ts": "8618f16ac9015c8589cbd946b3de1988cc4899b90ea251f3325c93c46745140e", + "https://deno.land/x/cliffy@v0.25.7/command/types/integer.ts": "29864725fd48738579d18123d7ee78fed37515e6dc62146c7544c98a82f1778d", + "https://deno.land/x/cliffy@v0.25.7/command/types/number.ts": "aeba96e6f470309317a16b308c82e0e4138a830ec79c9877e4622c682012bc1f", + "https://deno.land/x/cliffy@v0.25.7/command/types/string.ts": "e4dadb08a11795474871c7967beab954593813bb53d9f69ea5f9b734e43dc0e0", + "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts": "17e2df3b620905583256684415e6c4a31e8de5c59066eb6d6c9c133919292dc4", + "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider.ts": "d6fb846043232cbd23c57d257100c7fc92274984d75a5fead0f3e4266dc76ab8", + "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider/deno_land.ts": "24f8d82e38c51e09be989f30f8ad21f9dd41ac1bb1973b443a13883e8ba06d6d", + "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider/github.ts": "99e1b133dd446c6aa79f69e69c46eb8bc1c968dd331c2a7d4064514a317c7b59", + "https://deno.land/x/cliffy@v0.25.7/command/upgrade/provider/nest_land.ts": "0e07936cea04fa41ac9297f32d87f39152ea873970c54cb5b4934b12fee1885e", + "https://deno.land/x/cliffy@v0.25.7/command/upgrade/upgrade_command.ts": "3640a287d914190241ea1e636774b1b4b0e1828fa75119971dd5304784061e05", + "https://deno.land/x/cliffy@v0.25.7/flags/_errors.ts": "f1fbb6bfa009e7950508c9d491cfb4a5551027d9f453389606adb3f2327d048f", + "https://deno.land/x/cliffy@v0.25.7/flags/_utils.ts": "340d3ecab43cde9489187e1f176504d2c58485df6652d1cdd907c0e9c3ce4cc2", + "https://deno.land/x/cliffy@v0.25.7/flags/_validate_flags.ts": "16eb5837986c6f6f7620817820161a78d66ce92d690e3697068726bbef067452", + "https://deno.land/x/cliffy@v0.25.7/flags/deprecated.ts": "a72a35de3cc7314e5ebea605ca23d08385b218ef171c32a3f135fb4318b08126", + "https://deno.land/x/cliffy@v0.25.7/flags/flags.ts": "68a9dfcacc4983a84c07ba19b66e5e9fccd04389fad215210c60fb414cc62576", + "https://deno.land/x/cliffy@v0.25.7/flags/mod.ts": "b21c2c135cd2437cc16245c5f168a626091631d6d4907ad10db61c96c93bdb25", + "https://deno.land/x/cliffy@v0.25.7/flags/types.ts": "7452ea5296758fb7af89930349ce40d8eb9a43b24b3f5759283e1cb5113075fd", + "https://deno.land/x/cliffy@v0.25.7/flags/types/boolean.ts": "4c026dd66ec9c5436860dc6d0241427bdb8d8e07337ad71b33c08193428a2236", + "https://deno.land/x/cliffy@v0.25.7/flags/types/integer.ts": "b60d4d590f309ddddf066782d43e4dc3799f0e7d08e5ede7dc62a5ee94b9a6d9", + "https://deno.land/x/cliffy@v0.25.7/flags/types/number.ts": "610936e2d29de7c8c304b65489a75ebae17b005c6122c24e791fbed12444d51e", + "https://deno.land/x/cliffy@v0.25.7/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5", + "https://deno.land/x/cliffy@v0.25.7/keycode/key_code.ts": "c4ab0ffd102c2534962b765ded6d8d254631821bf568143d9352c1cdcf7a24be", + "https://deno.land/x/cliffy@v0.25.7/keycode/key_codes.ts": "917f0a2da0dbace08cf29bcfdaaa2257da9fe7e705fff8867d86ed69dfb08cfe", + "https://deno.land/x/cliffy@v0.25.7/keycode/mod.ts": "292d2f295316c6e0da6955042a7b31ab2968ff09f2300541d00f05ed6c2aa2d4", + "https://deno.land/x/cliffy@v0.25.7/mod.ts": "e3515ccf6bd4e4ac89322034e07e2332ed71901e4467ee5bc9d72851893e167b", + "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_input.ts": "737cff2de02c8ce35250f5dd79c67b5fc176423191a2abd1f471a90dd725659e", + "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_list.ts": "79b301bf09eb19f0d070d897f613f78d4e9f93100d7e9a26349ef0bfaa7408d2", + "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_prompt.ts": "8630ce89a66d83e695922df41721cada52900b515385d86def597dea35971bb2", + "https://deno.land/x/cliffy@v0.25.7/prompt/_generic_suggestions.ts": "2a8b619f91e8f9a270811eff557f10f1343a444a527b5fc22c94de832939920c", + "https://deno.land/x/cliffy@v0.25.7/prompt/_utils.ts": "676cca30762656ed1a9bcb21a7254244278a23ffc591750e98a501644b6d2df3", + "https://deno.land/x/cliffy@v0.25.7/prompt/checkbox.ts": "e5a5a9adbb86835dffa2afbd23c6f7a8fe25a9d166485388ef25aba5dc3fbf9e", + "https://deno.land/x/cliffy@v0.25.7/prompt/confirm.ts": "94c8e55de3bbcd53732804420935c432eab29945497d1c47c357d236a89cb5f6", + "https://deno.land/x/cliffy@v0.25.7/prompt/deps.ts": "4c38ab18e55a792c9a136c1c29b2b6e21ea4820c45de7ef4cf517ce94012c57d", + "https://deno.land/x/cliffy@v0.25.7/prompt/figures.ts": "26af0fbfe21497220e4b887bb550fab997498cde14703b98e78faf370fbb4b94", + "https://deno.land/x/cliffy@v0.25.7/prompt/input.ts": "ee45532e0a30c2463e436e08ae291d79d1c2c40872e17364c96d2b97c279bf4d", + "https://deno.land/x/cliffy@v0.25.7/prompt/list.ts": "6780427ff2a932a48c9b882d173c64802081d6cdce9ff618d66ba6504b6abc50", + "https://deno.land/x/cliffy@v0.25.7/prompt/mod.ts": "195aed14d10d279914eaa28c696dec404d576ca424c097a5bc2b4a7a13b66c89", + "https://deno.land/x/cliffy@v0.25.7/prompt/number.ts": "015305a76b50138234dde4fd50eb886c6c7c0baa1b314caf811484644acdc2cf", + "https://deno.land/x/cliffy@v0.25.7/prompt/prompt.ts": "0e7f6a1d43475ee33fb25f7d50749b2f07fc0bcddd9579f3f9af12d05b4a4412", + "https://deno.land/x/cliffy@v0.25.7/prompt/secret.ts": "58745f5231fb2c44294c4acf2511f8c5bfddfa1e12f259580ff90dedea2703d6", + "https://deno.land/x/cliffy@v0.25.7/prompt/select.ts": "1e982eae85718e4e15a3ee10a5ae2233e532d7977d55888f3a309e8e3982b784", + "https://deno.land/x/cliffy@v0.25.7/prompt/toggle.ts": "842c3754a40732f2e80bcd4670098713e402e64bd930e6cab2b787f7ad4d931a", + "https://deno.land/x/cliffy@v0.25.7/table/border.ts": "2514abae4e4f51eda60a5f8c927ba24efd464a590027e900926b38f68e01253c", + "https://deno.land/x/cliffy@v0.25.7/table/cell.ts": "1d787d8006ac8302020d18ec39f8d7f1113612c20801b973e3839de9c3f8b7b3", + "https://deno.land/x/cliffy@v0.25.7/table/deps.ts": "5b05fa56c1a5e2af34f2103fd199e5f87f0507549963019563eae519271819d2", + "https://deno.land/x/cliffy@v0.25.7/table/layout.ts": "46bf10ae5430cf4fbb92f23d588230e9c6336edbdb154e5c9581290562b169f4", + "https://deno.land/x/cliffy@v0.25.7/table/mod.ts": "e74f69f38810ee6139a71132783765feb94436a6619c07474ada45b465189834", + "https://deno.land/x/cliffy@v0.25.7/table/row.ts": "5f519ba7488d2ef76cbbf50527f10f7957bfd668ce5b9169abbc44ec88302645", + "https://deno.land/x/cliffy@v0.25.7/table/table.ts": "ec204c9d08bb3ff1939c5ac7412a4c9ed7d00925d4fc92aff9bfe07bd269258d", + "https://deno.land/x/cliffy@v0.25.7/table/utils.ts": "187bb7dcbcfb16199a5d906113f584740901dfca1007400cba0df7dcd341bc29", + "https://deno.land/x/deno_dom@v0.1.56/build/deno-wasm/deno-wasm-dynamic.js": "fd05e83a855aa68b09396dc47a7638ef526c39917971715ac37e0fc3227db886", + "https://deno.land/x/deno_dom@v0.1.56/build/deno-wasm/deno-wasm.js": "ccde5219e42040b8f7a8b653acc6c8cca197c29f98d6b75f74c48111e34bcbea", + "https://deno.land/x/deno_dom@v0.1.56/build/deno-wasm/deno-wasm_bg-wasm.js": "52c57905a74047d1574f0b96e301ef1d5974cd962705615555b5e46b5ac420e4", + "https://deno.land/x/deno_dom@v0.1.56/build/deno-wasm/deno-wasm_bg.wasm": "b5fbd7ede316f7c13e4a7b927a053a35b475a9acce7c7cae864e91d9b8dd24c7", + "https://deno.land/x/deno_dom@v0.1.56/build/deno-wasm/env.js": "17da784c11b192c591dbf7df21ec6f65f63f45ca9a713e4ca100f3c886b4023f", + "https://deno.land/x/deno_dom@v0.1.56/build/deno-wasm/wbg.js": "b5b6de12bc010fa23438c8a8276067b9044a405da516d0bbe4ae61b7c6d61a22", + "https://deno.land/x/deno_dom@v0.1.56/deno-dom-wasm.ts": "34f0654b452568fbd95b8d1b2a493de19710ac9ceeb6b82f541d09c29613572d", + "https://deno.land/x/deno_dom@v0.1.56/src/api.ts": "0ff5790f0a3eeecb4e00b7d8fbfa319b165962cf6d0182a65ba90f158d74f7d7", + "https://deno.land/x/deno_dom@v0.1.56/src/constructor-lock.ts": "0e7b297e8b9cf921a3b0d3a692ec5fb462c5afc47ec554292e20090b9e16b40a", + "https://deno.land/x/deno_dom@v0.1.56/src/deserialize.ts": "514953418b7ae558ed7361ad9be21013f46cba2f58bd7f4acc90cf1e89f9c8cf", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/document-fragment.ts": "0b915d094830d43b330dc2fb8012b990f2c815773c6cdcd4a9fdff99fe47412e", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/document.ts": "ad584ac4ce6dce03f0ff6ef4b7db86fd598f9c7824da1387f7f2acd7d6948e4a", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/dom-parser.ts": "ab5e8382700bc3936ac67e8d11b7bb9c6999a994ac8b070b7cae94e2d2ecee5a", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/element.ts": "9726ebf139b97ae96671d38da720988cfd6481ff9068ba13f3b0d00e72f0c8c0", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/elements/html-template-element.ts": "1707dfb4cbb145f3bcb94426d7cdedbaa336620d0afed30e99f50fe87ba24a98", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/html-collection.ts": "dcf328e883877f7748d3e20fb6319e739f297a6e24f4b00ec5b1a2f390cfa3c6", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/node-list.ts": "be9793475d82539da8b97a17b6b5538cc723538c10cc5820a23e5e4b2248845d", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/node.ts": "53ada9e4b2ae21f10f5941ff257ed4585920ae392020544648f349c05d15d30c", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/selectors/custom-api.ts": "852696bd58e534bc41bd3be9e2250b60b67cd95fd28ed16b1deff1d548531a71", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/selectors/nwsapi-types.ts": "c43b36c36acc5d32caabaa54fda8c9d239b2b0fcbce9a28efb93c84aa1021698", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/selectors/nwsapi.js": "985d7d8fc1eabbb88946b47a1c44c1b2d4aa79ff23c21424219f1528fa27a2ff", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/selectors/selectors.ts": "83eab57be2290fb48e3130533448c93c6c61239f2a2f3b85f1917f80ca0fdc75", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/selectors/sizzle-types.ts": "78149e2502409989ce861ed636b813b059e16bc267bb543e7c2b26ef43e4798b", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/selectors/sizzle.js": "c3aed60c1045a106d8e546ac2f85cc82e65f62d9af2f8f515210b9212286682a", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/string-cache.ts": "8e935804f7bac244cc70cec90a28c9f6d30fea14c61c2c4ea48fca274376d786", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/utils-types.ts": "96db30e3e4a75b194201bb9fa30988215da7f91b380fca6a5143e51ece2a8436", + "https://deno.land/x/deno_dom@v0.1.56/src/dom/utils.ts": "bc429635e9204051ba1ecc1b212031b5ee7c6bcd95120c91bef696804aa67e74", + "https://deno.land/x/deno_dom@v0.1.56/src/parser.ts": "e06b2300d693e6ae7564e53dfa5c9a9e97fdb8c044c39c52c8b93b5d60860be3", + "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6", + "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4", + "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d", + "https://deno.land/x/esbuild@v0.25.10/mod.js": "1f4e77a22600b008d4ae918043f2fe60fffbc185b475110880eeb6bd4ebc29fe", + "https://deno.land/x/lume@v3.0.11/cli.ts": "fc1acd64c7e4c1e148b4ee8f424594f2aa39f6867a1c3ae0358248a1d4d5cafd", + "https://deno.land/x/lume@v3.0.11/cli/build.ts": "fe8c45d8af369d719fb7378df09387abec21acffa37d04f1129d80d5b20ec1e8", + "https://deno.land/x/lume@v3.0.11/cli/build_worker.ts": "41abe46d5f0191a0beaa52eb88e8b7261b0502f124e489a53361b9bde91bd5f6", + "https://deno.land/x/lume@v3.0.11/cli/cms.ts": "7f3f46c3353661a7679926d0ddcfe3e596f3c97ad2de7f535bde5906e42c3f5a", + "https://deno.land/x/lume@v3.0.11/cli/create.ts": "db576c8cd3aa5a27685c6283573983be59de0be7ec404a9f815b24d4ea61c639", + "https://deno.land/x/lume@v3.0.11/cli/missing_worker_apis.ts": "70625ded7fee5de7d215e0829ce8dc4bb7060f6a496c09db880ebaec8b3efb92", + "https://deno.land/x/lume@v3.0.11/cli/run.ts": "27e7c84c2bcadc3aa4ca4fbad02330f33000dca9a2ef41780bad3676606bc029", + "https://deno.land/x/lume@v3.0.11/cli/upgrade.ts": "a11e7c9024f78c2e7376c57b4a99e389dbf490769779d2d37a4a3ccd6ef27d9e", + "https://deno.land/x/lume@v3.0.11/cli/utils.ts": "71e1ee512aa630cf4b2b3ddd646f1ef5f20b43b538d396ad4e27128f7a8439c3", + "https://deno.land/x/lume@v3.0.11/core/cache.ts": "bec945853a7111babf1fb465090d84dbf4176af0ad465b63b51031134ad6ea2a", + "https://deno.land/x/lume@v3.0.11/core/components.ts": "e5b0d2aca8e630735534a4cb781802fe9c194c3be4e1010c0abe73617c607d84", + "https://deno.land/x/lume@v3.0.11/core/data_loader.ts": "8698a9e9b1aac27147dc835ba89a0e30828c81338eceae86630607d78f146215", + "https://deno.land/x/lume@v3.0.11/core/debugbar.ts": "77d23362c29e69a8f42b8fc13d6129e62ac6ecf339bc0ac4739082c800b44f58", + "https://deno.land/x/lume@v3.0.11/core/events.ts": "e4fd1786eb7dd4a041d7d922779b9edf1ee89e51fd17ba5e756f380879ccb557", + "https://deno.land/x/lume@v3.0.11/core/file.ts": "5836010f28967a83fb02d721e78cfea1f2d125b4349204a8a3d04ac62070c127", + "https://deno.land/x/lume@v3.0.11/core/formats.ts": "e65130e5c5f2e49435619479710c812199b480a9e145fdc6b2bac11cfe6ea08e", + "https://deno.land/x/lume@v3.0.11/core/fs.ts": "ad0b1eb43361f76f36674505ef6b8870176ef386c43ee962e6c750506b40a071", + "https://deno.land/x/lume@v3.0.11/core/loaders/binary.ts": "bb1e1cf3faac49f6007dc6814168dc0f633da17356db18e68862e4b2a87a3f33", + "https://deno.land/x/lume@v3.0.11/core/loaders/json.ts": "ae28e711196215ca2772e9e31f2646ff4c3cf3f66ae75bf8cbcab94de5dbd24f", + "https://deno.land/x/lume@v3.0.11/core/loaders/module.ts": "abcb210fa6724b83407407cd0f7ef90462b35a2017bc135a3d124dd7f38843f6", + "https://deno.land/x/lume@v3.0.11/core/loaders/text.ts": "42860fc3482651fa6cfba18a734bb548d6e6e1163bf1015c2abc447ab150acbd", + "https://deno.land/x/lume@v3.0.11/core/loaders/toml.ts": "72ddfef2deea62815c28e27faa2c5356e09b3109e9547e47a6defea3d3332452", + "https://deno.land/x/lume@v3.0.11/core/loaders/yaml.ts": "241dc41fbe51b92e38dc748eda614c35d80fb8c63a6d40253453c6bb78c9c47e", + "https://deno.land/x/lume@v3.0.11/core/processors.ts": "047a87b0c9a0377ef15daaf1b671a29d541e4bb744c152f02a5c4f0a80fbbb64", + "https://deno.land/x/lume@v3.0.11/core/renderer.ts": "8c69046aa0fdc51fddbbd36c02aeb9b2226a5853f4ae8aeb549c17c43af13e88", + "https://deno.land/x/lume@v3.0.11/core/scopes.ts": "dbdf93d7a9cead84833779e974f190b1379356ec7c0ccd34aa92f917c2cdd2f9", + "https://deno.land/x/lume@v3.0.11/core/scripts.ts": "286969b120d2290ba57a7fdd9b37e587aacf4e4162d92f51f1f1e9e18c864f30", + "https://deno.land/x/lume@v3.0.11/core/searcher.ts": "19530e0149ca925334f98052863a52cdfbbeea9977342b209829999a34e816a6", + "https://deno.land/x/lume@v3.0.11/core/server.ts": "9378853e85df284ecbf23fb846cb106aa6ce7b1673a141f1920851f52904faef", + "https://deno.land/x/lume@v3.0.11/core/site.ts": "20c30402f81d7fe072fd5f385617198b0473cb20df49404c63929fbf210e616c", + "https://deno.land/x/lume@v3.0.11/core/source.ts": "d4dbe91058369ffaf23778da7e8d8287234f3901eed378accb7933e76529a216", + "https://deno.land/x/lume@v3.0.11/core/utils/browsers.ts": "f668e129d6b969663f736d1759b0544577e0ea609727d1009bf8d6e3ec9eef3e", + "https://deno.land/x/lume@v3.0.11/core/utils/cdn.ts": "93c528ff31f7e087ab0d62d09d2b4cf5bdcc7756853a6fa96da15f58016123c9", + "https://deno.land/x/lume@v3.0.11/core/utils/cli_options.ts": "c1fd59592064e76ce71c4d65d7c0de6cafee836fbd8eb42717cc68f56ed36b8a", + "https://deno.land/x/lume@v3.0.11/core/utils/concurrent.ts": "cb0775b3d95f3faa356aa3a3e489dccef8807ed93cc4f84fcf5bc81e87c29504", + "https://deno.land/x/lume@v3.0.11/core/utils/date.ts": "3eb0b0e2ea15a95cdfe737be70cd4f48cbe49401928cb04c25a230f411ab2478", + "https://deno.land/x/lume@v3.0.11/core/utils/digest.ts": "fcd15e7666fb28989b7d5b23c111044cfa71456fa39e26090ee88ac517ee4336", + "https://deno.land/x/lume@v3.0.11/core/utils/dom.ts": "fffb0c0c3ae613282e0447c3e4c122a62f44c776771d525a0ca09759883b4b9e", + "https://deno.land/x/lume@v3.0.11/core/utils/env.ts": "9d0d859303e8cb799d122088f077c54b85258763f2541313be3bf66b58ce33a3", + "https://deno.land/x/lume@v3.0.11/core/utils/format.ts": "bad71315eefd5ad0413841bbe5e8406d636d58d3ed3ef48674655b3a21a0aab0", + "https://deno.land/x/lume@v3.0.11/core/utils/generator.ts": "1e664e9fd4c469e38a0acf5c94fd49dac4f38cb6334563ea4b7fc498b5958877", + "https://deno.land/x/lume@v3.0.11/core/utils/log.ts": "9652d9b7a78fa61d667b6749a35ea02a00927b541d6d4d72e7f3de1881101bde", + "https://deno.land/x/lume@v3.0.11/core/utils/lume_config.ts": "854c9d037e12e50804266717a462f924855fe2b0a819ceed4950414c4f70c5ac", + "https://deno.land/x/lume@v3.0.11/core/utils/lume_version.ts": "c1c63818097e4a273183429ab5b2446a253307f7bc2d0d6361a17b4f230a617d", + "https://deno.land/x/lume@v3.0.11/core/utils/merge_data.ts": "8433920c7e66f27ae558777ed9add637f8c2f67adf9ca2c9ca60d566b9b3583f", + "https://deno.land/x/lume@v3.0.11/core/utils/net.ts": "d0d58c95668effc13669015c219295532f67e4a02396286308c772871b615a9b", + "https://deno.land/x/lume@v3.0.11/core/utils/object.ts": "9b2d1c20503137b612fcbb311b42d1f5500ae170b68f1dca43cc6b057423bc6e", + "https://deno.land/x/lume@v3.0.11/core/utils/page_content.ts": "bbadb588f9d9fcf1a2af156ce4b68974dfad39b65c3c8d42a6f1895b194c7eec", + "https://deno.land/x/lume@v3.0.11/core/utils/page_date.ts": "2a3d9c203df298ca61f568fdf509945f127f990769623c3edfd753d39807b757", + "https://deno.land/x/lume@v3.0.11/core/utils/page_url.ts": "e292cba024f66d35b1b622f9ac13e0910b0ed5ce12c368bd8014e03c1bb0b063", + "https://deno.land/x/lume@v3.0.11/core/utils/path.ts": "7a1d199113928cc35782aa3262cbe6f7a4894bc262d7d300de9385b3da45602f", + "https://deno.land/x/lume@v3.0.11/core/utils/read.ts": "9a0c87095be118bb599eeebba78613d5b3b1cf6c51c3b4c8016af45675bc7303", + "https://deno.land/x/lume@v3.0.11/core/utils/tokens.ts": "201777343e716403bfb1dbbc1a988a85b8d3f12699daaacbe8bbdc3c352a57ff", + "https://deno.land/x/lume@v3.0.11/core/watcher.ts": "9bdb33b2dac840b65545a894c6faecfd31064cc37852dc175081a2048548a89d", + "https://deno.land/x/lume@v3.0.11/core/writer.ts": "e8952538d57c0b587a3e9344b9b10d1b71274aca234b927b05a09c88ac3f4304", + "https://deno.land/x/lume@v3.0.11/deps/base64.ts": "c7b786540e9f348726e6c6f3ba00866328463f7323ebd02d91c6bc7b125e19b0", + "https://deno.land/x/lume@v3.0.11/deps/cli.ts": "3bfb2281c11151afa5f24adcbb0346390c9cb07060d7c233e4a425b31d2427ed", + "https://deno.land/x/lume@v3.0.11/deps/cliffy.ts": "faff0c2ca187ec9fd1ad8660141f85b9d05b5c36bab25b40eb5038c02590a310", + "https://deno.land/x/lume@v3.0.11/deps/colors.ts": "01c038ca4f8ad503ae0c81338223e3e9e1cbcaf0a14cc12bb6cd6c12f249df98", + "https://deno.land/x/lume@v3.0.11/deps/crypto.ts": "0939b1e974472d1db1d611b4160a5a51d796da1368289277d2e26803243959d5", + "https://deno.land/x/lume@v3.0.11/deps/debugbar.ts": "fac82ee05c8fc2c2a7686201d33fce65ffec7f35b7b7c293c7c9f563afa1aa02", + "https://deno.land/x/lume@v3.0.11/deps/deno_loader.ts": "b68402ff8b600efdbdb49565af3fba0342417e49d59bd0d1cfe89b989d5ca1c4", + "https://deno.land/x/lume@v3.0.11/deps/dom.ts": "00090db01ad1bf53d761aaad4e7deccc4841bdf4b5ddadb5e540b0c2e216a390", + "https://deno.land/x/lume@v3.0.11/deps/esbuild.ts": "d231b2f5c6559238e20896e35ebebc81fc41b37942435a43ca20ec2d1d3dbd71", + "https://deno.land/x/lume@v3.0.11/deps/front_matter.ts": "f5e5780d4a0502d50cde1f42a4aa7830756dc9bd0251ba7448cecd1eaa60878f", + "https://deno.land/x/lume@v3.0.11/deps/fs.ts": "95fbbfe19afd419842d0c36f0f480f1ec0e027a3310a389ef79e6cf2baf5aa27", + "https://deno.land/x/lume@v3.0.11/deps/hex.ts": "828718f24a780ff3ade8d0a8a5b57497cb31c257560ef12af99b6eb1a31e3bbd", + "https://deno.land/x/lume@v3.0.11/deps/http.ts": "8b0a9e00933c3506461596d94447e7fdc16dcbe93029a57cbda446207af22b4b", + "https://deno.land/x/lume@v3.0.11/deps/init.ts": "05d45af66ebdfe63e43540618f51ece8f99d98dc49de890f10eeb43abe9ed0f3", + "https://deno.land/x/lume@v3.0.11/deps/jsonc.ts": "79f0eddc3c9e593310eb8e5918eb1506b1c7d7816e4ecb96894f634ecbe626ff", + "https://deno.land/x/lume@v3.0.11/deps/lightningcss.ts": "5f5167c6eb306ef759f0043f8f33f2eaf63c69210aa1aa837505e990ee619c46", + "https://deno.land/x/lume@v3.0.11/deps/markdown_it.ts": "24c1c0fd18c99b9067d9ff5d051f934cb7c3446e6afbad934f6268af8d1ceb4d", + "https://deno.land/x/lume@v3.0.11/deps/path.ts": "f970dec772314a3ffbe672329eeb398e3eb66e875581c62af6db80c41d093aea", + "https://deno.land/x/lume@v3.0.11/deps/postcss.ts": "7ad7485032d40a2bc7977eb71d9431df8d52b53d61153e60b38c29d1c98cb870", + "https://deno.land/x/lume@v3.0.11/deps/purgecss.ts": "524914d3311933aaeb8052c89c361efdc4656bf6fb8aeec9d1b3d7750dbf8d3f", + "https://deno.land/x/lume@v3.0.11/deps/semver.ts": "f850a82a6cf8551e9b927d77f34218961027b1cbe44f3a88c387df62267e31f2", + "https://deno.land/x/lume@v3.0.11/deps/toml.ts": "32830bda333eaf4f1c3d79e4306ba449c17a85b25f94aae9b327d3790a2d1dea", + "https://deno.land/x/lume@v3.0.11/deps/vento.ts": "78db4022ee124fbcfd84caeb6c5a70f2c1e1706ec9f6415d0f1fe2e9aabcba2b", + "https://deno.land/x/lume@v3.0.11/deps/yaml.ts": "a639f4fc44ddcfc87f35e38980bbe9fc8101bf8ce34867522e76cc13cb156611", + "https://deno.land/x/lume@v3.0.11/lint.ts": "23cf68a7cc17edfdb16f2e905de3c5d5a1da541638f04fb8f7d5c762288f2c52", + "https://deno.land/x/lume@v3.0.11/middlewares/basic_auth.ts": "c18f0da9f88be4581e5e3da99214fd7abdad829ab00dbdd2fb3116f1f876add2", + "https://deno.land/x/lume@v3.0.11/middlewares/logger.ts": "c96f1a9f9d5757555b6f141865ce8551ac176f90c8ee3e9ad797b2b400a9a567", + "https://deno.land/x/lume@v3.0.11/middlewares/no_cache.ts": "0119e3ae3a596ab12c42df693b93e5b03dd9608e289d862242751a9739438f35", + "https://deno.land/x/lume@v3.0.11/middlewares/no_cors.ts": "4d24619b5373c98bcc3baf404db47ba088c87ac8538ea1784e58d197b81d4d02", + "https://deno.land/x/lume@v3.0.11/middlewares/not_found.ts": "0f92cd91239444247a1c3dce1bed4e978445687ca76f544a0ccd483a352f761a", + "https://deno.land/x/lume@v3.0.11/middlewares/reload.ts": "1f81573c7574d09bd0dbdf3dd107b98afbdcc63f9aa90edec8f8184157bbe677", + "https://deno.land/x/lume@v3.0.11/middlewares/reload_client.js": "9026da20a25fe58ad36233539ada3f38d56d935c5b0c1c69b7fcd21511efadee", + "https://deno.land/x/lume@v3.0.11/mod.ts": "349b3b7fe199bc6703b1e1fb77f3ab92699fce966da2261d3419a951b1ef2c5d", + "https://deno.land/x/lume@v3.0.11/plugins/esbuild.ts": "6f0a5684a7600b836b318d622ebbf77e4a3a7fa6f5ca366fddcea37871b5f770", + "https://deno.land/x/lume@v3.0.11/plugins/json.ts": "5c49499e56b919ec848d4118ec97dd4fe0a323a6cc4c648dc45ab55297614c12", + "https://deno.land/x/lume@v3.0.11/plugins/lume_cms.ts": "c7375fdc097fcd3edca2cfd05c34dbefd78ec03b5a8569a33c81fdfc6458a5d5", + "https://deno.land/x/lume@v3.0.11/plugins/markdown.ts": "7e82d897c1e35bf119dcd18b6aec7a6ba5aa06848897b34ff9cd161ec7c8757e", + "https://deno.land/x/lume@v3.0.11/plugins/modules.ts": "4e177c0ffe972b9deef10db2bf0ae52b405418af4dbac03db9e7ffbd6a3ec6ae", + "https://deno.land/x/lume@v3.0.11/plugins/paginate.ts": "6a1a9a24d0fabed2f722a6a6f29d98559219c69475685034181816e82d367f2e", + "https://deno.land/x/lume@v3.0.11/plugins/postcss.ts": "5a45a017b1385bb6cc30832d7de21d7d7d146a6c6e037f48446b33e36deafbd3", + "https://deno.land/x/lume@v3.0.11/plugins/purgecss.ts": "c90d540903b589b3a700196bbc05c9a428c91abaea7238e41f6fcd8f38dcd2d4", + "https://deno.land/x/lume@v3.0.11/plugins/search.ts": "5acb5be828bbbd012fb9226cb97ec3e370d43d05aa44d16e7e7d50bab368b442", + "https://deno.land/x/lume@v3.0.11/plugins/source_maps.ts": "10afd5d8617003ed68db9895bc13e57d1742697fa55657e27efd535da6e52c34", + "https://deno.land/x/lume@v3.0.11/plugins/toml.ts": "e5bf35ed4915587acd453f002b00ae9b88c1782cadc25c703d7642a390af43ea", + "https://deno.land/x/lume@v3.0.11/plugins/url.ts": "15f2e80b6fcbf86f8795a3676b8d533bab003ac016ff127e58165a6ac3bffc1a", + "https://deno.land/x/lume@v3.0.11/plugins/vento.ts": "fd60ee80435994bcf88b2cda9c51eaed0ba49a2363f42920675f2d5a0a4a6ab2", + "https://deno.land/x/lume@v3.0.11/plugins/yaml.ts": "d0ebf37c38648172c6b95c502753a3edf60278ab4f6a063f3ca00f31e0dd90cc", + "https://deno.land/x/lume_init@v0.5.6/deps.ts": "49352263d13ebd8296f53470b177c0fd1e67f7642b84b8fe5e19248c3382c998", + "https://deno.land/x/lume_init@v0.5.6/init.ts": "8c530b8e1401e516e4b1f4c3d7e545a33cdb0fbc81f3539ef2bf14ade12eca90", + "https://deno.land/x/lume_init@v0.5.6/mod.ts": "33ef6609ada31cbd25ffa362176c906bc9184b2a03080cfaf04c5dd5b0061443", + "https://deno.land/x/lume_init@v0.5.6/steps/cms.ts": "97e37fef3b04122a512c37859f1f2c0ef58cde0557852f5da86de51acce7592d", + "https://deno.land/x/lume_init@v0.5.6/steps/git.ts": "e55e7857ed89685724555707b9cdc2897ec7bd535644be3f1935c2030331a316", + "https://deno.land/x/lume_init@v0.5.6/steps/load.ts": "82ce37a99d1fd182c355692c7ec91f72658e81172b11bca0829e4ae4da58cdfb", + "https://deno.land/x/lume_init@v0.5.6/steps/lume.ts": "24e23abb2680fa66023434936852f3b927c9d7f34b981a482dc77f5fa963c994", + "https://deno.land/x/lume_init@v0.5.6/steps/plugins.ts": "3b4ea954420c31ae1c07ee5a509aef5433a9b75d67f72661a38cb8f338f94006", + "https://deno.land/x/lume_init@v0.5.6/steps/save.ts": "91b033d3a07b1cef365ce27f5dd3925c90a3923290e2120eaec1c1c79bbc34c2", + "https://deno.land/x/lume_init@v0.5.6/steps/success.ts": "986a3cf2f1ac795f398cf04ea3fded39266fa169e87fae61b3377be1649f04be", + "https://deno.land/x/lume_init@v0.5.6/steps/themes.ts": "284da34d8bd4c7b184ef3f624475f8306b5eca63c3e4784fd268f5e64512dcd2", + "https://deno.land/x/lume_init@v0.5.6/steps/update.ts": "1271c0d6895dfb2acf1495a36676e4f1c454e9ba090a85bf88835e0756f26d3a", + "https://deno.land/x/lume_init@v0.5.6/steps/utils.ts": "6ab641763c6fec0c72ea97438f42f2a22a1c50f4124ecbdf7c20731a62d283df", + "https://deno.land/x/lume_init@v0.5.6/upgrade.ts": "0db7e96794f082391d42fd65ebe0c2cfbb5fce5d625866f1b83138909d7265e1", + "https://deno.land/x/nudd@v0.2.10/deps.ts": "3c605a52a62eefe9892b77e1b91f781562a6b8791c7c9b49ad62d0c761e18f70", + "https://deno.land/x/nudd@v0.2.10/registry/denoland.ts": "15783578eaf6a2edbf348aa1170de7ae4cc8b158ef57096263cd80c35d17ccd6", + "https://deno.land/x/nudd@v0.2.10/registry/jsdelivr.ts": "ceb3f75529a42ae1bc28564ebe13427704c572e1f8edc5bb7ef135779da2cbf0", + "https://deno.land/x/nudd@v0.2.10/registry/utils.ts": "b7aeec1113f0408a9339938662cb24b5404e1ab67ddbfd59b3f9fd60414ba1b2", + "https://deno.land/x/ssx@v0.1.12/jsx-runtime.ts": "a334a1ee3a25de7f3b84b7b8d842bcae40e9116f6edb6ec76cb265712c8a2ab8", + "https://deno.land/x/vento@v2.1.1/core/environment.ts": "36f3e145adfe1af3740cfcfc6ff237d6fe48225d3627123b17022251afbe3074", + "https://deno.land/x/vento@v2.1.1/core/errors.ts": "8606b682b465d598a394feea135dd2f84033b5ef2a61a23d116ccb782a0a547a", + "https://deno.land/x/vento@v2.1.1/core/js.ts": "83084240150d7e8b83e43ec8fcf78564a8ba8599c3d517976efbb11b208903b2", + "https://deno.land/x/vento@v2.1.1/core/reserved.ts": "e3ccafb4e5524b9c51fa14fa0e4bf17fd9bdd791848f17afa2aeb97835c486d1", + "https://deno.land/x/vento@v2.1.1/core/tokenizer.ts": "460faa3de0e561e5046c46528c8bcbfc46b9de576af7ec8c4a8954a61a80ec76", + "https://deno.land/x/vento@v2.1.1/loaders/file.ts": "83f579ac39838642bb45c6ddf48c05c08134cdd95fa7364d004fb443e972196a", + "https://deno.land/x/vento@v2.1.1/mod.ts": "26081287509a87d81c51917795ff94ea80056c43599af8140bb5f666f0aaa767", + "https://deno.land/x/vento@v2.1.1/plugins/auto_trim.ts": "e244c3721c555ef3a247670c01c60dd22fca2ae40a0ce872672d7768e9e9c876", + "https://deno.land/x/vento@v2.1.1/plugins/echo.ts": "5672b58a399ca1475953e32c5282321f7430668bbe963ec40c0a6949c3ec2116", + "https://deno.land/x/vento@v2.1.1/plugins/escape.ts": "1db6eb7b22f88bbf94d21636c9b156ec4868e10bbcfa7dc5eae7113c36ef7b8a", + "https://deno.land/x/vento@v2.1.1/plugins/export.ts": "c1f25c8e1e35027cc21e780f8fb3961febec4771463eb262959fbf035b8b8a63", + "https://deno.land/x/vento@v2.1.1/plugins/for.ts": "a893a3de2c5cab43d5018cb80718ea46377c172d550e7a8ead076f00c58d59bf", + "https://deno.land/x/vento@v2.1.1/plugins/function.ts": "73cec242bb09c840ae2065fbdc2a9a03c4d20a7b996bded387e61e26b68e6171", + "https://deno.land/x/vento@v2.1.1/plugins/if.ts": "db13776a5b9e7988f65c27943c344bec7463c5f3162a06cc4da1a313d7109195", + "https://deno.land/x/vento@v2.1.1/plugins/import.ts": "4555c60768fc061f16c61281c5c6bf163cc5899106d95cdcddde080493372920", + "https://deno.land/x/vento@v2.1.1/plugins/include.ts": "929d35733f73e68068f22378b69195b49a111fcaa39c306017f40028e76b1532", + "https://deno.land/x/vento@v2.1.1/plugins/js.ts": "f5a7fd8cc48cb1b1dc0b396e3d1fee4664cf28932dc5f5f301e38243e0e987dc", + "https://deno.land/x/vento@v2.1.1/plugins/layout.ts": "0cfdccbd069eef61c013a6660cb265afd36e3931ff9051cc70e3133ee74dad64", + "https://deno.land/x/vento@v2.1.1/plugins/mod.ts": "017d5bb3e3c80b7f67271cdf8779686f55916070c5d168a143e6a37c35bcd731", + "https://deno.land/x/vento@v2.1.1/plugins/set.ts": "cf9dfbf68b52039781fd86ec0b9587a8bcd486fdef9f08989719cfdb7fa233d0", + "https://deno.land/x/vento@v2.1.1/plugins/trim.ts": "8d33271327b09ffd8f569ebde85125b1324fa9538a54d6072ac97a9fb5d24264", + "https://deno.land/x/vento@v2.1.1/plugins/unescape.ts": "1c56f0310c7757880df7684fc6b7bf9efd27fdb6b929b89626802f5a99cb93ee", + "https://esm.sh/@borewit/text-codec@0.1.1/denonext/text-codec.mjs": "ea8f41f92f2798340cf2b568602fc1e6f9957e8192265d92d6d06725d0bbfcff", + "https://esm.sh/@borewit/text-codec@0.1.1?target=denonext": "a3d25ab29c470e2e6909ba99905ec372fa66bcc46dd22732dc80b6871603548c", + "https://esm.sh/@borewit/text-codec@0.2.0/denonext/text-codec.mjs": "1fa7af74bcd1b8c1a460836447074360915a752cffe0dbdb8ac472f422a7fbd9", + "https://esm.sh/@borewit/text-codec@0.2.0?target=denonext": "eeee292364426dcdf670db8c94fab465a2308318f20be285ad500336bbc83e5c", + "https://esm.sh/@tokenizer/http@0.9.2/denonext/lib/http-client.mjs": "38999713979a80886994c1ed9b0e20936d019aee9d239b736f7a8bd857b98e1c", + "https://esm.sh/@tokenizer/http@0.9.2/lib/http-client.js": "8b84ffc15c9fe43835c1083d10940ed4397f940c3c2d04d2c9193059aff60964", + "https://esm.sh/@tokenizer/inflate@0.2.7/denonext/inflate.mjs": "ba141d0e59f11c19e2012b5b1fdd3f23f2514495e700fa3013db051e20863f91", + "https://esm.sh/@tokenizer/inflate@0.2.7?target=denonext": "bbc80d9e77f0da6ce0d1adef82539261020623b044410c2a52570bf15b02afe2", + "https://esm.sh/@tokenizer/range@0.12.0/denonext/range.mjs": "7e1e470c638f4e654ce1244e6b256f14b108148273987cbfb8736faa89b30f1b", + "https://esm.sh/@tokenizer/range@0.12.0?target=denonext": "10d3d7b112467d490e1743263d5dba231c069eba0b66f7a2c683d4d64334e19b", + "https://esm.sh/@tokenizer/range@0.13.0/denonext/range.mjs": "da9decedb2697a2dadc4293c2d99fb7de8eb262599a124590f26e62e692008b9", + "https://esm.sh/@tokenizer/range@0.13.0/lib/index.js": "a2b7e610d108dd0dd02da635fc42a4c14cd7a71d52e3704b7fc8e99653cb6416", + "https://esm.sh/content-type@1.0.5/denonext/content-type.mjs": "6fd962b99a80821956a22379aded6b7ca37cfbf95be5639047437e03211e1b56", + "https://esm.sh/content-type@1.0.5?target=denonext": "9eb28c5db95297f2dc51214da829cd8d7b7f3d7bf17dcbf2f60f300173ca8343", + "https://esm.sh/debug@4.4.3/denonext/debug.mjs": "fc4bb8fd2502bf87537c57f5c7c3213eb0258040361828a03970ba7d0933c516", + "https://esm.sh/debug@4.4.3?target=denonext": "9df8b8806c13297f96185899110f8146b8b0c7944847c8392cbfb4e730a634dd", + "https://esm.sh/fflate@0.8.2/denonext/fflate.mjs": "51759ec52e8522bbcd6dce941956a878c733975dcc159618c683c659270a59d3", + "https://esm.sh/fflate@0.8.2?target=denonext": "e0cdd268850b4eac9ef9f188a3b4c5c9dd3ebd0f66c4006a13911b2a0c0512bb", + "https://esm.sh/file-type@21.0.0/denonext/file-type.mjs": "ef71de29ef71c13095f6ce3fd63288a201dbba91cd70d2342436956b9e07d6e9", + "https://esm.sh/file-type@21.0.0?target=denonext": "e797e823999c60b6447441f60631803faee3d9c48baf948dfff9d75b8aae2361", + "https://esm.sh/ieee754@1.2.1/denonext/ieee754.mjs": "6e87635f6124ae21ea355a521f021a04afc277248bd5097dc85edce4d7017f86", + "https://esm.sh/ieee754@1.2.1?target=denonext": "b8839b7562d93529fd58ec105fa4230b816e3dd0a2dd35e3d3b07cae2f16ae97", + "https://esm.sh/media-typer@1.1.0/denonext/media-typer.mjs": "3dc6174765452b8271cad4031a308c8015c67bee4de2b0aff5daec36cb1b766e", + "https://esm.sh/media-typer@1.1.0?target=denonext": "8d2581e00eea8816b1bd4a698ad68e928402b462e697077e9fffb1e960e25d50", + "https://esm.sh/ms@2.1.3/denonext/ms.mjs": "9039464da1f4ae1c2042742d335c82556c048bbe49449b5d0cd5198193afa147", + "https://esm.sh/ms@2.1.3?target=denonext": "36f5aa7503ff0ff44ce9e3155a60362d8d3ae5db8db048be5764a3a515b6a263", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/aiff/AiffParser.mjs": "9589f066b8770e6d8cc5ccd08199158a7c1a5ec8797c8af268071866604a1852", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/apev2/APEv2Parser.mjs": "3595d619cef291dd82f23dd8045b91e995b448fa6727c9f9a802be1f496584e4", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/asf/AsfParser.mjs": "c7a8e7490d82eb8578d0d4f91293761f67d4217125791d2ebc730ec1318bbf85", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/core.mjs": "f8300b19edc7159ae65bbb3b15af99a52dc1067eb022a0acd60ae08e39b88961", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/dsdiff/DsdiffParser.mjs": "1f4e33d399200dbef55286a357dc8ff8e57377f37cfc999c04a6dff646663896", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/dsf/DsfParser.mjs": "eae0eac54d0638200b3cda31612559f3dcbf4d399aee6040364dd559487fea09", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/flac/FlacParser.mjs": "422777d4646ce64e0112c14f649f466b5d6fe2e74e4c582e2652edd03eb48ce0", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/matroska/MatroskaParser.mjs": "b8714d5e77c13319673db0f34a80710de8fdb1fb4047d6405d301fa4bed52ddf", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/mp4/MP4Parser.mjs": "9ab9fcae397cac31d54018ef05e81d639160c80847ed15356b85ec7937c2978a", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/mpeg/MpegParser.mjs": "9cade9c40906843ec596396352af8cb19f466fc78c081b3ba9f7135372304230", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/musepack/MusepackParser.mjs": "e9672e8603a0cd6bcbb52978082fb1cc7533cdebacf5c382f0e398c2badb86e4", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/ogg/OggParser.mjs": "a1801b61660907dec429f162e63367e048fb357cb0c83b0aa3f0cec8a8786554", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/wav/WaveParser.mjs": "99450cb1f387ed495f0880db1352e8aeef60f9f754116abd168c9fdd88d2beca", + "https://esm.sh/music-metadata@11.9.0/denonext/lib/wavpack/WavPackParser.mjs": "1e22a6a0886d48ed4d965e5f2535639bb8acbb2a992c7660f1a73c20256fc64b", + "https://esm.sh/music-metadata@11.9.0/lib/core.js": "5c5c3ef0ef7624e374ffb3cb615bd16fa35d99b67a45b6e76bd2bac6183a999f", + "https://esm.sh/peek-readable@5.4.2/denonext/peek-readable.mjs": "7e784eacb3816872f18606f82cc3f30f2d11e674c5199f84e6cf7d820a3ced23", + "https://esm.sh/peek-readable@5.4.2?target=denonext": "6e1779af5114cfd4f28e9c2d7bbc51a57aef81a958981ed4140761e616e0b8a1", + "https://esm.sh/strtok3@10.3.4/core?target=denonext": "935bc31ec3d554a733ef4dbaec81137e00d3333c81deadf9de58de4c095746d1", + "https://esm.sh/strtok3@10.3.4/denonext/core.mjs": "3183a30c522666467d6d0b5234113fe216e1ec79f4bd135002545458323d62f4", + "https://esm.sh/strtok3@10.3.4/denonext/lib/stream/AbstractStreamReader.mjs": "675c8d26bd83e2e8907a988ba996e1ca45ae10397579d83ed027c14b13e78134", + "https://esm.sh/strtok3@10.3.4/denonext/lib/stream/Errors.mjs": "1459007f24621eb036ba3be2fa592ce39381874722aab8e0302f8d6345a620d6", + "https://esm.sh/strtok3@10.3.4/denonext/lib/stream/index.mjs": "7b6b3d37331712b157f2f6108081831b574d2b6493049aa8738834c4961ae633", + "https://esm.sh/strtok3@10.3.4/denonext/strtok3.mjs": "7228ab10418d0fcee23ac018de8c4843c796471c691c9e940540a23145d589e6", + "https://esm.sh/strtok3@10.3.4?target=denonext": "529358386976a93cbb5db101cda728e9cf25ac9d7d13663ee0d714bd92e54f83", + "https://esm.sh/strtok3@9.1.1/denonext/strtok3.mjs": "72f95ec1079a343006ffc12ca7f7c39fac9f46009265e0ca90a4ea8ee1bb21f7", + "https://esm.sh/strtok3@9.1.1?target=denonext": "d72e11d3fbf73c34c4cd8662a9386d8efa0ac8f8a66c93e3a6f7111ba2761370", + "https://esm.sh/supports-color@10.2.2/denonext/supports-color.mjs": "a263c0229c209e3fed166348cd02e96f08996902c9e8e7c6de4ac50c8ee2a748", + "https://esm.sh/supports-color@10.2.2?target=denonext": "f938c90ae6175cd0ec937dabb90259b115033d9dfde267b69566089f06f92d06", + "https://esm.sh/token-types@6.1.1/denonext/token-types.mjs": "84e80406d9aed2a04e049b9629d9769d66c5665dcf0f781605e0b9a8c6276265", + "https://esm.sh/token-types@6.1.1?target=denonext": "006570441bb0e41ec43ef18fbf171264c2058298ce6511f2ad4a5f488a21312a", + "https://esm.sh/uint8array-extras@1.5.0/denonext/uint8array-extras.mjs": "660b5b7967799e1ab7274f0062b66fc33a86c68df583429d8c52fa0b32332ae9", + "https://esm.sh/uint8array-extras@1.5.0?target=denonext": "04a46e303f8dc389be14886417be8ac772550255f6801a33411511711cdb3546" + }, + "workspace": { + "dependencies": [ + "jsr:@bradenmacdonald/s3-lite-client@~0.9.4", + "jsr:@fry69/deep-diff@~0.1.10", + "jsr:@kunkun/kkrpc@0.6", + "jsr:@mary/ds-queue@~0.1.3", + "jsr:@mys/m-rpc@~0.12.2", + "jsr:@mys/worker-fn@^3.2.1", + "jsr:@okikio/transferables@^1.0.2", + "jsr:@orama/orama@^2.0.6", + "jsr:@std/fs@^1.0.19", + "jsr:@std/path@^1.1.2", + "jsr:@vicary/debounce-microtask@~0.1.8", + "npm:98.css@~0.1.21", + "npm:@atcute/lex-cli@^2.3.1", + "npm:@atcute/lexicons@^1.2.2", + "npm:@js-temporal/polyfill@~0.5.1", + "npm:@phosphor-icons/web@^2.1.2", + "npm:alien-signals@3", + "npm:esbuild-plugins-node-modules-polyfill@^1.7.1", + "npm:fast-average-color@^9.5.0", + "npm:idb-keyval@^6.2.2", + "npm:lit-html@^3.3.1", + "npm:morphdom@^2.7.7", + "npm:query-string@^9.3.1", + "npm:subsonic-api@^3.2.0", + "npm:throttle-debounce@^5.0.2", + "npm:uint8arrays@^5.1.0", + "npm:uri-js@^4.4.1", + "npm:webamp@^2.2.0", + "npm:xxh32@^2.0.5" + ] + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..aa335663d --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,64 @@ +# Architecture + +## Components / Elements + +Diffuse provides a set of custom (DOM) elements (aka. web components) that can be combined into a working audio player or media browser living in a web view. + +There are various categories of these elements, and may have subcategories. Each element lives in its own directory, containing the code, documentation and other assets associated with that element. The directory for the elements is `src/components/`, each subdirectory is a category which in turn may have one or more subcategories. If a directory has an `element.js` then that directory represents the element, not a subcategory. + +These elements are available in the build as well, under the same `components` directory. These will be built as code-splitted bundled ESNext Javascript. They are listed on the `elements/` page with links to each Javascript file that defines that custom element. + +A few examples: +- `src/components/engine/audio/element.js`: Category = engine, element = audio +- `src/components/output/polymorphic/indexed-db/element.js`: Category = output, subcategory = polymorphic, element = indexed-db +- `src/components/transformer/output/refiner/default/element.js`: Category = transformer, subcategory 1 = output, subcategory 2 = refiner, element = default + +Most categories of components are independent, they do not rely on other components. There are a few exceptions: + +- The category named 'orchestrator'. These are compositions of other components, meaning they consume other components. These have attributes ending with `-selector` which is a DOM selector that points at the component that is the dependency. +- The category named 'configurator'. These take other components as DOM children with the same actions/methods as the configurator itself. They serve to delegate or combine. + +A lot of components will have a worker (located in `worker.js` besides the `element.js`) which is connected to the element using `this.workerProxy()` in the constructor. The proxy itself may be stored on the class instance, but it is usually not required. + +Components share state with each other using signals (see `common/signal.js`) or using the worker. When using signals, do not expose the signal setter, unless it's absolutely necessary. The preferred format is `element.signalGetter()` + + + +## Definitions + +`lexicons/` are atproto lexicons, JSON schemas that describe data in the system. The generated TypeScript types live in `src/definitions/`. + + + +## Facets + +These are pieces of HTML that represent either an "interface" or a "feature" (called "interactive" or "prelude" facets respectively). They are loaded using the facet loader that lives in the `l` directory. + +Interface facets can be loaded from the default Diffuse set, the user's local data cache, an HTTPS URL or an [AT Protocol](https://atproto.com) URI. + +Each time an interface facet is loaded, all enabled feature facets from the user's local data cache are loaded too beforehand. The default set of facets lives in the `facets` directory, and a list of these can be found in `_data/facets.json`. + +Note that every interface facet MUST remove the loading animation using `foundation.ready()` otherwise the loading animation will stay visible, and ideally it would set the document title as well using `foundation.setup({ title })`. + +The loader injects the HTML into a `
` element in the body. Facets don't have a doctype, a `` element, or a `` element. + +Finally, note that the loader page configures a `` element, every relative url becomes relative to the root of the Diffuse build. Every Diffuse page defines an import map which specifies `~` as the root. + + + +## Foundation + +This is a default configuration of the Diffuse elements. It is used throughout the default set of facets. It configures the elements to be part of the `facets` group, though this can be overriden using the `group` url query parameter. + +A lot of the elements are written so that when the `group` attribute is set, it will create a broadcast channel to communicate within the group and/or create a shared worker. + + + +## Other directories + +- `src/common`: Common Javascript code shared by various components and/or pages. +- `src/styles`: Common CSS shared by pages or facets. +- `src/vendor`: Vendored Javascript to be used in facets. +- `src/oauth/callback`: Generic callback page to handle OAuth redirects. +- `src/favicons`, `src/fonts`, `src/images` are binary assets for facets and pages. +- `src/_components` and `src/_includes` are templates used in `.vto` templates. Part of the static Lume site. diff --git a/elm.json b/elm.json deleted file mode 100644 index 779122f5c..000000000 --- a/elm.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "type": "application", - "source-directories": ["src/Core", "src/Library"], - "elm-version": "0.19.1", - "dependencies": { - "direct": { - "FabienHenon/elm-infinite-list-view": "3.2.0", - "Fresheyeball/elm-return": "7.1.0", - "Gizra/elm-all-set": "1.0.1", - "Gizra/elm-debouncer": "2.0.0", - "Herteby/enum": "1.0.1", - "NoRedInk/elm-json-decode-pipeline": "1.0.1", - "arturopala/elm-monocle": "2.2.0", - "avh4/elm-color": "1.0.0", - "elm/browser": "1.0.2", - "elm/core": "1.0.5", - "elm/file": "1.0.5", - "elm/html": "1.0.0", - "elm/http": "2.0.0", - "elm/json": "1.1.3", - "elm/random": "1.0.0", - "elm/regex": "1.0.0", - "elm/svg": "1.0.1", - "elm/time": "1.0.0", - "elm/url": "1.0.0", - "elm/virtual-dom": "1.0.3", - "elm-community/dict-extra": "2.4.0", - "elm-community/html-extra": "3.4.0", - "elm-community/list-extra": "8.7.0", - "elm-community/maybe-extra": "5.3.0", - "elm-explorations/markdown": "1.0.0", - "icidasset/elm-binary": "2.1.0", - "icidasset/elm-material-icons": "11.0.0", - "icidasset/elm-sha": "2.0.2", - "jinjor/elm-xml-parser": "2.0.0", - "jzxhuang/http-extras": "2.1.0", - "lobanov/elm-taskport": "2.0.1", - "mpizenberg/elm-pointer-events": "4.0.2", - "newlandsvalley/elm-binary-base64": "1.0.3", - "noahzgordon/elm-color-extra": "1.0.2", - "ohanhi/keyboard": "2.0.1", - "prozacchiwawa/elm-urlbase64": "1.0.6", - "robinheghan/murmur3": "1.0.0", - "rtfeldman/elm-hex": "1.0.0", - "ryan-haskell/date-format": "1.0.0", - "truqu/elm-base64": "2.0.4", - "truqu/elm-md5": "1.1.0", - "wernerdegroot/listzipper": "4.0.0", - "ymtszw/elm-xml-decode": "3.2.2" - }, - "indirect": { - "elm/bytes": "1.0.8", - "elm/parser": "1.1.0", - "fredcy/elm-parseint": "2.0.1", - "miniBill/elm-xml-parser": "1.0.1", - "pzp1997/assoc-list": "1.0.0", - "zwilias/elm-utf-tools": "2.0.1" - } - }, - "test-dependencies": { - "direct": {}, - "indirect": {} - } -} diff --git a/gren.json b/gren.json deleted file mode 100644 index 4b56e6f9e..000000000 --- a/gren.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "type": "application", - "platform": "node", - "source-directories": ["src/Static", "system/Build/"], - "gren-version": "0.3.0", - "dependencies": { - "direct": { - "gren-lang/core": "4.0.1", - "gren-lang/node": "3.0.1", - "icidasset/html-gren": "4.1.0", - "icidasset/markdown-gren": "3.1.0", - "icidasset/shikensu-gren": "5.1.0" - }, - "indirect": { - "gren-lang/parser": "3.0.1", - "gren-lang/url": "3.0.0" - } - } -} diff --git a/lexicon.config.js b/lexicon.config.js new file mode 100644 index 000000000..e6ec3e9a2 --- /dev/null +++ b/lexicon.config.js @@ -0,0 +1,8 @@ +import { defineLexiconConfig } from "@atcute/lex-cli"; + +export default defineLexiconConfig({ + generate: { + files: ["lexicons/**/*.json"], + outdir: "src/definitions/", + }, +}); \ No newline at end of file diff --git a/lexicons/output/collaboration.json b/lexicons/output/collaboration.json new file mode 100644 index 000000000..1745053a3 --- /dev/null +++ b/lexicons/output/collaboration.json @@ -0,0 +1,48 @@ +{ + "lexicon": 1, + "id": "sh.diffuse.output.collaboration", + "defs": { + "main": { + "type": "record", + "record": { + "type": "object", + "required": ["id", "collaborators", "subject"], + "properties": { + "id": { "type": "string" }, + "collaborators": { + "type": "array", + "items": { "type": "ref", "ref": "#collaborator" } + }, + "createdAt": { "type": "string", "format": "datetime" }, + "subject": { + "type": "union", + "refs": ["#playlist"] + }, + "updatedAt": { "type": "string", "format": "datetime" } + } + } + }, + "collaborator": { + "type": "object", + "required": ["id", "hint"], + "properties": { + "id": { "type": "string" }, + "hint": { + "type": "string", + "description": "Hint at where this collaborator resides and/or what the id represents." + } + } + }, + "playlist": { + "type": "object", + "required": ["kind", "playlist"], + "properties": { + "kind": { "type": "string", "const": "playlist" }, + "playlist": { + "type": "string", + "description": "The name of the playlist, matches the `playlist` property of a playlist item" + } + } + } + } +} diff --git a/lexicons/output/facet.json b/lexicons/output/facet.json new file mode 100644 index 000000000..4ad228029 --- /dev/null +++ b/lexicons/output/facet.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "sh.diffuse.output.facet", + "defs": { + "main": { + "type": "record", + "record": { + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { + "type": "string", + "description": "A unique identifier" + }, + "cid": { + "type": "string", + "description": "A DASL CID representing the UTF8 encoded HTML (raw 0x55 codec)" + }, + "createdAt": { "type": "string", "format": "datetime" }, + "description": { "type": "string" }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether the facet is enabled or not" + }, + "favourite": { + "type": "boolean", + "default": false, + "description": "Whether the facet is favourited (shown at the top) or not. Some facets may be more important to the user than others." + }, + "html": { + "type": "string", + "description": "The UTF8 HTML string that makes up the facet" + }, + "kind": { + "type": "string", + "default": "interactive", + "enum": ["interactive", "prelude"], + "description": "A facet is by default interactive, but headless 'prelude' facets may also be created, these run before any main interactive facet is loaded." + }, + "name": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "updatedAt": { "type": "string", "format": "datetime" }, + "uri": { + "type": "string", + "description": "An optional URI that points at the facet; can be used to update this artifact" + } + } + } + } + } +} diff --git a/lexicons/output/playlistItem.json b/lexicons/output/playlistItem.json new file mode 100644 index 000000000..5f9df5e85 --- /dev/null +++ b/lexicons/output/playlistItem.json @@ -0,0 +1,50 @@ +{ + "lexicon": 1, + "id": "sh.diffuse.output.playlistItem", + "defs": { + "main": { + "type": "record", + "record": { + "type": "object", + "required": ["id", "criteria", "playlist"], + "properties": { + "id": { "type": "string" }, + "createdAt": { "type": "string", "format": "datetime" }, + "criteria": { + "type": "array", + "description": "Criteria for matching this item against a set of tracks", + "items": { "type": "ref", "ref": "#criterion" } + }, + "ephemeral": { "type": "boolean" }, + "playlist": { "type": "string", "description": "The name of the playlist" }, + "positionedAfter": { + "type": "string", + "description": "Id of the item that this item should be positioned after" + }, + "updatedAt": { "type": "string", "format": "datetime" } + } + } + }, + "criterion": { + "type": "object", + "required": ["field", "value"], + "properties": { + "amount": { "type": "integer", "minimum": 0 }, + "field": { "type": "string" }, + "mode": { "type": "string", "knownValues": ["include", "exclude"], "default": "include" }, + "transformations": { + "type": "array", + "items": { "type": "ref", "ref": "#transformation" } + }, + "value": { + "type": "unknown", + "description": "Value that should be matched belonging to the given field" + } + } + }, + "transformation": { + "type": "string", + "description": "Transformation to apply to the field value before comparison. This transformation is method to call on the value. For example, `toString`" + } + } +} diff --git a/lexicons/output/playlistItemBundle.json b/lexicons/output/playlistItemBundle.json new file mode 100644 index 000000000..f316b91a1 --- /dev/null +++ b/lexicons/output/playlistItemBundle.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "sh.diffuse.output.playlistItemBundle", + "defs": { + "main": { + "type": "record", + "record": { + "type": "object", + "required": ["id", "data"], + "properties": { + "id": { "type": "string" }, + "playlist": { "type": "string" }, + "createdAt": { "type": "string", "format": "datetime" }, + "data": { + "type": "blob", + "description": "CBOR-encoded playlist items", + "accept": ["application/octet-stream"] + }, + "updatedAt": { "type": "string", "format": "datetime" } + } + } + } + } +} diff --git a/lexicons/output/setting.json b/lexicons/output/setting.json new file mode 100644 index 000000000..d4c2da243 --- /dev/null +++ b/lexicons/output/setting.json @@ -0,0 +1,20 @@ +{ + "lexicon": 1, + "id": "sh.diffuse.output.setting", + "defs": { + "main": { + "type": "record", + "record": { + "type": "object", + "required": ["id", "key", "value"], + "properties": { + "id": { "type": "string" }, + "createdAt": { "type": "string", "format": "datetime" }, + "updatedAt": { "type": "string", "format": "datetime" }, + "key": { "type": "string" }, + "value": { "type": "string" } + } + } + } + } +} diff --git a/lexicons/output/track.json b/lexicons/output/track.json new file mode 100644 index 000000000..f0c04ca09 --- /dev/null +++ b/lexicons/output/track.json @@ -0,0 +1,121 @@ +{ + "lexicon": 1, + "id": "sh.diffuse.output.track", + "defs": { + "main": { + "type": "record", + "record": { + "type": "object", + "required": ["id", "uri"], + "properties": { + "id": { "type": "string" }, + "createdAt": { "type": "string", "format": "datetime" }, + "ephemeral": { "type": "boolean" }, + "kind": { + "type": "string", + "enum": ["audiobook", "miscellaneous", "music", "placeholder", "podcast", "stream"] + }, + "stats": { + "type": "ref", + "ref": "#stats" + }, + "tags": { + "type": "ref", + "ref": "#tags" + }, + "updatedAt": { "type": "string", "format": "datetime" }, + "uri": { + "type": "string", + "description": "This is a 'semi-permanent' URI. Tracks are typically cached so you can't, for example, use an URL that expires in several hours." + } + } + } + }, + "count": { + "type": "object", + "required": ["no"], + "properties": { + "no": { "type": "integer" }, + "of": { "type": "integer" } + } + }, + "stats": { + "type": "object", + "properties": { + "albumGain": { "type": "integer", "description": "Album gain in dB" }, + "bitrate": { "type": "integer", "description": "Bits per second" }, + "bitsPerSample": { "type": "integer", "description": "Bit depth" }, + "codec": { "type": "string", "description": "Compression algorithm" }, + "container": { "type": "string", "description": "Encoding format" }, + "duration": { "type": "integer", "description": "Duration in milliseconds" }, + "lossless": { "type": "boolean", "description": "Is track lossless" }, + "numberOfChannels": { "type": "integer", "description": "Number of audio channels" }, + "sampleRate": { "type": "integer", "description": "Samples per second" }, + "spectralCentroid": { "type": "integer", "description": "Average spectral centroid in Hz, the perceptual brightness of the track derived from its spectrogram" }, + "spectralFlatness": { "type": "integer", "description": "Average spectral flatness on a 0-1000 scale (0 = fully tonal, 1000 = fully noisy), derived from the track's spectrogram" }, + "spectralFlux": { "type": "integer", "description": "Average spectral flux scaled by 1000, measuring how quickly the spectrum changes over time in the track's spectrogram" }, + "spectralRolloff": { "type": "integer", "description": "Average spectral rolloff in Hz, the frequency below which 85% of the spectral energy is contained, derived from the track's spectrogram" }, + "spectralSpread": { "type": "integer", "description": "Average spectral spread in Hz, the standard deviation of the spectrum around its centroid, derived from the track's spectrogram" }, + "trackGain": { "type": "integer", "description": "Track gain in dB" } + } + }, + "tags": { + "type": "object", + "properties": { + "album": { "type": "string" }, + "albumartist": { "type": "string" }, + "albumartists": { "type": "array", "items": { "type": "string" } }, + "albumartistsort": { "type": "string" }, + "albumsort": { "type": "string" }, + "arranger": { "type": "array", "items": { "type": "string" } }, + "artist": { "type": "string" }, + "artists": { "type": "array", "items": { "type": "string" } }, + "artistsort": { "type": "string" }, + "asin": { "type": "string" }, + "averageLevel": { "type": "integer" }, + "barcode": { "type": "string" }, + "bpm": { "type": "integer" }, + "catalognumbers": { "type": "array", "items": { "type": "string" } }, + "compilation": { "type": "boolean" }, + "composers": { "type": "array", "items": { "type": "string" } }, + "composersort": { "type": "string" }, + "conductors": { "type": "array", "items": { "type": "string" } }, + "date": { "type": "string" }, + "disc": { + "type": "ref", + "ref": "#count" + }, + "djmixers": { "type": "array", "items": { "type": "string" } }, + "engineers": { "type": "array", "items": { "type": "string" } }, + "gapless": { "type": "boolean" }, + "genres": { "type": "array", "items": { "type": "string" } }, + "isrc": { "type": "array", "items": { "type": "string" } }, + "labels": { "type": "array", "items": { "type": "string" } }, + "lyricists": { "type": "array", "items": { "type": "string" } }, + "media": { "type": "string" }, + "mixers": { "type": "array", "items": { "type": "string" } }, + "moods": { "type": "array", "items": { "type": "string" } }, + "originaldate": { "type": "string" }, + "originalyear": { "type": "integer" }, + "peakLevel": { "type": "integer" }, + "producers": { "type": "array", "items": { "type": "string" } }, + "publishers": { "type": "array", "items": { "type": "string" } }, + "releasecountry": { "type": "string" }, + "releasedate": { "type": "string" }, + "releasestatus": { "type": "string" }, + "releasetypes": { "type": "array", "items": { "type": "string" } }, + "remixers": { "type": "array", "items": { "type": "string" } }, + "technicians": { "type": "array", "items": { "type": "string" } }, + "title": { "type": "string" }, + "titlesort": { "type": "string" }, + "track": { + "type": "ref", + "ref": "#count" + }, + "work": { "type": "string" }, + "writers": { "type": "array", "items": { "type": "string" } }, + "year": { "type": "integer" } + } + } + } +} diff --git a/lexicons/output/trackBundle.json b/lexicons/output/trackBundle.json new file mode 100644 index 000000000..17db08c77 --- /dev/null +++ b/lexicons/output/trackBundle.json @@ -0,0 +1,24 @@ +{ + "lexicon": 1, + "id": "sh.diffuse.output.trackBundle", + "defs": { + "main": { + "type": "record", + "record": { + "type": "object", + "required": ["id", "data"], + "properties": { + "id": { "type": "string" }, + "scheme": { "type": "string" }, + "createdAt": { "type": "string", "format": "datetime" }, + "data": { + "type": "blob", + "description": "CBOR-encoded tracks", + "accept": ["application/octet-stream"] + }, + "updatedAt": { "type": "string", "format": "datetime" } + } + } + } + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 38747e77a..000000000 --- a/package-lock.json +++ /dev/null @@ -1,10697 +0,0 @@ -{ - "name": "diffuse", - "version": "3.5.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "diffuse", - "version": "3.5.0", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@tokenizer/http": "^0.9.1", - "@tokenizer/range": "^0.10.0", - "elm-taskport": "^2.0.1", - "encoding-japanese": "^2.0.0", - "fast-text-encoding": "^1.0.6", - "file-saver": "^2.0.2", - "jszip": "^3.7.1", - "load-script2": "^2.0.5", - "localforage": "^1.10.0", - "lunr": "^2.3.8", - "mediainfo.js": "0.3.3", - "music-metadata": "^10.6.0", - "readable-stream": "^4.5.2", - "remotestoragejs": "^2.0.0-beta.6", - "serve": "^14.2.1", - "throttle-debounce": "^5.0.0", - "timer.js": "^1.0.4", - "tocca": "^2.0.9", - "uint8arrays": "^4.0.10" - }, - "devDependencies": { - "@tauri-apps/api": "^2.0.0-beta.0", - "@tauri-apps/cli": "^2.0.0-beta.0", - "@tauri-apps/plugin-dialog": "^2.0.0-beta.0", - "@tauri-apps/plugin-fs": "^2.0.0-beta.0", - "@tauri-apps/plugin-shell": "^2.0.0-beta.0", - "@types/elm": "^0.19.3", - "@types/file-saver": "^2.0.7", - "@types/lunr": "^2.3.7", - "@types/throttle-debounce": "^5.0.2", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "assert": "^2.1.0", - "autoprefixer": "^10.4.19", - "buffer": "^6.0.3", - "elm": "0.19.1-6", - "elm-format": "^0.8.7", - "elm-review": "^2.10.3", - "esbuild": "^0.20.2", - "esbuild-plugin-wasm": "^1.1.0", - "eslint": "^8.56.0", - "events": "^3.3.0", - "gren-lang": "^0.3.0", - "just-install": "^2.0.2", - "postcss": "^8.4.34", - "postcss-custom-properties": "^13.3.4", - "postcss-import": "^16.0.0", - "stream": "^0.0.3", - "tailwindcss": "^3.4.1", - "tailwindcss-animations": "^2.0.0", - "tailwindcss-interaction-variants": "^5.0.0", - "tiny-parse-argv": "^2.4.0" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@avh4/elm-format-darwin-arm64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-arm64/-/elm-format-darwin-arm64-0.8.7-2.tgz", - "integrity": "sha512-F5JD44mJ3KX960J5GkXMfh1/dtkXuPcQpX2EToHQKjLTZUfnhZ++ytQQt0gAvrJ0bzoOvhNzjNjUHDA1ruTVbg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@avh4/elm-format-darwin-x64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-x64/-/elm-format-darwin-x64-0.8.7-2.tgz", - "integrity": "sha512-4pfF1cl0KyTion+7Mg4XKM3yi4Yc7vP76Kt/DotLVGJOSag4ISGic1og2mt8RZZ7XArybBmHNyYkiUbe/cEiCw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@avh4/elm-format-linux-arm64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-arm64/-/elm-format-linux-arm64-0.8.7-2.tgz", - "integrity": "sha512-WkVmuce2zU6s9dupHhqPc886Vaqpea8dZlxv2fpZ4wSzPUbiiKHoHZzoVndMIMTUL0TZukP3Ps0n/lWO5R5+FA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@avh4/elm-format-linux-x64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-x64/-/elm-format-linux-x64-0.8.7-2.tgz", - "integrity": "sha512-kmncfJrTBjVT94JtQvMf4M5Pn2Yl0sZt3wo7AzgFiDnB/CiZ+KjJyXuWM64NeGiv4MQqzPq65tsFXUH1CIJeiQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@avh4/elm-format-win32-x64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-win32-x64/-/elm-format-win32-x64-0.8.7-2.tgz", - "integrity": "sha512-sBdMBGq/8mD8Y5C+fIr5vlb3N50yB7S1MfgeAq2QEbvkr/sKrCZI540i43lZDH9gWsfA1w2W8wCe0penFYzsGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", - "integrity": "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", - "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^2.2.3" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz", - "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": "^14 || ^16 || >=18" - } - }, - "node_modules/@elm_binaries/darwin_arm64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/darwin_arm64/-/darwin_arm64-0.19.1-0.tgz", - "integrity": "sha512-mjbsH7BNHEAmoE2SCJFcfk5fIHwFIpxtSgnEAqMsVLpBUFoEtAeX+LQ+N0vSFJB3WAh73+QYx/xSluxxLcL6dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@elm_binaries/darwin_x64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/darwin_x64/-/darwin_x64-0.19.1-0.tgz", - "integrity": "sha512-QGUtrZTPBzaxgi9al6nr+9313wrnUVHuijzUK39UsPS+pa+n6CmWyV/69sHZeX9qy6UfeugE0PzF3qcUiy2GDQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@elm_binaries/linux_x64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/linux_x64/-/linux_x64-0.19.1-0.tgz", - "integrity": "sha512-T1ZrWVhg2kKAsi8caOd3vp/1A3e21VuCpSG63x8rDie50fHbCytTway9B8WHEdnBFv4mYWiA68dzGxYCiFmU2w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@elm_binaries/win32_x64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/win32_x64/-/win32_x64-0.19.1-0.tgz", - "integrity": "sha512-yDleiXqSE9EcqKtd9SkC/4RIW8I71YsXzMPL79ub2bBPHjWTcoyyeBbYjoOB9SxSlArJ74HaoBApzT6hY7Zobg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tauri-apps/api": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-beta.0.tgz", - "integrity": "sha512-WLoh/Vk8cgY7XrJV7Vpb6PssReBZWQCATfYBb1aCRDk+sp0NyPwumx6fZ2ECAKzAcs3OeanluwZcajruIW4CPQ==", - "dev": true, - "engines": { - "node": ">= 18", - "npm": ">= 6.6.0", - "yarn": ">= 1.19.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-beta.1.tgz", - "integrity": "sha512-u3AcZPdHsg9qT3e9PSD0H2IVZetQvWuBOyF81CN7/sY+AJGOli7i2d38Bj4wJs50tuMotoseiMcxuyxTlAdBnw==", - "dev": true, - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.0.0-beta.1", - "@tauri-apps/cli-darwin-x64": "2.0.0-beta.1", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-beta.1", - "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-beta.1", - "@tauri-apps/cli-linux-arm64-musl": "2.0.0-beta.1", - "@tauri-apps/cli-linux-x64-gnu": "2.0.0-beta.1", - "@tauri-apps/cli-linux-x64-musl": "2.0.0-beta.1", - "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-beta.1", - "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-beta.1", - "@tauri-apps/cli-win32-x64-msvc": "2.0.0-beta.1" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.1.tgz", - "integrity": "sha512-d71utEr9H3fXAI6nKPaPuINpnvMQn+UIscOTzTMcrmIDqptOO0ix8z6C3HSvNxV0OjtlxzNJGWwOb24U0OYrgw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.1.tgz", - "integrity": "sha512-bzsWZjQt5NG1uhbDTGw8Hmvm+J1d+9J7HXMMMwQc4E3kBns95sr4bIoXvgIq3cZYS4uyZOvdhEdjkSGg1c65Lg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.1.tgz", - "integrity": "sha512-FMnZpk4a5D9QgZKkT00P3f4CHEZFpn/b+pWfZJ7vxCdir+Cc1eKOHiqhvmMBEeLlYlQFBaYeAK0EaZWnN82ZJA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.1.tgz", - "integrity": "sha512-0kE65P+6ppeAOFsJV6av5VhkjDv1dcHkObErpjJHpwYowuC3aqaCCnH3biR9gNvcoVUXsCwmMA/BkxUpq9W9/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.1.tgz", - "integrity": "sha512-Wsj1eSrrAVeuFQWJq1gVIA78I8JM50fEsxbrMAOf89ZXpCYxJTNCJkyRQyLB+yHhv9nmhA3a1Mmr5ubhRETy1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.1.tgz", - "integrity": "sha512-LkzLJWg+ud2gWuq8yAWJ3Sahrp79Vbd2Cotbm/RbfMi7RbRV8TQYj4zfUhyFJVnk4nF89kTnwfNxLdTw67CAOw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.1.tgz", - "integrity": "sha512-Ro3PuLSNEZAw9/Rc2CP3k9P7LaUQ2TOFXJeW6G4aCXrd0MlJwlGhhjdZuLbmgzD1rda4dSpZGJPhbYvu8YD7eQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.1.tgz", - "integrity": "sha512-SWNF+5B+lBbW/Kq1wTMVG9x97PqJUOo8eWAr/nlMm3J0lYbTWAa8/ScibaPjq82HiPhv8WCJXlcO6FEqWCoJ2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.1.tgz", - "integrity": "sha512-NvfP16fSlfq6GLHJH+gAxEsJn+Jvz3HoxMTLxAg7Ra0ycMODFu4xbNn6Hp7Djn297qTHHLYDva4Np6Whw5DUlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.1.tgz", - "integrity": "sha512-9TKbDQyVHW0p1a7aXQEKg+MhCyFMpzD26puLKOxbTPiTcRUR4lUFq5Bhf1VR5ihoqnZNhJEtuR1mA16ZrIkuKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-beta.0.tgz", - "integrity": "sha512-cNxiBwGB6Xgnwy5GyAabwPyvm1iaC7MmrgzcUwB1JaydLjp/Yu3Z0TmkmmEClFrrvIQA+sMcq0C8wwv6i4aedw==", - "dev": true, - "dependencies": { - "@tauri-apps/api": "2.0.0-beta.0" - } - }, - "node_modules/@tauri-apps/plugin-fs": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.0.tgz", - "integrity": "sha512-oFAA8T5wyg5dmsF/WmjjtUMEnJtzu+p2ElsK2FdwLVSB2AdAdud5izZhd1o63HFjR6waiIYebSI+llzJG7b5MQ==", - "dev": true, - "dependencies": { - "@tauri-apps/api": "2.0.0-beta.0" - } - }, - "node_modules/@tauri-apps/plugin-shell": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.0.tgz", - "integrity": "sha512-AADGPuGP+YM5Ar48XfA8o9k9jtlSBpEVcsbOEwWdifi0oAiVOBXgiy9C1Icsub/tlD3YjK7oT8vDcOXLGYBhUg==", - "dev": true, - "dependencies": { - "@tauri-apps/api": "2.0.0-beta.0" - } - }, - "node_modules/@tokenizer/http": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@tokenizer/http/-/http-0.9.1.tgz", - "integrity": "sha512-jwHLkafS9ZVEt8dFR4s4GEa5d/iKiowtn7uE19hbxIYh7IK1tAkU4eWRXB5GJ0PtPRf2BEDzAqKMhAzDpDlUmw==", - "dependencies": { - "@tokenizer/range": "^0.10.0", - "debug": "^4.3.7", - "strtok3": "^9.0.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/range": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tokenizer/range/-/range-0.10.0.tgz", - "integrity": "sha512-noYSSTeDdpxLn1WYVB7TZhcq80pA+5b0HdM1Dtvkkk3nlHw4h2nFoaAdrvdwtbjAAT76GIL87/uK5mKE3T+oYg==", - "dependencies": { - "debug": "^4.3.6", - "strtok3": "^9.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/elm": { - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@types/elm/-/elm-0.19.3.tgz", - "integrity": "sha512-1DnHZiIHvDyjL6MHrePqbD3ooLLix13k6ow8gEydFOAXImkcvbzQX0Ri+WJOM7RvgPfmyUe6uQ2Acupb1oL+GA==", - "dev": true - }, - "node_modules/@types/file-saver": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", - "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", - "dev": true - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/lunr": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.3.7.tgz", - "integrity": "sha512-Tb/kUm38e8gmjahQzdCKhbdsvQ9/ppzHFfsJ0dMs3ckqQsRj+P5IkSAwFTBrBxdyr3E/LoMUUrZngjDYAjiE3A==", - "dev": true - }, - "node_modules/@types/node": { - "version": "18.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", - "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==", - "dev": true - }, - "node_modules/@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", - "dev": true - }, - "node_modules/@types/throttle-debounce": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.2.tgz", - "integrity": "sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==", - "dev": true - }, - "node_modules/@types/tv4": { - "version": "1.2.31", - "resolved": "https://registry.npmjs.org/@types/tv4/-/tv4-1.2.31.tgz", - "integrity": "sha512-P97XU07fcpauSw3/fE2Q7eF6bHl4oHhwkikjnM7zlQLENrdC2rZuHSdNlMBhnW82NyBEsVJHII1Jk3d/MtQsQQ==" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/@zeit/schemas": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.29.0.tgz", - "integrity": "sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/boxen": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", - "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.0", - "chalk": "^5.0.1", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/boxen/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/boxen/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", - "dev": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", - "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "set-function-length": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001636", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", - "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", - "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", - "dependencies": { - "chalk": "^4.1.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.8.0.tgz", - "integrity": "sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", - "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", - "dependencies": { - "arch": "^2.2.0", - "execa": "^5.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/component-emitter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", - "integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", - "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.806", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.806.tgz", - "integrity": "sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg==", - "dev": true - }, - "node_modules/elm": { - "version": "0.19.1-6", - "resolved": "https://registry.npmjs.org/elm/-/elm-0.19.1-6.tgz", - "integrity": "sha512-mKYyierHICPdMx/vhiIacdPmTPnh889gjHOZ75ZAoCxo3lZmSWbGP8HMw78wyctJH0HwvTmeKhlYSWboQNYPeQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "elm": "bin/elm" - }, - "engines": { - "node": ">=7.0.0" - }, - "optionalDependencies": { - "@elm_binaries/darwin_arm64": "0.19.1-0", - "@elm_binaries/darwin_x64": "0.19.1-0", - "@elm_binaries/linux_x64": "0.19.1-0", - "@elm_binaries/win32_x64": "0.19.1-0" - } - }, - "node_modules/elm-format": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/elm-format/-/elm-format-0.8.7.tgz", - "integrity": "sha512-sVzFXfWnb+6rzXK+q3e3Ccgr6/uS5mFbFk1VSmigC+x2XZ28QycAa7lS8owl009ALPhRQk+pZ95Eq5ANjpEZsQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "elm-format": "bin/elm-format" - }, - "optionalDependencies": { - "@avh4/elm-format-darwin-arm64": "0.8.7-2", - "@avh4/elm-format-darwin-x64": "0.8.7-2", - "@avh4/elm-format-linux-arm64": "0.8.7-2", - "@avh4/elm-format-linux-x64": "0.8.7-2", - "@avh4/elm-format-win32-x64": "0.8.7-2" - } - }, - "node_modules/elm-review": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/elm-review/-/elm-review-2.10.3.tgz", - "integrity": "sha512-9gBqLc5Xm3v9Ncpf8y2haEaBZZyMX25vmwQkrNqSIaNYQ2vdGOIx1eyqRtt1xwX5O8pZdqU8IWC/ENyTrOTKbw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "chokidar": "^3.5.2", - "cross-spawn": "^7.0.3", - "elm-tooling": "^1.14.1", - "fastest-levenshtein": "^1.0.16", - "find-up": "^4.1.0", - "folder-hash": "^3.3.0", - "fs-extra": "^9.0.0", - "glob": "^7.1.4", - "got": "^11.8.5", - "graceful-fs": "^4.2.11", - "minimist": "^1.2.6", - "ora": "^5.4.0", - "path-key": "^3.1.1", - "prompts": "^2.2.1", - "rimraf": "^5.0.0", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.1.1", - "which": "^2.0.2", - "wrap-ansi": "^6.2.0" - }, - "bin": { - "elm-review": "bin/elm-review" - }, - "engines": { - "node": ">=10.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/jfmengels" - } - }, - "node_modules/elm-review/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/elm-review/node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/elm-review/node_modules/rimraf": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", - "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", - "dev": true, - "dependencies": { - "glob": "^10.2.5" - }, - "bin": { - "rimraf": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/elm-review/node_modules/rimraf/node_modules/glob": { - "version": "10.2.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.7.tgz", - "integrity": "sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2", - "path-scurry": "^1.7.0" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/elm-taskport": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/elm-taskport/-/elm-taskport-2.0.1.tgz", - "integrity": "sha512-8UgIjzmGuoU6Wt6VC0tkJnzvc5xHL5yH7GdN+/QNxaaA3ckoyveCtV0QJqNCRa42bpyR1JhOwLSApwf2Id5xZg==" - }, - "node_modules/elm-tooling": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.15.0.tgz", - "integrity": "sha512-quRE5LGJyrkPBoJ3MvFQ5RGgf80J0L0d3NkduStvXh4TmZuMXNC3Z/l2ZRoq2UTUaNWeYfO1Zx5wns1AvsTrnw==", - "dev": true, - "bin": { - "elm-tooling": "index.js" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encoding-japanese": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz", - "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==", - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "node_modules/esbuild-plugin-wasm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/esbuild-plugin-wasm/-/esbuild-plugin-wasm-1.1.0.tgz", - "integrity": "sha512-0bQ6+1tUbySSnxzn5jnXHMDvYnT0cN/Wd4Syk8g/sqAIJUg7buTIi22svS3Qz6ssx895NT+TgLPb33xi1OkZig==", - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "individual", - "url": "https://ko-fi.com/tschrock" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-text-encoding": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", - "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" - }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", - "dependencies": { - "punycode": "^1.3.2" - } - }, - "node_modules/fast-url-parser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" - }, - "node_modules/file-type": { - "version": "19.6.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", - "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", - "dependencies": { - "get-stream": "^9.0.1", - "strtok3": "^9.0.1", - "token-types": "^6.0.0", - "uint8array-extras": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/file-type/node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-type/node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "node_modules/folder-hash": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.3.tgz", - "integrity": "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "graceful-fs": "~4.2.0", - "minimatch": "~3.0.4" - }, - "bin": { - "folder-hash": "bin/folder-hash" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/folder-hash/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/gren-compiler-library": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/gren-compiler-library/-/gren-compiler-library-0.3.0.tgz", - "integrity": "sha512-CvcdoHYJuqwc4BUKvQPPhtegTUmW5I6rhQlKxxmu+qRbDYFeB/cypM3Xha3aqJr9l14JdmL5fMichENRqg3CBg==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.2", - "xdg-basedir": "^5.1.0" - } - }, - "node_modules/gren-lang": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/gren-lang/-/gren-lang-0.3.0.tgz", - "integrity": "sha512-Gl/0jckyK97BH/lUPj68vP4m05d1QTrLlhZ2ELQgKfDJu8PVKeG+LtRRkdpIzcncCfgR/htWdwt2UkwpPXzalA==", - "dev": true, - "dependencies": { - "gren-compiler-library": "0.3.0" - }, - "bin": { - "gren": "index.js" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-port-reachable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", - "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/jackspeak": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz", - "integrity": "sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/just-install": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/just-install/-/just-install-2.0.2.tgz", - "integrity": "sha512-zH6aon3V2P8ZbD+njaMB/orHsOyFMgONSpxKtbovNu7Bhb1rD9qhnMkT2Nj91++b9GgqHNbozhUdIMxecmWJaw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "extract-zip": "^2.0.1", - "node-fetch": "^3.3.2" - }, - "bin": { - "just": "bin/just.js", - "just-install": "install.js" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/just-install/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "node_modules/load-script2": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/load-script2/-/load-script2-2.0.6.tgz", - "integrity": "sha512-pyuw/AR+ycZkRKgkMyXOIf/o2OnRQPc9grjZ04wVOeoJ7SqX97WlcZahl+3/r5qi09L/5d0iCVZ0q4OqNT/v0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "dependencies": { - "lie": "3.1.1" - } - }, - "node_modules/localforage/node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/mediainfo.js": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/mediainfo.js/-/mediainfo.js-0.3.3.tgz", - "integrity": "sha512-+k6cobEQ0Ies/XajglElyHZyguxzEfnztGrZ+bEcuJ4gK74MeN1T0aX1DIFAMRwmFsnJlTJVTiOHJHWZssEJjA==", - "dependencies": { - "yargs": "^17.7.2" - }, - "bin": { - "mediainfo.js": "dist/esm/cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz", - "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/music-metadata": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-10.6.0.tgz", - "integrity": "sha512-YRTM8KhXZHe1zBsBKA+1JwZBR7SYP/6S/9tm+FRne9dD6SdC6F+s3e5+OugwaChh/zxmAb/yw6SaLAhHZxvIiA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - }, - { - "type": "buymeacoffee", - "url": "https://buymeacoffee.com/borewit" - } - ], - "dependencies": { - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.3.7", - "file-type": "^19.6.0", - "media-typer": "^1.1.0", - "strtok3": "^9.0.1", - "token-types": "^6.0.0", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/optionator/node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz", - "integrity": "sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==", - "dev": true, - "dependencies": { - "lru-cache": "^9.1.1", - "minipass": "^5.0.0 || ^6.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz", - "integrity": "sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-to-regexp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", - "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/peek-readable": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.3.1.tgz", - "integrity": "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.4.34", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.34.tgz", - "integrity": "sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-custom-properties": { - "version": "13.3.4", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.4.tgz", - "integrity": "sha512-9YN0gg9sG3OH+Z9xBrp2PWRb+O4msw+5Sbp3ZgqrblrwKspXVQe5zr5sVqi43gJGwW/Rv1A483PRQUzQOEewvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.7", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-import": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.0.0.tgz", - "integrity": "sha512-e77lhVvrD1I2y7dYmBv0k9ULTdArgEYZt97T4w6sFIU5uxIHvDFQlKgUUyY7v7Barj0Yf/zm5A4OquZN7jKm5Q==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", - "dev": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" - }, - "engines": { - "node": ">= 14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz", - "integrity": "sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "dependencies": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", - "dependencies": { - "rc": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remotestoragejs": { - "version": "2.0.0-beta.6", - "resolved": "https://registry.npmjs.org/remotestoragejs/-/remotestoragejs-2.0.0-beta.6.tgz", - "integrity": "sha512-xGBmCPe99F4Gs/IEEzvGFjlmOkG48rRCSFz4+7xFNVs4WUJ3nzkm+fQbKelTB2HHErgOLSIyxc2dloqhI5srAQ==", - "dependencies": { - "@types/node": "16.11.59", - "@types/tv4": "^1.2.29", - "esm": "^3.2.25", - "tv4": "^1.3.0", - "webfinger.js": "^2.7.1", - "xhr2": "^0.2.1" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/remotestoragejs/node_modules/@types/node": { - "version": "16.11.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.59.tgz", - "integrity": "sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw==" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serve": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.1.tgz", - "integrity": "sha512-48er5fzHh7GCShLnNyPBRPEjs2I6QBozeGr02gaacROiyS/8ARADlj595j39iZXAqBbJHH/ivJJyPRWY9sQWZA==", - "dependencies": { - "@zeit/schemas": "2.29.0", - "ajv": "8.11.0", - "arg": "5.0.2", - "boxen": "7.0.0", - "chalk": "5.0.1", - "chalk-template": "0.4.0", - "clipboardy": "3.0.0", - "compression": "1.7.4", - "is-port-reachable": "4.0.0", - "serve-handler": "6.1.5", - "update-check": "1.5.4" - }, - "bin": { - "serve": "build/main.js" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/serve-handler": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz", - "integrity": "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==", - "dependencies": { - "bytes": "3.0.0", - "content-disposition": "0.5.2", - "fast-url-parser": "1.1.3", - "mime-types": "2.1.18", - "minimatch": "3.1.2", - "path-is-inside": "1.0.2", - "path-to-regexp": "2.2.1", - "range-parser": "1.2.0" - } - }, - "node_modules/serve-handler/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-handler/node_modules/mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-handler/node_modules/mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dependencies": { - "mime-db": "~1.33.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/serve/node_modules/chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/serve/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz", - "integrity": "sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==", - "dev": true, - "dependencies": { - "component-emitter": "^2.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strtok3": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.0.1.tgz", - "integrity": "sha512-ERPW+XkvX9W2A+ov07iy+ZFJpVdik04GhDA4eVogiG9hpC97Kem2iucyzhFxbFRvQ5o2UckFtKZdp1hkGvnrEw==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.3.1" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/sucrase": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", - "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", - "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss-animations": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tailwindcss-animations/-/tailwindcss-animations-2.0.0.tgz", - "integrity": "sha512-YLdhPiQeDtpnqWd0J70rvy08MDbsq+BBLrzMmPR1Pe6MJDFYDuk4CYGI14GVgj7xRBGkByzp/rAfBcUWD7zpng==", - "deprecated": "Use Tailwind 1.6's animation utilities", - "dev": true, - "dependencies": { - "lodash": "^4.17.15" - } - }, - "node_modules/tailwindcss-interaction-variants": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tailwindcss-interaction-variants/-/tailwindcss-interaction-variants-5.0.0.tgz", - "integrity": "sha512-Tmx9HjwuNA3G7cLUcebGkNchKkNVIZXBroxa0LBoq/KUb/xEKEM8qugUG7QczSNPhaZzpXmo3IzPkRhf4tDaOQ==", - "deprecated": "Use Tailwind's JIT engine", - "dev": true, - "dependencies": { - "lodash": "^4.17.19", - "postcss-selector-parser": "^6.0.2" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tailwindcss/node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/throttle-debounce": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", - "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", - "engines": { - "node": ">=12.22" - } - }, - "node_modules/timer.js": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/timer.js/-/timer.js-1.0.4.tgz", - "integrity": "sha512-UU/lL02CjPMu8cu29h/uyt5yBqcm0vlx51Je9xCmVcH+TLS+X/+chv4V4rHlb37ZiI4iUVyLuC3QvyoFMzadwQ==" - }, - "node_modules/tiny-parse-argv": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tiny-parse-argv/-/tiny-parse-argv-2.4.0.tgz", - "integrity": "sha512-WTEsnmeHNr99hLQIDA+gnsS+fDsCDITlqgI+zEhx9M6ErPt0heoNZ1PGvql6wcf95sIx40J0MLYXaPveGwtpoA==", - "dev": true - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tocca": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/tocca/-/tocca-2.0.9.tgz", - "integrity": "sha512-FXofUGAFcgEIEOS3m9Dk9URaY3x+JeerPfVFc8K820PjV0JcCsH98BFxrVOTANC8YvM4QTrmKMol2/818gFmCw==" - }, - "node_modules/token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/ts-api-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", - "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", - "dev": true, - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "node_modules/tv4": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", - "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uint8array-extras": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/uint8arrays": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.10.tgz", - "integrity": "sha512-AnJNUGGDJAgFw/eWu/Xb9zrVKEGlwJJCaeInlf3BkecE/zcTobk5YXYIPNQJO1q5Hh1QZrQQHf0JvcHqz2hqoA==", - "dependencies": { - "multiformats": "^12.0.1" - } - }, - "node_modules/uint8arrays/node_modules/multiformats": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-12.0.1.tgz", - "integrity": "sha512-s01wijBJoDUqESWSzePY0lvTw7J3PVO9x2Cc6ASI5AMZM2Gnhh7BC17+nlFhHKU7dDzaCaRfb+NiqNzOsgPUoQ==", - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-check": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", - "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", - "dependencies": { - "registry-auth-token": "3.3.2", - "registry-url": "3.1.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/webfinger.js": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/webfinger.js/-/webfinger.js-2.7.1.tgz", - "integrity": "sha512-H4RokaE4RC39N3uiRTcjKMmy6yYg06lUPORQHvv8DjowZ6I5SalxUeoqHbtTN3EVBmYP/XHQ8Ow6BLEIpe2DtA==", - "dependencies": { - "xhr2": "0.2.1" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xhr2": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", - "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true - }, - "@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true - }, - "@avh4/elm-format-darwin-arm64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-arm64/-/elm-format-darwin-arm64-0.8.7-2.tgz", - "integrity": "sha512-F5JD44mJ3KX960J5GkXMfh1/dtkXuPcQpX2EToHQKjLTZUfnhZ++ytQQt0gAvrJ0bzoOvhNzjNjUHDA1ruTVbg==", - "dev": true, - "optional": true - }, - "@avh4/elm-format-darwin-x64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-x64/-/elm-format-darwin-x64-0.8.7-2.tgz", - "integrity": "sha512-4pfF1cl0KyTion+7Mg4XKM3yi4Yc7vP76Kt/DotLVGJOSag4ISGic1og2mt8RZZ7XArybBmHNyYkiUbe/cEiCw==", - "dev": true, - "optional": true - }, - "@avh4/elm-format-linux-arm64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-arm64/-/elm-format-linux-arm64-0.8.7-2.tgz", - "integrity": "sha512-WkVmuce2zU6s9dupHhqPc886Vaqpea8dZlxv2fpZ4wSzPUbiiKHoHZzoVndMIMTUL0TZukP3Ps0n/lWO5R5+FA==", - "dev": true, - "optional": true - }, - "@avh4/elm-format-linux-x64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-x64/-/elm-format-linux-x64-0.8.7-2.tgz", - "integrity": "sha512-kmncfJrTBjVT94JtQvMf4M5Pn2Yl0sZt3wo7AzgFiDnB/CiZ+KjJyXuWM64NeGiv4MQqzPq65tsFXUH1CIJeiQ==", - "dev": true, - "optional": true - }, - "@avh4/elm-format-win32-x64": { - "version": "0.8.7-2", - "resolved": "https://registry.npmjs.org/@avh4/elm-format-win32-x64/-/elm-format-win32-x64-0.8.7-2.tgz", - "integrity": "sha512-sBdMBGq/8mD8Y5C+fIr5vlb3N50yB7S1MfgeAq2QEbvkr/sKrCZI540i43lZDH9gWsfA1w2W8wCe0penFYzsGw==", - "dev": true, - "optional": true - }, - "@csstools/cascade-layer-name-parser": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.7.tgz", - "integrity": "sha512-9J4aMRJ7A2WRjaRLvsMeWrL69FmEuijtiW1XlK/sG+V0UJiHVYUyvj9mY4WAXfU/hGIiGOgL8e0jJcRyaZTjDQ==", - "dev": true, - "requires": {} - }, - "@csstools/css-parser-algorithms": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", - "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", - "dev": true, - "requires": {} - }, - "@csstools/css-tokenizer": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz", - "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==", - "dev": true - }, - "@elm_binaries/darwin_arm64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/darwin_arm64/-/darwin_arm64-0.19.1-0.tgz", - "integrity": "sha512-mjbsH7BNHEAmoE2SCJFcfk5fIHwFIpxtSgnEAqMsVLpBUFoEtAeX+LQ+N0vSFJB3WAh73+QYx/xSluxxLcL6dA==", - "dev": true, - "optional": true - }, - "@elm_binaries/darwin_x64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/darwin_x64/-/darwin_x64-0.19.1-0.tgz", - "integrity": "sha512-QGUtrZTPBzaxgi9al6nr+9313wrnUVHuijzUK39UsPS+pa+n6CmWyV/69sHZeX9qy6UfeugE0PzF3qcUiy2GDQ==", - "dev": true, - "optional": true - }, - "@elm_binaries/linux_x64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/linux_x64/-/linux_x64-0.19.1-0.tgz", - "integrity": "sha512-T1ZrWVhg2kKAsi8caOd3vp/1A3e21VuCpSG63x8rDie50fHbCytTway9B8WHEdnBFv4mYWiA68dzGxYCiFmU2w==", - "dev": true, - "optional": true - }, - "@elm_binaries/win32_x64": { - "version": "0.19.1-0", - "resolved": "https://registry.npmjs.org/@elm_binaries/win32_x64/-/win32_x64-0.19.1-0.tgz", - "integrity": "sha512-yDleiXqSE9EcqKtd9SkC/4RIW8I71YsXzMPL79ub2bBPHjWTcoyyeBbYjoOB9SxSlArJ74HaoBApzT6hY7Zobg==", - "dev": true, - "optional": true - }, - "@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "dev": true, - "optional": true - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - } - } - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true - }, - "@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" - }, - "@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "requires": { - "defer-to-connect": "^2.0.0" - } - }, - "@tauri-apps/api": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-beta.0.tgz", - "integrity": "sha512-WLoh/Vk8cgY7XrJV7Vpb6PssReBZWQCATfYBb1aCRDk+sp0NyPwumx6fZ2ECAKzAcs3OeanluwZcajruIW4CPQ==", - "dev": true - }, - "@tauri-apps/cli": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-beta.1.tgz", - "integrity": "sha512-u3AcZPdHsg9qT3e9PSD0H2IVZetQvWuBOyF81CN7/sY+AJGOli7i2d38Bj4wJs50tuMotoseiMcxuyxTlAdBnw==", - "dev": true, - "requires": { - "@tauri-apps/cli-darwin-arm64": "2.0.0-beta.1", - "@tauri-apps/cli-darwin-x64": "2.0.0-beta.1", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-beta.1", - "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-beta.1", - "@tauri-apps/cli-linux-arm64-musl": "2.0.0-beta.1", - "@tauri-apps/cli-linux-x64-gnu": "2.0.0-beta.1", - "@tauri-apps/cli-linux-x64-musl": "2.0.0-beta.1", - "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-beta.1", - "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-beta.1", - "@tauri-apps/cli-win32-x64-msvc": "2.0.0-beta.1" - } - }, - "@tauri-apps/cli-darwin-arm64": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.1.tgz", - "integrity": "sha512-d71utEr9H3fXAI6nKPaPuINpnvMQn+UIscOTzTMcrmIDqptOO0ix8z6C3HSvNxV0OjtlxzNJGWwOb24U0OYrgw==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-darwin-x64": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.1.tgz", - "integrity": "sha512-bzsWZjQt5NG1uhbDTGw8Hmvm+J1d+9J7HXMMMwQc4E3kBns95sr4bIoXvgIq3cZYS4uyZOvdhEdjkSGg1c65Lg==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.1.tgz", - "integrity": "sha512-FMnZpk4a5D9QgZKkT00P3f4CHEZFpn/b+pWfZJ7vxCdir+Cc1eKOHiqhvmMBEeLlYlQFBaYeAK0EaZWnN82ZJA==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.1.tgz", - "integrity": "sha512-0kE65P+6ppeAOFsJV6av5VhkjDv1dcHkObErpjJHpwYowuC3aqaCCnH3biR9gNvcoVUXsCwmMA/BkxUpq9W9/g==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-linux-arm64-musl": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.1.tgz", - "integrity": "sha512-Wsj1eSrrAVeuFQWJq1gVIA78I8JM50fEsxbrMAOf89ZXpCYxJTNCJkyRQyLB+yHhv9nmhA3a1Mmr5ubhRETy1Q==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-linux-x64-gnu": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.1.tgz", - "integrity": "sha512-LkzLJWg+ud2gWuq8yAWJ3Sahrp79Vbd2Cotbm/RbfMi7RbRV8TQYj4zfUhyFJVnk4nF89kTnwfNxLdTw67CAOw==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-linux-x64-musl": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.1.tgz", - "integrity": "sha512-Ro3PuLSNEZAw9/Rc2CP3k9P7LaUQ2TOFXJeW6G4aCXrd0MlJwlGhhjdZuLbmgzD1rda4dSpZGJPhbYvu8YD7eQ==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.1.tgz", - "integrity": "sha512-SWNF+5B+lBbW/Kq1wTMVG9x97PqJUOo8eWAr/nlMm3J0lYbTWAa8/ScibaPjq82HiPhv8WCJXlcO6FEqWCoJ2A==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.1.tgz", - "integrity": "sha512-NvfP16fSlfq6GLHJH+gAxEsJn+Jvz3HoxMTLxAg7Ra0ycMODFu4xbNn6Hp7Djn297qTHHLYDva4Np6Whw5DUlQ==", - "dev": true, - "optional": true - }, - "@tauri-apps/cli-win32-x64-msvc": { - "version": "2.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.1.tgz", - "integrity": "sha512-9TKbDQyVHW0p1a7aXQEKg+MhCyFMpzD26puLKOxbTPiTcRUR4lUFq5Bhf1VR5ihoqnZNhJEtuR1mA16ZrIkuKQ==", - "dev": true, - "optional": true - }, - "@tauri-apps/plugin-dialog": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-beta.0.tgz", - "integrity": "sha512-cNxiBwGB6Xgnwy5GyAabwPyvm1iaC7MmrgzcUwB1JaydLjp/Yu3Z0TmkmmEClFrrvIQA+sMcq0C8wwv6i4aedw==", - "dev": true, - "requires": { - "@tauri-apps/api": "2.0.0-beta.0" - } - }, - "@tauri-apps/plugin-fs": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-beta.0.tgz", - "integrity": "sha512-oFAA8T5wyg5dmsF/WmjjtUMEnJtzu+p2ElsK2FdwLVSB2AdAdud5izZhd1o63HFjR6waiIYebSI+llzJG7b5MQ==", - "dev": true, - "requires": { - "@tauri-apps/api": "2.0.0-beta.0" - } - }, - "@tauri-apps/plugin-shell": { - "version": "2.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.0.tgz", - "integrity": "sha512-AADGPuGP+YM5Ar48XfA8o9k9jtlSBpEVcsbOEwWdifi0oAiVOBXgiy9C1Icsub/tlD3YjK7oT8vDcOXLGYBhUg==", - "dev": true, - "requires": { - "@tauri-apps/api": "2.0.0-beta.0" - } - }, - "@tokenizer/http": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@tokenizer/http/-/http-0.9.1.tgz", - "integrity": "sha512-jwHLkafS9ZVEt8dFR4s4GEa5d/iKiowtn7uE19hbxIYh7IK1tAkU4eWRXB5GJ0PtPRf2BEDzAqKMhAzDpDlUmw==", - "requires": { - "@tokenizer/range": "^0.10.0", - "debug": "^4.3.7", - "strtok3": "^9.0.1" - } - }, - "@tokenizer/range": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tokenizer/range/-/range-0.10.0.tgz", - "integrity": "sha512-noYSSTeDdpxLn1WYVB7TZhcq80pA+5b0HdM1Dtvkkk3nlHw4h2nFoaAdrvdwtbjAAT76GIL87/uK5mKE3T+oYg==", - "requires": { - "debug": "^4.3.6", - "strtok3": "^9.0.0" - } - }, - "@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, - "@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "requires": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "@types/elm": { - "version": "0.19.3", - "resolved": "https://registry.npmjs.org/@types/elm/-/elm-0.19.3.tgz", - "integrity": "sha512-1DnHZiIHvDyjL6MHrePqbD3ooLLix13k6ow8gEydFOAXImkcvbzQX0Ri+WJOM7RvgPfmyUe6uQ2Acupb1oL+GA==", - "dev": true - }, - "@types/file-saver": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", - "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", - "dev": true - }, - "@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/lunr": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.3.7.tgz", - "integrity": "sha512-Tb/kUm38e8gmjahQzdCKhbdsvQ9/ppzHFfsJ0dMs3ckqQsRj+P5IkSAwFTBrBxdyr3E/LoMUUrZngjDYAjiE3A==", - "dev": true - }, - "@types/node": { - "version": "18.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", - "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==", - "dev": true - }, - "@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", - "dev": true - }, - "@types/throttle-debounce": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.2.tgz", - "integrity": "sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==", - "dev": true - }, - "@types/tv4": { - "version": "1.2.31", - "resolved": "https://registry.npmjs.org/@types/tv4/-/tv4-1.2.31.tgz", - "integrity": "sha512-P97XU07fcpauSw3/fE2Q7eF6bHl4oHhwkikjnM7zlQLENrdC2rZuHSdNlMBhnW82NyBEsVJHII1Jk3d/MtQsQQ==" - }, - "@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "requires": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - } - }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "@zeit/schemas": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.29.0.tgz", - "integrity": "sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==" - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "requires": { - "string-width": "^4.1.0" - } - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==" - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true - }, - "autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "dev": true, - "requires": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "boxen": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", - "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", - "requires": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.0", - "chalk": "^5.0.1", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" - }, - "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" - }, - "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" - } - }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, - "cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true - }, - "cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - } - }, - "call-bind": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", - "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", - "dev": true, - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "set-function-length": "^1.2.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==" - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001636", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", - "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chalk-template": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", - "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", - "requires": { - "chalk": "^4.1.2" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==" - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-spinners": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.8.0.tgz", - "integrity": "sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ==", - "dev": true - }, - "clipboardy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", - "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", - "requires": { - "arch": "^2.2.0", - "execa": "^5.1.1", - "is-wsl": "^2.2.0" - } - }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true - }, - "clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, - "component-emitter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", - "integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==", - "dev": true - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==" - }, - "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true - }, - "debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "requires": { - "ms": "^2.1.3" - } - }, - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "requires": { - "mimic-response": "^3.1.0" - }, - "dependencies": { - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true - } - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true - }, - "define-data-property": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", - "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", - "dev": true, - "requires": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - } - }, - "define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "requires": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "electron-to-chromium": { - "version": "1.4.806", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.806.tgz", - "integrity": "sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg==", - "dev": true - }, - "elm": { - "version": "0.19.1-6", - "resolved": "https://registry.npmjs.org/elm/-/elm-0.19.1-6.tgz", - "integrity": "sha512-mKYyierHICPdMx/vhiIacdPmTPnh889gjHOZ75ZAoCxo3lZmSWbGP8HMw78wyctJH0HwvTmeKhlYSWboQNYPeQ==", - "dev": true, - "requires": { - "@elm_binaries/darwin_arm64": "0.19.1-0", - "@elm_binaries/darwin_x64": "0.19.1-0", - "@elm_binaries/linux_x64": "0.19.1-0", - "@elm_binaries/win32_x64": "0.19.1-0" - } - }, - "elm-format": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/elm-format/-/elm-format-0.8.7.tgz", - "integrity": "sha512-sVzFXfWnb+6rzXK+q3e3Ccgr6/uS5mFbFk1VSmigC+x2XZ28QycAa7lS8owl009ALPhRQk+pZ95Eq5ANjpEZsQ==", - "dev": true, - "requires": { - "@avh4/elm-format-darwin-arm64": "0.8.7-2", - "@avh4/elm-format-darwin-x64": "0.8.7-2", - "@avh4/elm-format-linux-arm64": "0.8.7-2", - "@avh4/elm-format-linux-x64": "0.8.7-2", - "@avh4/elm-format-win32-x64": "0.8.7-2" - } - }, - "elm-review": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/elm-review/-/elm-review-2.10.3.tgz", - "integrity": "sha512-9gBqLc5Xm3v9Ncpf8y2haEaBZZyMX25vmwQkrNqSIaNYQ2vdGOIx1eyqRtt1xwX5O8pZdqU8IWC/ENyTrOTKbw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "chokidar": "^3.5.2", - "cross-spawn": "^7.0.3", - "elm-tooling": "^1.14.1", - "fastest-levenshtein": "^1.0.16", - "find-up": "^4.1.0", - "folder-hash": "^3.3.0", - "fs-extra": "^9.0.0", - "glob": "^7.1.4", - "got": "^11.8.5", - "graceful-fs": "^4.2.11", - "minimist": "^1.2.6", - "ora": "^5.4.0", - "path-key": "^3.1.1", - "prompts": "^2.2.1", - "rimraf": "^5.0.0", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.1.1", - "which": "^2.0.2", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "rimraf": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", - "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", - "dev": true, - "requires": { - "glob": "^10.2.5" - }, - "dependencies": { - "glob": { - "version": "10.2.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.7.tgz", - "integrity": "sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2", - "path-scurry": "^1.7.0" - } - } - } - } - } - }, - "elm-taskport": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/elm-taskport/-/elm-taskport-2.0.1.tgz", - "integrity": "sha512-8UgIjzmGuoU6Wt6VC0tkJnzvc5xHL5yH7GdN+/QNxaaA3ckoyveCtV0QJqNCRa42bpyR1JhOwLSApwf2Id5xZg==" - }, - "elm-tooling": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.15.0.tgz", - "integrity": "sha512-quRE5LGJyrkPBoJ3MvFQ5RGgf80J0L0d3NkduStvXh4TmZuMXNC3Z/l2ZRoq2UTUaNWeYfO1Zx5wns1AvsTrnw==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encoding-japanese": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz", - "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true - }, - "esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "esbuild-plugin-wasm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/esbuild-plugin-wasm/-/esbuild-plugin-wasm-1.1.0.tgz", - "integrity": "sha512-0bQ6+1tUbySSnxzn5jnXHMDvYnT0cN/Wd4Syk8g/sqAIJUg7buTIi22svS3Qz6ssx895NT+TgLPb33xi1OkZig==", - "dev": true - }, - "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "dependencies": { - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - } - } - }, - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - }, - "esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" - }, - "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "requires": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "dependencies": { - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" - } - } - }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-text-encoding": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", - "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" - }, - "fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", - "requires": { - "punycode": "^1.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" - } - } - }, - "fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" - }, - "file-type": { - "version": "19.6.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", - "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", - "requires": { - "get-stream": "^9.0.1", - "strtok3": "^9.0.1", - "token-types": "^6.0.0", - "uint8array-extras": "^1.3.0" - }, - "dependencies": { - "get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "requires": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - } - }, - "is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==" - } - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "folder-hash": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.3.tgz", - "integrity": "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "graceful-fs": "~4.2.0", - "minimatch": "~3.0.4" - }, - "dependencies": { - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "dependencies": { - "signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true - } - } - }, - "formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "requires": { - "fetch-blob": "^3.1.2" - } - }, - "fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "requires": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "gren-compiler-library": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/gren-compiler-library/-/gren-compiler-library-0.3.0.tgz", - "integrity": "sha512-CvcdoHYJuqwc4BUKvQPPhtegTUmW5I6rhQlKxxmu+qRbDYFeB/cypM3Xha3aqJr9l14JdmL5fMichENRqg3CBg==", - "dev": true, - "requires": { - "follow-redirects": "^1.15.2", - "xdg-basedir": "^5.1.0" - } - }, - "gren-lang": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/gren-lang/-/gren-lang-0.3.0.tgz", - "integrity": "sha512-Gl/0jckyK97BH/lUPj68vP4m05d1QTrLlhZ2ELQgKfDJu8PVKeG+LtRRkdpIzcncCfgR/htWdwt2UkwpPXzalA==", - "dev": true, - "requires": { - "gren-compiler-library": "0.3.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.2" - } - }, - "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, - "requires": { - "function-bind": "^1.1.2" - } - }, - "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "requires": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - } - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true - }, - "is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true - }, - "is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-port-reachable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", - "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==" - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "jackspeak": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz", - "integrity": "sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, - "jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "just-install": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/just-install/-/just-install-2.0.2.tgz", - "integrity": "sha512-zH6aon3V2P8ZbD+njaMB/orHsOyFMgONSpxKtbovNu7Bhb1rD9qhnMkT2Nj91++b9GgqHNbozhUdIMxecmWJaw==", - "dev": true, - "requires": { - "extract-zip": "^2.0.1", - "node-fetch": "^3.3.2" - }, - "dependencies": { - "node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - } - } - }, - "keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "requires": { - "immediate": "~3.0.5" - } - }, - "lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "load-script2": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/load-script2/-/load-script2-2.0.6.tgz", - "integrity": "sha512-pyuw/AR+ycZkRKgkMyXOIf/o2OnRQPc9grjZ04wVOeoJ7SqX97WlcZahl+3/r5qi09L/5d0iCVZ0q4OqNT/v0Q==" - }, - "localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "requires": { - "lie": "3.1.1" - }, - "dependencies": { - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "requires": { - "immediate": "~3.0.5" - } - } - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" - }, - "media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" - }, - "mediainfo.js": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/mediainfo.js/-/mediainfo.js-0.3.3.tgz", - "integrity": "sha512-+k6cobEQ0Ies/XajglElyHZyguxzEfnztGrZ+bEcuJ4gK74MeN1T0aX1DIFAMRwmFsnJlTJVTiOHJHWZssEJjA==", - "requires": { - "yargs": "^17.7.2" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "minipass": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz", - "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==", - "dev": true - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "music-metadata": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-10.6.0.tgz", - "integrity": "sha512-YRTM8KhXZHe1zBsBKA+1JwZBR7SYP/6S/9tm+FRne9dD6SdC6F+s3e5+OugwaChh/zxmAb/yw6SaLAhHZxvIiA==", - "requires": { - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.3.7", - "file-type": "^19.6.0", - "media-typer": "^1.1.0", - "strtok3": "^9.0.1", - "token-types": "^6.0.0", - "uint8array-extras": "^1.4.0" - } - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true - }, - "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true - }, - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "requires": { - "path-key": "^3.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "requires": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "dependencies": { - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - } - } - }, - "ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "requires": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - } - }, - "p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-scurry": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz", - "integrity": "sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==", - "dev": true, - "requires": { - "lru-cache": "^9.1.1", - "minipass": "^5.0.0 || ^6.0.2" - }, - "dependencies": { - "lru-cache": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz", - "integrity": "sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==", - "dev": true - } - } - }, - "path-to-regexp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", - "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "peek-readable": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.3.1.tgz", - "integrity": "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==" - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true - }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true - }, - "postcss": { - "version": "8.4.34", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.34.tgz", - "integrity": "sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==", - "dev": true, - "requires": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-custom-properties": { - "version": "13.3.4", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.4.tgz", - "integrity": "sha512-9YN0gg9sG3OH+Z9xBrp2PWRb+O4msw+5Sbp3ZgqrblrwKspXVQe5zr5sVqi43gJGwW/Rv1A483PRQUzQOEewvA==", - "dev": true, - "requires": { - "@csstools/cascade-layer-name-parser": "^1.0.7", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-import": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.0.0.tgz", - "integrity": "sha512-e77lhVvrD1I2y7dYmBv0k9ULTdArgEYZt97T4w6sFIU5uxIHvDFQlKgUUyY7v7Barj0Yf/zm5A4OquZN7jKm5Q==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - }, - "postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, - "postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", - "dev": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" - } - }, - "postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.11" - } - }, - "postcss-selector-parser": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz", - "integrity": "sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==" - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" - } - } - }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "requires": { - "pify": "^2.3.0" - } - }, - "readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "requires": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", - "requires": { - "rc": "^1.0.1" - } - }, - "remotestoragejs": { - "version": "2.0.0-beta.6", - "resolved": "https://registry.npmjs.org/remotestoragejs/-/remotestoragejs-2.0.0-beta.6.tgz", - "integrity": "sha512-xGBmCPe99F4Gs/IEEzvGFjlmOkG48rRCSFz4+7xFNVs4WUJ3nzkm+fQbKelTB2HHErgOLSIyxc2dloqhI5srAQ==", - "requires": { - "@types/node": "16.11.59", - "@types/tv4": "^1.2.29", - "esm": "^3.2.25", - "fsevents": "^2.3.2", - "tv4": "^1.3.0", - "webfinger.js": "^2.7.1", - "xhr2": "^0.2.1" - }, - "dependencies": { - "@types/node": { - "version": "16.11.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.59.tgz", - "integrity": "sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw==" - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "requires": { - "lowercase-keys": "^2.0.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "serve": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.1.tgz", - "integrity": "sha512-48er5fzHh7GCShLnNyPBRPEjs2I6QBozeGr02gaacROiyS/8ARADlj595j39iZXAqBbJHH/ivJJyPRWY9sQWZA==", - "requires": { - "@zeit/schemas": "2.29.0", - "ajv": "8.11.0", - "arg": "5.0.2", - "boxen": "7.0.0", - "chalk": "5.0.1", - "chalk-template": "0.4.0", - "clipboardy": "3.0.0", - "compression": "1.7.4", - "is-port-reachable": "4.0.0", - "serve-handler": "6.1.5", - "update-check": "1.5.4" - }, - "dependencies": { - "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==" - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - } - } - }, - "serve-handler": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz", - "integrity": "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==", - "requires": { - "bytes": "3.0.0", - "content-disposition": "0.5.2", - "fast-url-parser": "1.1.3", - "mime-types": "2.1.18", - "minimatch": "3.1.2", - "path-is-inside": "1.0.2", - "path-to-regexp": "2.2.1", - "range-parser": "1.2.0" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "~1.33.0" - } - } - } - }, - "set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", - "dev": true, - "requires": { - "define-data-property": "^1.1.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "stream": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz", - "integrity": "sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==", - "dev": true, - "requires": { - "component-emitter": "^2.0.0" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "strtok3": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.0.1.tgz", - "integrity": "sha512-ERPW+XkvX9W2A+ov07iy+ZFJpVdik04GhDA4eVogiG9hpC97Kem2iucyzhFxbFRvQ5o2UckFtKZdp1hkGvnrEw==", - "requires": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.3.1" - } - }, - "sucrase": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", - "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", - "dev": true, - "requires": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "dependencies": { - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - } - } - }, - "tailwindcss-animations": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tailwindcss-animations/-/tailwindcss-animations-2.0.0.tgz", - "integrity": "sha512-YLdhPiQeDtpnqWd0J70rvy08MDbsq+BBLrzMmPR1Pe6MJDFYDuk4CYGI14GVgj7xRBGkByzp/rAfBcUWD7zpng==", - "dev": true, - "requires": { - "lodash": "^4.17.15" - } - }, - "tailwindcss-interaction-variants": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tailwindcss-interaction-variants/-/tailwindcss-interaction-variants-5.0.0.tgz", - "integrity": "sha512-Tmx9HjwuNA3G7cLUcebGkNchKkNVIZXBroxa0LBoq/KUb/xEKEM8qugUG7QczSNPhaZzpXmo3IzPkRhf4tDaOQ==", - "dev": true, - "requires": { - "lodash": "^4.17.19", - "postcss-selector-parser": "^6.0.2" - } - }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "throttle-debounce": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", - "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==" - }, - "timer.js": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/timer.js/-/timer.js-1.0.4.tgz", - "integrity": "sha512-UU/lL02CjPMu8cu29h/uyt5yBqcm0vlx51Je9xCmVcH+TLS+X/+chv4V4rHlb37ZiI4iUVyLuC3QvyoFMzadwQ==" - }, - "tiny-parse-argv": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tiny-parse-argv/-/tiny-parse-argv-2.4.0.tgz", - "integrity": "sha512-WTEsnmeHNr99hLQIDA+gnsS+fDsCDITlqgI+zEhx9M6ErPt0heoNZ1PGvql6wcf95sIx40J0MLYXaPveGwtpoA==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tocca": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/tocca/-/tocca-2.0.9.tgz", - "integrity": "sha512-FXofUGAFcgEIEOS3m9Dk9URaY3x+JeerPfVFc8K820PjV0JcCsH98BFxrVOTANC8YvM4QTrmKMol2/818gFmCw==" - }, - "token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", - "requires": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - } - }, - "ts-api-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", - "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", - "dev": true, - "requires": {} - }, - "ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "tv4": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", - "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==" - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "dev": true, - "peer": true - }, - "uint8array-extras": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==" - }, - "uint8arrays": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.10.tgz", - "integrity": "sha512-AnJNUGGDJAgFw/eWu/Xb9zrVKEGlwJJCaeInlf3BkecE/zcTobk5YXYIPNQJO1q5Hh1QZrQQHf0JvcHqz2hqoA==", - "requires": { - "multiformats": "^12.0.1" - }, - "dependencies": { - "multiformats": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-12.0.1.tgz", - "integrity": "sha512-s01wijBJoDUqESWSzePY0lvTw7J3PVO9x2Cc6ASI5AMZM2Gnhh7BC17+nlFhHKU7dDzaCaRfb+NiqNzOsgPUoQ==" - } - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", - "dev": true, - "requires": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - } - }, - "update-check": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", - "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", - "requires": { - "registry-auth-token": "3.3.2", - "registry-url": "3.1.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "web-streams-polyfill": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", - "dev": true - }, - "webfinger.js": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/webfinger.js/-/webfinger.js-2.7.1.tgz", - "integrity": "sha512-H4RokaE4RC39N3uiRTcjKMmy6yYg06lUPORQHvv8DjowZ6I5SalxUeoqHbtTN3EVBmYP/XHQ8Ow6BLEIpe2DtA==", - "requires": { - "xhr2": "0.2.1" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "requires": { - "string-width": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "dev": true - }, - "xhr2": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", - "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==" - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "dev": true - }, - "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index b4a27019d..000000000 --- a/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "name": "diffuse", - "description": "A music player that connects to your cloud/distributed storage", - "version": "3.5.0", - "author": "Steven Vandevelde ", - "homepage": "https://diffuse.sh", - "repository": "github:icidasset/diffuse", - "license": "SEE LICENSE IN LICENSE", - "devDependencies": { - "@tauri-apps/api": "^2.0.0-beta.0", - "@tauri-apps/cli": "^2.0.0-beta.0", - "@tauri-apps/plugin-dialog": "^2.0.0-beta.0", - "@tauri-apps/plugin-fs": "^2.0.0-beta.0", - "@tauri-apps/plugin-shell": "^2.0.0-beta.0", - "@types/elm": "^0.19.3", - "@types/file-saver": "^2.0.7", - "@types/lunr": "^2.3.7", - "@types/throttle-debounce": "^5.0.2", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "assert": "^2.1.0", - "autoprefixer": "^10.4.19", - "buffer": "^6.0.3", - "elm": "0.19.1-6", - "elm-format": "^0.8.7", - "elm-review": "^2.10.3", - "esbuild": "^0.20.2", - "esbuild-plugin-wasm": "^1.1.0", - "eslint": "^8.56.0", - "events": "^3.3.0", - "gren-lang": "^0.3.0", - "just-install": "^2.0.2", - "postcss": "^8.4.34", - "postcss-custom-properties": "^13.3.4", - "postcss-import": "^16.0.0", - "stream": "^0.0.3", - "tailwindcss": "^3.4.1", - "tailwindcss-animations": "^2.0.0", - "tailwindcss-interaction-variants": "^5.0.0", - "tiny-parse-argv": "^2.4.0" - }, - "dependencies": { - "@tokenizer/http": "^0.9.1", - "@tokenizer/range": "^0.10.0", - "elm-taskport": "^2.0.1", - "encoding-japanese": "^2.0.0", - "fast-text-encoding": "^1.0.6", - "file-saver": "^2.0.2", - "jszip": "^3.7.1", - "load-script2": "^2.0.5", - "localforage": "^1.10.0", - "lunr": "^2.3.8", - "mediainfo.js": "0.3.3", - "music-metadata": "^10.6.0", - "readable-stream": "^4.5.2", - "remotestoragejs": "^2.0.0-beta.6", - "serve": "^14.2.1", - "throttle-debounce": "^5.0.0", - "timer.js": "^1.0.4", - "tocca": "^2.0.9", - "uint8arrays": "^4.0.10" - }, - "scripts": { - "tauri": "tauri" - } -} diff --git a/specs/components/SPEC.md b/specs/components/SPEC.md new file mode 100644 index 000000000..f6fe27943 --- /dev/null +++ b/specs/components/SPEC.md @@ -0,0 +1,67 @@ +# Components + +These web components are custom elements (DOM elements) that serve as building blocks to make web software. + +## Requirements + +- Components should never rely on instances of other components, except if they live in the 'orchestrator' category or if the instance is a child of itself. Orchestrator components can refer to other components using a DOM selector, this can be provided via an attribute ending with `-selector` as its name. These orchestrators are responsible for bringing components together and making prebuilt setups. +- Components rely on signals exposed via the `common/signal.js` module. Only signal getters are exposed, never the entire signal object. +- A base class should be built that all components build on. This should be made available via `common/element.js`. +- Components should be broadcastable, meaning that various instances of that component should be able to communicate with each other. This will be used to replicate state and call methods. Broadcasting should have two modes: 'replicate' and 'leaderOnly'. The former is executed on all instances, while the latter is only executed on the leader. Another base class should be provided for broadcastable components. A `group` attribute will be used to choose the group in which we'll broadcast, so can we can form groups of instances that communicate. +- Components live in the `components` directory. +- Another special category of components is 'configurator', those components take DOM children of the same category. For example, A configurator must have at least the same properties as the component provided via the DOM, but may have more. +- If heavy computation work or other work which could block the main thread is needed, the element should be accompanied by a web worker. +- A tunnel should be able to be formed between workers using message ports, passing the message port along using `postMessage`. +- Communication between different threads/workers should be performant. +- Register the custom element as soon as possible. Ideally using the `defineElement` helper to prevent double registration. +- When any namespace is needed, prefer to prefix with `diffuse/` +- When code is shared between files from a specific component, components in a specific category, or components across different categories; put it in a `common.js` file (or `common` directory with multiple js files if it gets too big) on the appropriate level. +- Tag naming convention: all lowercase, starts with letter `d`, then the first letter of each sub category, a dash, and finally the name of the element in kebab case. Collisions may occur with the prefix, in that case, change the element name. +- All elements should have a `static NAME` property with the directory path in src prefixed with `diffuse/`. + +## Other categories + +### Artwork + +Various ways to fetch artwork for tracks. Must adhere to types defined in `artwork/types.d.ts` + + +### Engine + +All kinds of core behaviour. + + +### Input + +Input components generate tracks and resolve track URIs into audio. They must adhere to types defined in `input/types.d.ts` + + +### Metadata + +Various ways to fetch metadata for tracks. Must adhere to types defined in `metadata/types.d.ts` + + +### Upload + +Upload components handle uploading audio files to cloud storage (e.g. Dropbox) and deleting them again. They are paired with input components by scheme: an upload component's `SCHEME` matches the input component that can resolve the URIs it produces. `upload()` returns a URI carrying that scheme, which is stored on a track; the matching input component can then resolve it, and `delete()` the file remotely. Must adhere to types defined in `upload/types.d.ts` + + +### Output + +Output components hold all the user data: facets, playlist items, settings and tracks. They all have the same surface API. Data is exposed via a signal getter called `collection`, one for each kind of data. These components are also responsible loading and saving that data. The data may be encoded, each component decides for its own what the type of the data is. The subcategory states what that type is and this is reflected in the directory structure these components live in: + +- `polymorphic`: The data can be of multiple types. +- `raw`: The data is not encoded at all, it is of the same type stated in the definitions (lexicons). +- `bytes`: Encoded as `Uint8Array`. + +Must adhere to types defined in `output/types.d.ts` + + +### Supplement + +All kinds of additional components, not really required to make a working audio player application. Scrobbling for example. + + +### Transformer + +Transformers are also intermediate components like configurators, with the same API requirements. They don't take DOM elements as children however, instead they serve to be used as a chain. Output transformers have a `output-selector` which chains the transformer to another component. These output transformers also have the same subcategories and directory structure as the output category. diff --git a/specs/components/artwork/SPEC.md b/specs/components/artwork/SPEC.md new file mode 100644 index 000000000..4a4dc2dbb --- /dev/null +++ b/specs/components/artwork/SPEC.md @@ -0,0 +1,12 @@ +# Artwork components + +These components fetch artwork for a `Track`, if possible. + + +## Requirements + +- Every artwork component must be of the `ArtworkElement` type (see `types.d.ts`) +- The `get` action must never throw, return `null` if anything fails. This `null` means "no artwork found". +- Prefer the quick failure resolution path. For example, when the artwork fetching requires an internet connection; don't retry the fetch, return `null` immediately. +- Ideally the best quality is picked for the images, unless that image size is insanely large (ie. larger than a 4K resolution) +- Follows naming conventions specified in the top-level spec. diff --git a/specs/components/artwork/audio-metadata/SPEC.md b/specs/components/artwork/audio-metadata/SPEC.md new file mode 100644 index 000000000..a1cf08275 --- /dev/null +++ b/specs/components/artwork/audio-metadata/SPEC.md @@ -0,0 +1,8 @@ +# Audio metadata artwork + +This component implements an artwork component that retrieves artwork from an audio file using the resolved input url or stream. + + +## Requirements + +- An `input-selector` attribute must be present so the component can reference the input component (used to resolve the track's url/stream). diff --git a/specs/components/artwork/input/SPEC.md b/specs/components/artwork/input/SPEC.md new file mode 100644 index 000000000..484108c71 --- /dev/null +++ b/specs/components/artwork/input/SPEC.md @@ -0,0 +1,8 @@ +# Input artwork + +This component implements an artwork component that retrieves artwork from a given input component (see types). + + +## Requirements + +- A `input-selector` attribute must be present to so the component can reference the input component. diff --git a/specs/components/artwork/last.fm/SPEC.md b/specs/components/artwork/last.fm/SPEC.md new file mode 100644 index 000000000..17bd7f78b --- /dev/null +++ b/specs/components/artwork/last.fm/SPEC.md @@ -0,0 +1,3 @@ +# Last.fm artwork + +This component implements an artwork component that retrieves artwork from the [last.fm](https://last.fm) API. diff --git a/specs/components/artwork/musicbrainz/SPEC.md b/specs/components/artwork/musicbrainz/SPEC.md new file mode 100644 index 000000000..f8a70aa4a --- /dev/null +++ b/specs/components/artwork/musicbrainz/SPEC.md @@ -0,0 +1,3 @@ +# MusicBrainz artwork + +This component implements an artwork component that retrieves artwork from the [musicbrainz.org](https://musicbrainz.org) API. diff --git a/specs/components/artwork/types.d.ts b/specs/components/artwork/types.d.ts new file mode 100644 index 000000000..7425ec4c6 --- /dev/null +++ b/specs/components/artwork/types.d.ts @@ -0,0 +1,9 @@ +import type { Track } from "~/definitions/types.d.ts"; +import type { DiffuseElement } from "~/common/element.js"; +import type { ProxiedActions } from "~/common/worker.d.ts"; + +export type Actions = { + get(track: Track): Promise; +}; + +export type ArtworkElement = DiffuseElement & ProxiedActions; diff --git a/specs/components/configurator/artwork/types.d.ts b/specs/components/configurator/artwork/types.d.ts new file mode 100644 index 000000000..ee7974a4e --- /dev/null +++ b/specs/components/configurator/artwork/types.d.ts @@ -0,0 +1,3 @@ +import type { Actions } from "@specs/components/artwork/types.d.ts"; + +export type { Actions }; diff --git a/specs/components/configurator/input/types.d.ts b/specs/components/configurator/input/types.d.ts new file mode 100644 index 000000000..14a63feb7 --- /dev/null +++ b/specs/components/configurator/input/types.d.ts @@ -0,0 +1,8 @@ +import type {InputActions} from "@specs/components/input/types.d.ts" + +export type Actions = InputActions & { + cache(uris: string[]): Promise + cacheBlob(blob: Blob): Promise + listCached(): Promise + removeFromCache(uris: string[]): Promise +}; diff --git a/specs/components/configurator/metadata/types.d.ts b/specs/components/configurator/metadata/types.d.ts new file mode 100644 index 000000000..bab0470be --- /dev/null +++ b/specs/components/configurator/metadata/types.d.ts @@ -0,0 +1 @@ +export type { Actions } from "@specs/components/metadata/types.d.ts"; diff --git a/specs/components/configurator/output/types.d.ts b/specs/components/configurator/output/types.d.ts new file mode 100644 index 000000000..57a76f324 --- /dev/null +++ b/specs/components/configurator/output/types.d.ts @@ -0,0 +1,26 @@ +import type { OutputElement } from "@specs/components/output/types.d.ts"; +import type { SignalReader } from "~/common/signal.d.ts"; + +export type OutputConfiguratorElement = + & OutputElement + & { + deselect: () => Promise; + hasDefault: () => boolean; + hasSelected: () => boolean; + loadSelected: () => Promise; + options: () => Promise>>; + select: (id: string) => Promise; + waitForOption: (label: string) => Promise>; + + /** Output-element ids that have been selected earlier. */ + activated: SignalReader>; + + /** Selected output element. */ + selected: SignalReader; + }; + +export type OutputOption = { + id: string; + label: string; + element: ElementType; +}; diff --git a/specs/components/configurator/upload/types.d.ts b/specs/components/configurator/upload/types.d.ts new file mode 100644 index 000000000..3f3451e32 --- /dev/null +++ b/specs/components/configurator/upload/types.d.ts @@ -0,0 +1,3 @@ +import type {UploadActions} from "@specs/components/upload/types.d.ts" + +export type Actions = UploadActions; diff --git a/specs/components/engine/audio/SPEC.md b/specs/components/engine/audio/SPEC.md new file mode 100644 index 000000000..f62937f69 --- /dev/null +++ b/specs/components/engine/audio/SPEC.md @@ -0,0 +1,21 @@ +# Audio engine + +This component is responsible for playing audio in the browser or web view. This component does not depend on any Diffuse-specific data structures. + +## Requirements + +- It should be able to play audio from a URL to a binary audio file, or an audio stream. +- When given an URL, it should not load the entire audio file into memory. +- Seeking should be enabled if possible (obviously not feasible for live streams). +- The element should operate in broadcast mode when a group attribute is present. +- Broadcasting the audio element allows other browser tabs to monitor its playback progress and state, volume, etc. +- When the broadcast leader tab gets closed, another tab will take the leader role and when that happens; if the audio was playing, the audio must start playing in the tab of the new leader. +- Consumers of this custom element must be able to tap into the code that allows for audio visualisations. Though it is not required to work simultaneously on all browser tabs. +- The volume of the audio must be able to be set and persisted across sessions. This value depends on the used `group`. +- The mime type must be set on the audio if provided. +- The element must support a `initial-progress` attribute which can be used to start playing audio from that particular point. +- Audio must be able to preloaded in the background. + +## Implementation specific + +- When using a HTML audio element, use a silent audio file data-uri as the `src` to cancel the loading of any data. diff --git a/specs/components/engine/audio/types.d.ts b/specs/components/engine/audio/types.d.ts new file mode 100644 index 000000000..65a4fb417 --- /dev/null +++ b/specs/components/engine/audio/types.d.ts @@ -0,0 +1,84 @@ +import type { Signal, SignalReader } from "~/common/signal.d.ts"; +import type { Track } from "~/definitions/types.d.ts"; + +export type Actions = { + adjustVolume: (_: { audioId?: string; volume: number }) => void; + pause: (_: { audioId: string }) => void; + play: (_: { audioId: string; volume?: number }) => void; + reload: (_: { audioId: string; play: boolean; progress?: number }) => void; + seek: ( + _: { currentTime?: number; audioId: string; percentage?: number }, + ) => void; + state: (audioId: string) => AudioStateReadOnly | undefined; + supply: ( + _: { audio: Audio[]; play?: { audioId: string; volume?: number } }, + ) => void; +}; + +export type Audio = AudioUrl | AudioStream; + +export type AudioUrl = AudioBase & { url: string }; + +export type AudioStream = AudioBase & { + stream: ReadableStream; + + /** Total duration in seconds. */ + duration?: number; + + /** Return a new stream starting at the given time. Required for seeking on stream items. */ + seek?: (timeSeconds: number) => Promise; +}; + +export type AudioBase = { + id: string; + isPreload: boolean; + mimeType?: string; + /** + * Initial progress + */ + progress?: number; + track: Track; +}; + +export type AudioState = { + currentTime: Signal; + duration: Signal; + hasEnded: Signal; + isPlaying: Signal; + isPreload: Signal; + loadingState: Signal; + + // Computed + progress: SignalReader; +}; + +export type AudioStateReadOnly = { + id: string; + url: string; + mimeType: string | undefined; + + /** https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/currentTime */ + currentTime: SignalReader; + duration: SignalReader; + hasEnded: SignalReader; + isPlaying: SignalReader; + isPreload: SignalReader; + loadingState: SignalReader; + + // Computed + progress: SignalReader; +}; + +export type LoadingState = + | "initialisation" + | "loading" + | "loaded" + | { + error: { code: number }; + }; + +export type State = { + isPlaying: SignalReader; + items: SignalReader; + volume: SignalReader; +}; diff --git a/specs/components/engine/queue/SPEC.md b/specs/components/engine/queue/SPEC.md new file mode 100644 index 000000000..9d2eedd2b --- /dev/null +++ b/specs/components/engine/queue/SPEC.md @@ -0,0 +1,15 @@ +# Queue engine + +This is a simple queue component to queue up audio to play in the future, keep track of what is playing now, and what has played in the past. + + +## Requirements + +- Keep track of what to play in the future, what is playing now (if anything), and what has played in the past. Keep this state in signals so UIs can reflect state changes. +- Must be able to differentiate between "manual" and "automatic" items. The former is added by the user manually through a UI action using, for example, the `add` method. The latter is an automatic trigger, eg. where the queue is filled up automatically for infinite playback. +- The queue can be "filled" with items that were "supplied" earlier, may be shuffled. +- Items have a `key` property which can be used to remove (`expel`) it or `move` it. +- `shift` method takes the first item from the future list and sets it as "now playing". If something was already in the "now playing" state, move that to past list as the last item. +- `unshift` is the inverse of `shift`. +- Must be able to add items in front of the queue, as well as the end of the queue (beginning and ending of the future list respectively). +- Should also be able to `clear` items from the queue, be it automatic items, manual items, or both at the same time. diff --git a/specs/components/engine/queue/types.d.ts b/specs/components/engine/queue/types.d.ts new file mode 100644 index 000000000..67f7cf924 --- /dev/null +++ b/specs/components/engine/queue/types.d.ts @@ -0,0 +1,39 @@ +import type { SignalReader } from "~/common/signal.d.ts"; + +export type Actions = { + add: (args: { inFront?: boolean; trackIds: string[] }) => void; + /** + * Clear the `future()` items. + */ + clear: (args: { keepManual?: boolean }) => void; + expel: (args: { key: string }) => void; + fill: ( + args: { + /** Always keep adding, even if the amount of non-manual items in the queue are passed the given `amount` */ + augment?: boolean; + amount: number; + shuffled: boolean; + }, + ) => void; + move: (args: { key: string; to: number }) => void; + shift: (args?: { by?: number }) => void; + supply: (args: { trackIds: string[] }) => void; + unshift: (args?: { by?: number }) => void; +}; + +export type Item = { + id: string; + key: string; + manualEntry: boolean; +}; + +export type State = { + future: SignalReader; + now: SignalReader; + past: SignalReader; + + /** + * Initially this is set to `undefined`, but whenever the cache is changed afterwards this will be the hash of the items in the supply. + */ + supplyFingerprint: SignalReader; +}; diff --git a/specs/components/input/opensubsonic/types.d.ts b/specs/components/input/opensubsonic/types.d.ts new file mode 100644 index 000000000..94be2a0ea --- /dev/null +++ b/specs/components/input/opensubsonic/types.d.ts @@ -0,0 +1,26 @@ +// https://opensubsonic.netlify.app/docs/api-reference/ +export type Server = { + apiKey?: string; + host: string; + password?: string; + tls: boolean; + username?: string; +}; + +export interface SubsonicConfig { + /** The base URL of the Subsonic server, e.g., https://demo.navidrome.org. */ + url: string; + + /** The authentication details to use when connecting to the server. */ + auth: + | { + username: string; + password: string; + apiKey?: never; + } + | { + username?: never; + password?: never; + apiKey: string; + }; +} diff --git a/specs/components/input/s3/types.d.ts b/specs/components/input/s3/types.d.ts new file mode 100644 index 000000000..0bfe80f67 --- /dev/null +++ b/specs/components/input/s3/types.d.ts @@ -0,0 +1,15 @@ +import type { Track } from "~/definitions/types.d.ts"; + +export type Bucket = { + accessKey: string; + bucketName: string; + host: string; + path: string; + region: string; + secretKey: string; +}; + +export type Demo = { + bucket: Bucket; + track: Track; +}; diff --git a/specs/components/input/types.d.ts b/specs/components/input/types.d.ts new file mode 100644 index 000000000..780dfb303 --- /dev/null +++ b/specs/components/input/types.d.ts @@ -0,0 +1,70 @@ +import type { ProxiedActions } from "~/common/worker.d.ts"; + +import type { Track } from "~/definitions/types.d.ts"; +import type { DiffuseElement } from "~/common/element.js"; + +/** + * Consultation. + * + * `consult` can be "undetermined" if only a scheme was given + * instead of a full URI. + */ +export type Consult = + | { supported: false; reason: string } + | { supported: true; consult: "undetermined" | ConsultResult }; + +/** + * Tri-state availability for a single source. + * + * - `"yes"` → the source confirmed it is reachable; + * - `"no"` → the source explicitly rejected (e.g. HTTP 404, auth fail); + * - `"unsure"` → the consult was inconclusive (network blip, timeout, + * aborted fetch). Consult results are never cached in + * this state, and consumers should treat the source + * optimistically rather than hiding it. + */ +export type ConsultResult = "yes" | "no" | "unsure"; + +export type ConsultGrouping = { + available: ConsultResult; + reason?: string; + scheme: string; + uris: string[]; +}; + +export type GroupConsult = Record; + +export type InputActions = { + artwork(uri: string): Promise; + consult(uriOrScheme: string): Promise; + detach(args: { fileUriOrScheme: string; tracks: Track[] }): Promise; + groupConsult(uris: string[]): Promise; + list(tracks: Track[]): Promise; + resolve(args: { method?: string; uri: string }): Promise; +}; + +export type InputElement = + & DiffuseElement + & InputSchemeProvider + & ProxiedActions + & { sources: (tracks: Track[]) => Source[] }; + +export type InputSchemeProvider = { SCHEME: string }; + +export type ResolvedUri = undefined | ResolveUriAsUrl | ResolveUriAsStream; + +export type ResolveUriAsUrl = { + expiresAt: number; + url: string; +}; + +export type ResolveUriAsStream = { + expiresAt: number; + mimeType: string; + stream: ReadableStream; + + /** Total duration in seconds. */ + duration?: number; +}; + +export type Source = { label: string; uri: string }; diff --git a/specs/components/input/webdav/types.d.ts b/specs/components/input/webdav/types.d.ts new file mode 100644 index 000000000..0f1f87583 --- /dev/null +++ b/specs/components/input/webdav/types.d.ts @@ -0,0 +1,6 @@ +export type Server = { + username: string; + password: string; + host: string; + dir: string; +}; diff --git a/specs/components/metadata/audio-file/types.d.ts b/specs/components/metadata/audio-file/types.d.ts new file mode 100644 index 000000000..d15ec575f --- /dev/null +++ b/specs/components/metadata/audio-file/types.d.ts @@ -0,0 +1,10 @@ +import type { IPicture } from "music-metadata"; +import type { TrackStats, TrackTags } from "~/definitions/types.d.ts"; + +export type Extraction = { + artwork?: IPicture[]; + stats?: TrackStats; + tags?: TrackTags; +}; + +export type Urls = { get: string; head: string }; diff --git a/specs/components/metadata/spectrogram/types.d.ts b/specs/components/metadata/spectrogram/types.d.ts new file mode 100644 index 000000000..4c3f31bc9 --- /dev/null +++ b/specs/components/metadata/spectrogram/types.d.ts @@ -0,0 +1,26 @@ +import type { TrackStats } from "~/definitions/types.d.ts"; + +/** + * The spectral descriptors derived from a track's spectrogram. These are the + * spectrogram-analysis fields stored on `TrackStats`. + */ +export type SpectralStats = { + spectralCentroid: number; + spectralRolloff: number; + spectralSpread: number; + spectralFlatness: number; + spectralFlux: number; +}; + +/** + * The spectrogram component re-uses the standard metadata `patch` action, + * merging its spectral stats into the track's existing `stats`. + */ +export type SpectralTrackStats = Pick< + TrackStats, + | "spectralCentroid" + | "spectralRolloff" + | "spectralSpread" + | "spectralFlatness" + | "spectralFlux" +>; diff --git a/specs/components/metadata/types.d.ts b/specs/components/metadata/types.d.ts new file mode 100644 index 000000000..ed6d3b458 --- /dev/null +++ b/specs/components/metadata/types.d.ts @@ -0,0 +1,9 @@ +import type { DiffuseElement } from "~/common/element.js"; +import type { ProxiedActions } from "~/common/worker.d.ts"; +import type { Track } from "~/definitions/types.d.ts"; + +export type Actions = { + patch(track: Track): Promise; +}; + +export type MetadataElement = DiffuseElement & ProxiedActions; diff --git a/specs/components/orchestrator/artwork/types.d.ts b/specs/components/orchestrator/artwork/types.d.ts new file mode 100644 index 000000000..e64589df8 --- /dev/null +++ b/specs/components/orchestrator/artwork/types.d.ts @@ -0,0 +1,4 @@ +export type Artwork = { + bytes: Uint8Array; + mime: string; +}; diff --git a/specs/components/orchestrator/favourites/types.d.ts b/specs/components/orchestrator/favourites/types.d.ts new file mode 100644 index 000000000..bceae3216 --- /dev/null +++ b/specs/components/orchestrator/favourites/types.d.ts @@ -0,0 +1,13 @@ +import type { PlaylistItem, Track } from "~/definitions/types.d.ts"; + +export type Actions = { + include(args: { playlistItems: PlaylistItem[]; tracks: Track[] }): Promise< + PlaylistItem[] | null + >; + expel(args: { playlistItems: PlaylistItem[]; tracks: Track[] }): Promise< + PlaylistItem[] | null + >; + toggle(args: { playlistItems: PlaylistItem[]; tracks: Track[] }): Promise< + PlaylistItem[] | null + >; +}; diff --git a/specs/components/orchestrator/process-tracks/types.d.ts b/specs/components/orchestrator/process-tracks/types.d.ts new file mode 100644 index 000000000..b6bf8ff65 --- /dev/null +++ b/specs/components/orchestrator/process-tracks/types.d.ts @@ -0,0 +1,11 @@ +import type { Track } from "~/definitions/types.d.ts"; + +export type Progress = { + processed: number; + total: number; +}; + +export type Actions = { + process: (args: { tracks: Track[]; disabledUris: string[] }) => Promise; + progress: () => Progress; +}; diff --git a/specs/components/orchestrator/scoped-tracks/types.d.ts b/specs/components/orchestrator/scoped-tracks/types.d.ts new file mode 100644 index 000000000..234ecefbf --- /dev/null +++ b/specs/components/orchestrator/scoped-tracks/types.d.ts @@ -0,0 +1,22 @@ +import type { Orama, SearchParams } from "@orama/orama"; + +import type { SignalReader } from "~/common/signal.d.ts"; +import type { Track } from "~/definitions/types.d.ts"; +import type { SCHEMA } from "~/components/orchestrator/scoped-tracks/constants.js"; + +export type Actions = { + /** + * https://docs.orama.com/docs/orama-js/search + */ + search(params: SearchParams): Promise; + supply(args: { tracks: Track[] }): Promise; +}; + +export type Schema = Orama; + +export type State = { + /** + * Initially this is set to `undefined`, but whenever the cache is changed afterwards this will be the hash of the items in the supply. + */ + supplyFingerprint: SignalReader; +}; diff --git a/specs/components/output/bytes/dropbox/types.d.ts b/specs/components/output/bytes/dropbox/types.d.ts new file mode 100644 index 000000000..ac8ba153f --- /dev/null +++ b/specs/components/output/bytes/dropbox/types.d.ts @@ -0,0 +1,26 @@ +import type { SignalReader } from "~/common/signal.d.ts"; +import type { OutputElement } from "@specs/components/output/types.d.ts"; + +export type DropboxOutputElement = + & OutputElement + & { + refreshToken: SignalReader; + + getRefreshToken(): Promise; + setRefreshToken(token: string): Promise; + unsetRefreshToken(): Promise; + + authorize(): Promise; + }; + +export type DropboxOutputWorkerActions = { + get(args: { + refreshToken: string; + name: string; + }): Promise; + put(args: { + refreshToken: string; + data: Uint8Array; + name: string; + }): Promise; +}; diff --git a/specs/components/output/bytes/s3/types.d.ts b/specs/components/output/bytes/s3/types.d.ts new file mode 100644 index 000000000..eae4b2bd9 --- /dev/null +++ b/specs/components/output/bytes/s3/types.d.ts @@ -0,0 +1,22 @@ +import type { SignalReader } from "~/common/signal.d.ts"; +import type { OutputElement } from "@specs/components/output/types.d.ts"; +import type { Bucket } from "@specs/components/input/s3/types.d.ts"; + +export type S3OutputElement = + & OutputElement + & { + bucket: SignalReader; + + getBucket(): Promise; + setBucket(bucket: Bucket): Promise; + unsetBucket(): Promise; + }; + +export type S3OutputWorkerActions = { + get(args: { bucket: Bucket; name: string }): Promise; + put(args: { + bucket: Bucket; + data: Uint8Array; + name: string; + }): Promise; +}; diff --git a/specs/components/output/polymorphic/indexed-db/types.d.ts b/specs/components/output/polymorphic/indexed-db/types.d.ts new file mode 100644 index 000000000..0c35ba7e5 --- /dev/null +++ b/specs/components/output/polymorphic/indexed-db/types.d.ts @@ -0,0 +1 @@ +export type SupportedDataTypes = any; diff --git a/specs/components/output/raw/atproto-passkey/SPEC.md b/specs/components/output/raw/atproto-passkey/SPEC.md new file mode 100644 index 000000000..c5787bd83 --- /dev/null +++ b/specs/components/output/raw/atproto-passkey/SPEC.md @@ -0,0 +1,11 @@ +# AT Protocol raw output + +This element implements the output element interface using the AT Protocol (PDS). + +## Requirements + +- The definition lexicons are used as the schema for each output type (tracks, playlist items, etc). +- The AT Protocol OAuth flow is preferred as the authentication method. +- The authenticated account must be remembered across browser sessions. +- The atproto pds has strict rate limits, we must opt for data structures that take this into consideration. There can be a large amount of tracks, 25000 for example, so a bundle would probably be preferred. Same for playlist items, maybe 5000 items or more. +- Reading and writing should be done as less as possible, though we don't want to miss out on any updates. diff --git a/specs/components/output/raw/atproto-passkey/types.d.ts b/specs/components/output/raw/atproto-passkey/types.d.ts new file mode 100644 index 000000000..37941ff23 --- /dev/null +++ b/specs/components/output/raw/atproto-passkey/types.d.ts @@ -0,0 +1,16 @@ +import type { SignalReader } from "~/common/signal.d.ts"; +import type { OutputElement } from "@specs/components/output/types.d.ts"; + +export type ATProtoPasskeyOutputElement = + & OutputElement + & { + did: SignalReader; + firehoseRev: SignalReader<{ rev: string; collections: ReadonlySet } | null>; + handle: SignalReader; + rev: SignalReader; + + getLatestCommit(): Promise; + login(handle: string): Promise; + logout(): Promise; + whenRestored(): Promise; + }; diff --git a/specs/components/output/types.d.ts b/specs/components/output/types.d.ts new file mode 100644 index 000000000..31f0a6159 --- /dev/null +++ b/specs/components/output/types.d.ts @@ -0,0 +1,99 @@ +import type { Signal, SignalReader } from "~/common/signal.d.ts"; +import type { DiffuseElement } from "~/common/element.js"; +import type { + Facet, + PlaylistItem, + Setting, + Track, +} from "~/definitions/types.d.ts"; + +export type OutputCollectionState = + | { state: "loading" } + | { state: "loaded"; data: T } + | { state: "error" }; + +export type OutputElement = + & DiffuseElement + & OutputManagerDeputy; + +export type OutputManagerDeputy = + & Omit, "signals"> + & { ready: SignalReader }; + +export type OutputManager = { + facets: { + collection: SignalReader< + OutputCollectionState + >; + reload: () => Promise; + save: ( + facets: Encoding extends null ? Facet[] : Encoding, + ) => Promise; + }; + playlistItems: { + collection: SignalReader< + OutputCollectionState + >; + reload: () => Promise; + save: ( + playlistItems: Encoding extends null ? PlaylistItem[] : Encoding, + ) => Promise; + }; + settings: { + collection: SignalReader< + OutputCollectionState + >; + reload: () => Promise; + save: ( + settings: Encoding extends null ? Setting[] : Encoding, + ) => Promise; + }; + signals: { + facets: Signal; + playlistItems: Signal; + settings: Signal; + tracks: Signal; + }; + tracks: { + collection: SignalReader< + OutputCollectionState + >; + reload: () => Promise; + save: (tracks: Encoding extends null ? Track[] : Encoding) => Promise; + }; +}; + +export type OutputManagerProperties = { + facets: { + empty(): Encoding extends null ? Facet[] : Encoding; + get(): Promise; + put( + facets: Encoding extends null ? Facet[] : Encoding, + ): Promise; + }; + init?: () => Promise; + playlistItems: { + empty(): Encoding extends null ? PlaylistItem[] : Encoding; + get(): Promise; + put( + playlistItems: Encoding extends null ? PlaylistItem[] : Encoding, + ): Promise; + }; + settings: { + empty(): Encoding extends null ? Setting[] : Encoding; + get(): Promise; + put( + settings: Encoding extends null ? Setting[] : Encoding, + ): Promise; + }; + tracks: { + empty(): Encoding extends null ? Track[] : Encoding; + get(): Promise; + put(tracks: Encoding extends null ? Track[] : Encoding): Promise; + }; +}; + +export type OutputWorkerActions = { + get(args: { name: string }): Promise; + put(args: { data: DataType; name: string }): Promise; +}; diff --git a/specs/components/supplement/types.d.ts b/specs/components/supplement/types.d.ts new file mode 100644 index 000000000..01812c6e4 --- /dev/null +++ b/specs/components/supplement/types.d.ts @@ -0,0 +1,20 @@ +import type { DiffuseElement } from "~/common/element.js"; +import type { SignalReader } from "~/common/signal.d.ts"; +import type { Track } from "~/definitions/types.d.ts"; + +export type ScrobbleElement = DiffuseElement & ScrobbleActions & { + isAuthenticated: SignalReader; + isAuthenticating: SignalReader; + handle: SignalReader; +}; + +export type ScrobbleActions = { + nowPlaying(track: Track): Promise; + + /** + * @param {Track} track + * @param {number} startedAt Unix timestamp in milliseconds + * @param {{ duration?: number }} [options] duration in milliseconds + */ + scrobble(track: Track, startedAt: number, options?: { duration?: number }): Promise; +}; diff --git a/specs/components/transformer/output/bytes/automerge/types.d.ts b/specs/components/transformer/output/bytes/automerge/types.d.ts new file mode 100644 index 000000000..7881fff25 --- /dev/null +++ b/specs/components/transformer/output/bytes/automerge/types.d.ts @@ -0,0 +1,11 @@ +import type { + Facet, + PlaylistItem, + Setting, + Track, +} from "~/definitions/types.d.ts"; + +export type FacetsDocument = { collection: Facet[] }; +export type PlaylistItemsDocument = { collection: PlaylistItem[] }; +export type SettingsDocument = { collection: Setting[] }; +export type TracksDocument = { collection: Track[] }; diff --git a/specs/components/transformer/output/bytes/dasl-sync/types.d.ts b/specs/components/transformer/output/bytes/dasl-sync/types.d.ts new file mode 100644 index 000000000..43fe46772 --- /dev/null +++ b/specs/components/transformer/output/bytes/dasl-sync/types.d.ts @@ -0,0 +1,21 @@ +export type Container = { + /** + * CID of the inventory, + * which in turns represents the current state of the data. + */ + cid?: string; + data: T[]; + inventory: Inventory; +}; + +export type Inventory = { + /** + * `id` to `cid` map. + */ + current: Record; + + /** + * List of `id`s + */ + removed: string[]; +}; diff --git a/specs/components/upload/types.d.ts b/specs/components/upload/types.d.ts new file mode 100644 index 000000000..cd9b7bce5 --- /dev/null +++ b/specs/components/upload/types.d.ts @@ -0,0 +1,49 @@ +import type { ProxiedActions } from "~/common/worker.d.ts"; + +import type { Track } from "~/definitions/types.d.ts"; +import type { Consult } from "@specs/components/input/types.d.ts"; +import type { DiffuseElement } from "~/common/element.js"; + +export type UploadActions = { + /** + * Check if this uploader or an individual URI can be used. + */ + consult(uriOrScheme: string): Promise; + + /** + * Builds a placeholder track for the scheme's account, used to "add" the + * corresponding input component so it lists the uploaded files. + * + * `scheme` is used by the configurator to route to the correct upload + * component; individual components ignore it (they know their own scheme). + */ + createSource(args: { + scheme: string; + refreshToken: string; + directoryPath: string; + }): Promise; + + /** + * Delete an uploaded track. + */ + delete(uri: string): Promise; + + /** + * Upload a track. + */ + upload(args: { file: File; uri: string; path?: string }): Promise; +}; + +export type UploadElement = + & DiffuseElement + & UploadSchemeProvider + & ProxiedActions + & { + /** + * Triggers the OAuth flow for this upload component, if applicable. + * Not all upload components require OAuth. + */ + authorize?(): void; + }; + +export type UploadSchemeProvider = { SCHEME: string }; diff --git a/specs/facets/themes/catalogue/SPEC.md b/specs/facets/themes/catalogue/SPEC.md new file mode 100644 index 000000000..010a44fbe --- /dev/null +++ b/specs/facets/themes/catalogue/SPEC.md @@ -0,0 +1,18 @@ +# Catalogue theme + +A theme that's optimised for browsing through one's collection. + + +## Requirements + +- Focuses on a primarily visual approach to browsing the track collection. Uses cover art heavily. +- It starts with selecting the scope: search for a specific term or select a playlist. +- From there the user can browse through artists, albums or just see all songs/audio. +- Also must be able to see the queue and items played previously. +- There should also be a way to control the (playing) audio. + + +## Design + +- Do not opt for a typical audio player/browser design. +- I'm thinking grids and blocks. diff --git a/specs/service-worker/SPEC.md b/specs/service-worker/SPEC.md new file mode 100644 index 000000000..5d1f8c434 --- /dev/null +++ b/specs/service-worker/SPEC.md @@ -0,0 +1,22 @@ +# Service worker + +## Requirements + +- It must be able to serve every resource offline that has been requested before. +- Every file must be cached by CID. +- The file tree (a map of path → CID for file) is embedded directly in the service worker at build time. Because every build produces a new service worker, the embedded tree is always current. +- Every new build means a new service worker. +- All pages must use the same code. +- When a new service worker is available, install and activate it immediately, while still making sure all pages are on the same service worker. This is done through sending a `{ type: "sw-activated" }` message to the client, on which they must reload the page. +- Media and partial requests should never be cached. +- When fetching a resource from the network, use a `cache: "no-cache"` to bypass the cache (to ensure we always got the latest content). +- When the connection is offline and we don't have the content cached, return a `503` error. +- For items that are listed in the file tree, prefer the cache first and the network second. Otherwise network first if online. +- When a network request is made, cache the result. +- Cross-origin requests are cached too. +- Non-GET requests are not cached. +- When making a network-first request, when that returns an error or the resource is not found; do not fallback to cache. + +### Credentials + +The service worker must be able to translate requests with the `diffuse:basic-auth` query parameter and requests with user credentials in the URL (eg. `user:pass@host`). The credentials are moved to a `Authorization: Basic` header. These requests aren't cached. diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore deleted file mode 100644 index f4dfb82b2..000000000 --- a/src-tauri/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -/target/ - diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock deleted file mode 100644 index 95073b87a..000000000 --- a/src-tauri/Cargo.lock +++ /dev/null @@ -1,5549 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" - -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - -[[package]] -name = "ashpd" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" -dependencies = [ - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.8.5", - "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-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "async-trait" -version = "0.1.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" -dependencies = [ - "serde", -] - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2", -] - -[[package]] -name = "brotli" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "bytemuck" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.8.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.11", -] - -[[package]] -name = "cargo_toml" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" -dependencies = [ - "serde", - "toml 0.8.20", -] - -[[package]] -name = "cc" -version = "1.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" -dependencies = [ - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "windows-targets 0.52.6", -] - -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - -[[package]] -name = "cocoa" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" -dependencies = [ - "bitflags 2.8.0", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" -dependencies = [ - "bitflags 2.8.0", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.8.0", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.8.0", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa 0.4.8", - "matches", - "phf 0.8.0", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.98", -] - -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.98", -] - -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.98", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive_more" -version = "0.99.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.98", -] - -[[package]] -name = "diffuse" -version = "3.5.0" -dependencies = [ - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-fs", - "tauri-plugin-localhost", - "tauri-plugin-positioner", - "tauri-plugin-shell", - "tauri-plugin-window-state", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.0", - "windows-sys 0.59.0", -] - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading 0.8.6", -] - -[[package]] -name = "dlopen2" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" - -[[package]] -name = "embed-resource" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b68b6f9f63a0b6a38bc447d4ce84e2b388f3ec95c99c641c8ff0dd3ef89a6379" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 0.8.20", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -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.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" -dependencies = [ - "serde", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "event-listener" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "flate2" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.8.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.0", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html5ever" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "http" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" -dependencies = [ - "bytes", - "fnv", - "itoa 1.0.14", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "itoa 1.0.14", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core 0.52.0", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" -dependencies = [ - "byteorder", - "png", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", - "serde", -] - -[[package]] -name = "infer" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" -dependencies = [ - "cfb", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -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 = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.8.0", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "kuchikiki" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 1.9.3", - "matches", - "selectors", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading 0.7.4", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libloading" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" -dependencies = [ - "cfg-if", - "windows-targets 0.52.6", -] - -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.8.0", - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "markup5ever" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" -dependencies = [ - "log", - "phf 0.10.1", - "phf_codegen 0.10.0", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "muda" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 1.0.69", - "windows-sys 0.59.0", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.8.0", - "jni-sys", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.8.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" -dependencies = [ - "proc-macro-crate 3.2.0", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.8.0", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2", - "objc2-contacts", - "objc2-foundation", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.8.0", - "block2", - "dispatch", - "libc", - "objc2", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-core-location", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" -dependencies = [ - "bitflags 2.8.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" - -[[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_pipe" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_macros 0.8.0", - "phf_shared 0.8.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.1", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - -[[package]] -name = "plist" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" -dependencies = [ - "base64 0.22.1", - "indexmap 2.7.1", - "quick-xml 0.32.0", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" -dependencies = [ - "toml_edit 0.20.7", -] - -[[package]] -name = "proc-macro-crate" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" -dependencies = [ - "toml_edit 0.22.24", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" -dependencies = [ - "memchr", -] - -[[package]] -name = "quinn" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.11", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" -dependencies = [ - "bytes", - "getrandom 0.2.15", - "rand 0.8.5", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.11", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" -dependencies = [ - "bitflags 2.8.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 2.0.11", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "reqwest" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", - "windows-registry", -] - -[[package]] -name = "rfd" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f" -dependencies = [ - "ashpd", - "block2", - "core-foundation", - "core-foundation-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "ring" -version = "0.17.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.8.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" -dependencies = [ - "web-time", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" - -[[package]] -name = "ryu" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars_derive" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.98", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "selectors" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" -dependencies = [ - "bitflags 1.3.2", - "cssparser", - "derive_more", - "fxhash", - "log", - "matches", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc", - "smallvec", - "thin-slice", -] - -[[package]] -name = "semver" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" -dependencies = [ - "serde", -] - -[[package]] -name = "serde" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" -dependencies = [ - "erased-serde", - "serde", - "typeid", -] - -[[package]] -name = "serde_derive" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "serde_json" -version = "1.0.138" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" -dependencies = [ - "itoa 1.0.14", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa 1.0.14", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.7.1", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "servo_arc" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shared_child" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "softbuffer" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" -dependencies = [ - "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types", - "js-sys", - "log", - "objc2", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall", - "wasm-bindgen", - "web-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244292f3441c89febe5b5bdfbb6863aeaf4f64da810ea3050fd927b27b8d92ce" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.20", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3731d04d4ac210cd5f344087733943b9bfb1a32654387dad4d1c70de21aee2c9" -dependencies = [ - "bitflags 2.8.0", - "cocoa", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dispatch", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "lazy_static", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc", - "once_cell", - "parking_lot", - "raw-window-handle", - "scopeguard", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.58.0", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58a998b6be84104ca05c7e9a21f2180ddec020c8b84ea59a8fc8530a2a19588d" -dependencies = [ - "anyhow", - "bytes", - "dirs 6.0.0", - "dunce", - "embed_plist", - "futures-util", - "getrandom 0.2.15", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.11", - "tokio", - "tray-icon", - "url", - "urlpattern", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e950124f6779c6cf98e3260c7a6c8488a74aa6350dd54c6950fdaa349bca2df" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs 5.0.1", - "glob", - "heck 0.5.0", - "json-patch", - "schemars", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "toml 0.8.20", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77894f9ddb5cb6c04fcfe8c8869ebe0aded4dabf19917118d48be4a95599ab5" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.98", - "tauri-utils", - "thiserror 2.0.11", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3240a5caed760a532e8f687be6f05b2c7d11a1d791fb53ccc08cfeb3e5308736" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.98", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5841b9a0200e954ef7457f8d327091424328891e267a97b641dc246cc54d0dec" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars", - "serde", - "serde_json", - "tauri-utils", - "toml 0.8.20", - "walkdir", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b59fd750551b1066744ab956a1cd6b1ea3e1b3763b0b9153ac27a044d596426" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.11", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1edf18000f02903a7c2e5997fb89aca455ecbc0acc15c6535afbb883be223" -dependencies = [ - "anyhow", - "dunce", - "glob", - "percent-encoding", - "schemars", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.11", - "toml 0.8.20", - "url", - "uuid", -] - -[[package]] -name = "tauri-plugin-localhost" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f19f292368d8ec7bbbfd4b3a473636385ae2ce6c0a70d40ef0f5a0f024c85df" -dependencies = [ - "http", - "log", - "serde", - "serde_json", - "tauri", - "thiserror 2.0.11", - "tiny_http", -] - -[[package]] -name = "tauri-plugin-positioner" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c95b371d489bee3d1be5e5bd1538080ad408317fcc2d8546d24b290249f7bb5" -dependencies = [ - "log", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "thiserror 2.0.11", -] - -[[package]] -name = "tauri-plugin-shell" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c50a63e60fb8925956cc5b7569f4b750ac197a4d39f13b8dd46ea8e2bad79" -dependencies = [ - "encoding_rs", - "log", - "open", - "os_pipe", - "regex", - "schemars", - "serde", - "serde_json", - "shared_child", - "tauri", - "tauri-plugin", - "thiserror 2.0.11", - "tokio", -] - -[[package]] -name = "tauri-plugin-window-state" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e344b512b0d99d9d06225f235d87d6c66d89496a3bf323d9b578d940596e6c" -dependencies = [ - "bitflags 2.8.0", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.11", -] - -[[package]] -name = "tauri-runtime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2274ef891ccc0a8d318deffa9d70053f947664d12d58b9c0d1ae5e89237e01f7" -dependencies = [ - "dpi", - "gtk", - "http", - "jni", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.11", - "url", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3707b40711d3b9f6519150869e358ffbde7c57567fb9b5a8b51150606939b2a0" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96fb10e7cc97456b2d5b9c03e335b5de5da982039a303a20d10006885e4523a0" -dependencies = [ - "brotli", - "cargo_metadata", - "ctor", - "dunce", - "glob", - "html5ever", - "http", - "infer", - "json-patch", - "kuchikiki", - "log", - "memchr", - "phf 0.11.3", - "proc-macro2", - "quote", - "regex", - "schemars", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.11", - "toml 0.8.20", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" -dependencies = [ - "embed-resource", - "toml 0.7.8", -] - -[[package]] -name = "tempfile" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40f762a77d2afa88c2d919489e390a12bdd261ed568e60cfa7e48d4e20f0d33" -dependencies = [ - "cfg-if", - "fastrand", - "getrandom 0.3.1", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "thin-slice" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" -dependencies = [ - "thiserror-impl 2.0.11", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "time" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "deranged", - "itoa 1.0.14", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny_http" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" -dependencies = [ - "ascii", - "chunked_transfer", - "httpdate", - "log", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.19.15", -] - -[[package]] -name = "toml" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.24", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.7.1", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" -dependencies = [ - "indexmap 2.7.1", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" -dependencies = [ - "indexmap 2.7.1", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.7.2", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -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.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" -dependencies = [ - "core-graphics", - "crossbeam-channel", - "dirs 5.0.1", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 1.0.69", - "windows-sys 0.59.0", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" - -[[package]] -name = "unicode-segmentation" -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.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" -dependencies = [ - "getrandom 0.3.1", - "serde", -] - -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.98", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" -dependencies = [ - "cc", - "downcast-rs", - "rustix", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" -dependencies = [ - "bitflags 2.8.0", - "rustix", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" -dependencies = [ - "bitflags 2.8.0", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" -dependencies = [ - "proc-macro2", - "quick-xml 0.37.2", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" -dependencies = [ - "dlib", - "log", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webpki-roots" -version = "0.26.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webview2-com" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823e7ebcfaea51e78f72c87fc3b65a1e602c321f407a0b36dbb327d7bb7cd921" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.58.0", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "webview2-com-sys" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a82bce72db6e5ee83c68b5de1e2cd6ea195b9fbff91cb37df5884cbe3222df4" -dependencies = [ - "thiserror 1.0.69", - "windows", - "windows-core 0.58.0", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831ad7678290beae36be6f9fad9234139c7f00f3b536347de7745621716be82d" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows-version" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12476c23a74725c539b24eae8bfc0dac4029c39cdb561d9f23616accd4ae26d" -dependencies = [ - "windows-targets 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags 2.8.0", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "wry" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e33c08b174442ff80d5c791020696f9f8b4e4a87b8cfc7494aad6167ec44e1" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dpi", - "dunce", - "gdkx11", - "gtk", - "html5ever", - "http", - "javascriptcore-rs", - "jni", - "kuchikiki", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.11", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.58.0", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" -dependencies = [ - "async-broadcast", - "async-recursion", - "async-trait", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "nix", - "ordered-stream", - "serde", - "serde_repr", - "static_assertions", - "tokio", - "tracing", - "uds_windows", - "windows-sys 0.59.0", - "winnow 0.7.2", - "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" -dependencies = [ - "proc-macro-crate 3.2.0", - "proc-macro2", - "quote", - "syn 2.0.98", - "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.2", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "zerofrom" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "zvariant" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "url", - "winnow 0.7.2", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" -dependencies = [ - "proc-macro-crate 3.2.0", - "proc-macro2", - "quote", - "syn 2.0.98", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "static_assertions", - "syn 2.0.98", - "winnow 0.7.2", -] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml deleted file mode 100644 index a39e1f8e1..000000000 --- a/src-tauri/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "diffuse" -version = "3.5.0" -description = "A music player that connects to your cloud/distributed storage" -authors = ["Steven Vandevelde"] -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[build-dependencies] -tauri-build = { version = "2.0.5", features = [] } - -[dependencies] -tauri = { version = "2.2.5", features = ["unstable"] } -tauri-plugin-dialog = "2.2.0" -tauri-plugin-fs = "2.2.0" -tauri-plugin-localhost = "2.2.0" -tauri-plugin-positioner = "2.2.0" -tauri-plugin-shell = "2.2.0" -tauri-plugin-window-state = "2.2.1" -serde_json = "1.0" - -[features] -# this feature is used for production builds or when `devPath` points to the filesystem -# DO NOT REMOVE!! -custom-protocol = ["tauri/custom-protocol"] - -# [lib] -# crate-type = ["staticlib", "cdylib", "rlib"] diff --git a/src-tauri/build.rs b/src-tauri/build.rs deleted file mode 100644 index d860e1e6a..000000000 --- a/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build() -} diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json deleted file mode 100644 index 8c59b539d..000000000 --- a/src-tauri/capabilities/main.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "main-capability", - "description": "Capability for the main window", - "context": "local", - "windows": ["main"], - "permissions": [ - "core:path:default", - "core:event:default", - "core:window:default", - "core:app:default", - "dialog:allow-ask", - "dialog:allow-confirm", - "dialog:allow-message", - "dialog:allow-open", - "dialog:allow-save", - "fs:allow-download-write", - "core:resources:default", - "core:menu:default", - "core:tray:default", - "shell:allow-open" - ], - "platforms": ["linux", "macOS", "windows", "android", "iOS"] -} diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json deleted file mode 100644 index e6d0fa5b6..000000000 --- a/src-tauri/gen/schemas/acl-manifests.json +++ /dev/null @@ -1 +0,0 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"fs":{"default_permission":{"identifier":"default","description":"This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n","permissions":["create-app-specific-dirs","read-app-specific-dirs-recursive","deny-default"]},"permissions":{"allow-copy-file":{"identifier":"allow-copy-file","description":"Enables the copy_file command without any pre-configured scope.","commands":{"allow":["copy_file"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-exists":{"identifier":"allow-exists","description":"Enables the exists command without any pre-configured scope.","commands":{"allow":["exists"],"deny":[]}},"allow-fstat":{"identifier":"allow-fstat","description":"Enables the fstat command without any pre-configured scope.","commands":{"allow":["fstat"],"deny":[]}},"allow-ftruncate":{"identifier":"allow-ftruncate","description":"Enables the ftruncate command without any pre-configured scope.","commands":{"allow":["ftruncate"],"deny":[]}},"allow-lstat":{"identifier":"allow-lstat","description":"Enables the lstat command without any pre-configured scope.","commands":{"allow":["lstat"],"deny":[]}},"allow-mkdir":{"identifier":"allow-mkdir","description":"Enables the mkdir command without any pre-configured scope.","commands":{"allow":["mkdir"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-read":{"identifier":"allow-read","description":"Enables the read command without any pre-configured scope.","commands":{"allow":["read"],"deny":[]}},"allow-read-dir":{"identifier":"allow-read-dir","description":"Enables the read_dir command without any pre-configured scope.","commands":{"allow":["read_dir"],"deny":[]}},"allow-read-file":{"identifier":"allow-read-file","description":"Enables the read_file command without any pre-configured scope.","commands":{"allow":["read_file"],"deny":[]}},"allow-read-text-file":{"identifier":"allow-read-text-file","description":"Enables the read_text_file command without any pre-configured scope.","commands":{"allow":["read_text_file"],"deny":[]}},"allow-read-text-file-lines":{"identifier":"allow-read-text-file-lines","description":"Enables the read_text_file_lines command without any pre-configured scope.","commands":{"allow":["read_text_file_lines","read_text_file_lines_next"],"deny":[]}},"allow-read-text-file-lines-next":{"identifier":"allow-read-text-file-lines-next","description":"Enables the read_text_file_lines_next command without any pre-configured scope.","commands":{"allow":["read_text_file_lines_next"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-rename":{"identifier":"allow-rename","description":"Enables the rename command without any pre-configured scope.","commands":{"allow":["rename"],"deny":[]}},"allow-seek":{"identifier":"allow-seek","description":"Enables the seek command without any pre-configured scope.","commands":{"allow":["seek"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"allow-stat":{"identifier":"allow-stat","description":"Enables the stat command without any pre-configured scope.","commands":{"allow":["stat"],"deny":[]}},"allow-truncate":{"identifier":"allow-truncate","description":"Enables the truncate command without any pre-configured scope.","commands":{"allow":["truncate"],"deny":[]}},"allow-unwatch":{"identifier":"allow-unwatch","description":"Enables the unwatch command without any pre-configured scope.","commands":{"allow":["unwatch"],"deny":[]}},"allow-watch":{"identifier":"allow-watch","description":"Enables the watch command without any pre-configured scope.","commands":{"allow":["watch"],"deny":[]}},"allow-write":{"identifier":"allow-write","description":"Enables the write command without any pre-configured scope.","commands":{"allow":["write"],"deny":[]}},"allow-write-file":{"identifier":"allow-write-file","description":"Enables the write_file command without any pre-configured scope.","commands":{"allow":["write_file","open","write"],"deny":[]}},"allow-write-text-file":{"identifier":"allow-write-text-file","description":"Enables the write_text_file command without any pre-configured scope.","commands":{"allow":["write_text_file"],"deny":[]}},"create-app-specific-dirs":{"identifier":"create-app-specific-dirs","description":"This permissions allows to create the application specific directories.\n","commands":{"allow":["mkdir","scope-app-index"],"deny":[]}},"deny-copy-file":{"identifier":"deny-copy-file","description":"Denies the copy_file command without any pre-configured scope.","commands":{"allow":[],"deny":["copy_file"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-exists":{"identifier":"deny-exists","description":"Denies the exists command without any pre-configured scope.","commands":{"allow":[],"deny":["exists"]}},"deny-fstat":{"identifier":"deny-fstat","description":"Denies the fstat command without any pre-configured scope.","commands":{"allow":[],"deny":["fstat"]}},"deny-ftruncate":{"identifier":"deny-ftruncate","description":"Denies the ftruncate command without any pre-configured scope.","commands":{"allow":[],"deny":["ftruncate"]}},"deny-lstat":{"identifier":"deny-lstat","description":"Denies the lstat command without any pre-configured scope.","commands":{"allow":[],"deny":["lstat"]}},"deny-mkdir":{"identifier":"deny-mkdir","description":"Denies the mkdir command without any pre-configured scope.","commands":{"allow":[],"deny":["mkdir"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-read":{"identifier":"deny-read","description":"Denies the read command without any pre-configured scope.","commands":{"allow":[],"deny":["read"]}},"deny-read-dir":{"identifier":"deny-read-dir","description":"Denies the read_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["read_dir"]}},"deny-read-file":{"identifier":"deny-read-file","description":"Denies the read_file command without any pre-configured scope.","commands":{"allow":[],"deny":["read_file"]}},"deny-read-text-file":{"identifier":"deny-read-text-file","description":"Denies the read_text_file command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text_file"]}},"deny-read-text-file-lines":{"identifier":"deny-read-text-file-lines","description":"Denies the read_text_file_lines command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text_file_lines"]}},"deny-read-text-file-lines-next":{"identifier":"deny-read-text-file-lines-next","description":"Denies the read_text_file_lines_next command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text_file_lines_next"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-rename":{"identifier":"deny-rename","description":"Denies the rename command without any pre-configured scope.","commands":{"allow":[],"deny":["rename"]}},"deny-seek":{"identifier":"deny-seek","description":"Denies the seek command without any pre-configured scope.","commands":{"allow":[],"deny":["seek"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}},"deny-stat":{"identifier":"deny-stat","description":"Denies the stat command without any pre-configured scope.","commands":{"allow":[],"deny":["stat"]}},"deny-truncate":{"identifier":"deny-truncate","description":"Denies the truncate command without any pre-configured scope.","commands":{"allow":[],"deny":["truncate"]}},"deny-unwatch":{"identifier":"deny-unwatch","description":"Denies the unwatch command without any pre-configured scope.","commands":{"allow":[],"deny":["unwatch"]}},"deny-watch":{"identifier":"deny-watch","description":"Denies the watch command without any pre-configured scope.","commands":{"allow":[],"deny":["watch"]}},"deny-webview-data-linux":{"identifier":"deny-webview-data-linux","description":"This denies read access to the\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.","commands":{"allow":[],"deny":[]}},"deny-webview-data-windows":{"identifier":"deny-webview-data-windows","description":"This denies read access to the\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.","commands":{"allow":[],"deny":[]}},"deny-write":{"identifier":"deny-write","description":"Denies the write command without any pre-configured scope.","commands":{"allow":[],"deny":["write"]}},"deny-write-file":{"identifier":"deny-write-file","description":"Denies the write_file command without any pre-configured scope.","commands":{"allow":[],"deny":["write_file"]}},"deny-write-text-file":{"identifier":"deny-write-text-file","description":"Denies the write_text_file command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text_file"]}},"read-all":{"identifier":"read-all","description":"This enables all read related commands without any pre-configured accessible paths.","commands":{"allow":["read_dir","read_file","read","open","read_text_file","read_text_file_lines","read_text_file_lines_next","seek","stat","lstat","fstat","exists","watch","unwatch"],"deny":[]}},"read-app-specific-dirs-recursive":{"identifier":"read-app-specific-dirs-recursive","description":"This permission allows recursive read functionality on the application\nspecific base directories. \n","commands":{"allow":["read_dir","read_file","read_text_file","read_text_file_lines","read_text_file_lines_next","exists","scope-app-recursive"],"deny":[]}},"read-dirs":{"identifier":"read-dirs","description":"This enables directory read and file metadata related commands without any pre-configured accessible paths.","commands":{"allow":["read_dir","stat","lstat","fstat","exists"],"deny":[]}},"read-files":{"identifier":"read-files","description":"This enables file read related commands without any pre-configured accessible paths.","commands":{"allow":["read_file","read","open","read_text_file","read_text_file_lines","read_text_file_lines_next","seek","stat","lstat","fstat","exists"],"deny":[]}},"read-meta":{"identifier":"read-meta","description":"This enables all index or metadata related commands without any pre-configured accessible paths.","commands":{"allow":["read_dir","stat","lstat","fstat","exists","size"],"deny":[]}},"scope":{"identifier":"scope","description":"An empty permission you can use to modify the global scope.","commands":{"allow":[],"deny":[]}},"scope-app":{"identifier":"scope-app","description":"This scope permits access to all files and list content of top level directories in the application folders.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG"},{"path":"$APPCONFIG/*"},{"path":"$APPDATA"},{"path":"$APPDATA/*"},{"path":"$APPLOCALDATA"},{"path":"$APPLOCALDATA/*"},{"path":"$APPCACHE"},{"path":"$APPCACHE/*"},{"path":"$APPLOG"},{"path":"$APPLOG/*"}]}},"scope-app-index":{"identifier":"scope-app-index","description":"This scope permits to list all files and folders in the application directories.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG"},{"path":"$APPDATA"},{"path":"$APPLOCALDATA"},{"path":"$APPCACHE"},{"path":"$APPLOG"}]}},"scope-app-recursive":{"identifier":"scope-app-recursive","description":"This scope permits recursive access to the complete application folders, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG"},{"path":"$APPCONFIG/**"},{"path":"$APPDATA"},{"path":"$APPDATA/**"},{"path":"$APPLOCALDATA"},{"path":"$APPLOCALDATA/**"},{"path":"$APPCACHE"},{"path":"$APPCACHE/**"},{"path":"$APPLOG"},{"path":"$APPLOG/**"}]}},"scope-appcache":{"identifier":"scope-appcache","description":"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCACHE"},{"path":"$APPCACHE/*"}]}},"scope-appcache-index":{"identifier":"scope-appcache-index","description":"This scope permits to list all files and folders in the `$APPCACHE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCACHE"}]}},"scope-appcache-recursive":{"identifier":"scope-appcache-recursive","description":"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCACHE"},{"path":"$APPCACHE/**"}]}},"scope-appconfig":{"identifier":"scope-appconfig","description":"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG"},{"path":"$APPCONFIG/*"}]}},"scope-appconfig-index":{"identifier":"scope-appconfig-index","description":"This scope permits to list all files and folders in the `$APPCONFIG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG"}]}},"scope-appconfig-recursive":{"identifier":"scope-appconfig-recursive","description":"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG"},{"path":"$APPCONFIG/**"}]}},"scope-appdata":{"identifier":"scope-appdata","description":"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPDATA"},{"path":"$APPDATA/*"}]}},"scope-appdata-index":{"identifier":"scope-appdata-index","description":"This scope permits to list all files and folders in the `$APPDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPDATA"}]}},"scope-appdata-recursive":{"identifier":"scope-appdata-recursive","description":"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]}},"scope-applocaldata":{"identifier":"scope-applocaldata","description":"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOCALDATA"},{"path":"$APPLOCALDATA/*"}]}},"scope-applocaldata-index":{"identifier":"scope-applocaldata-index","description":"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOCALDATA"}]}},"scope-applocaldata-recursive":{"identifier":"scope-applocaldata-recursive","description":"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOCALDATA"},{"path":"$APPLOCALDATA/**"}]}},"scope-applog":{"identifier":"scope-applog","description":"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOG"},{"path":"$APPLOG/*"}]}},"scope-applog-index":{"identifier":"scope-applog-index","description":"This scope permits to list all files and folders in the `$APPLOG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOG"}]}},"scope-applog-recursive":{"identifier":"scope-applog-recursive","description":"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOG"},{"path":"$APPLOG/**"}]}},"scope-audio":{"identifier":"scope-audio","description":"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$AUDIO"},{"path":"$AUDIO/*"}]}},"scope-audio-index":{"identifier":"scope-audio-index","description":"This scope permits to list all files and folders in the `$AUDIO`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$AUDIO"}]}},"scope-audio-recursive":{"identifier":"scope-audio-recursive","description":"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$AUDIO"},{"path":"$AUDIO/**"}]}},"scope-cache":{"identifier":"scope-cache","description":"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CACHE"},{"path":"$CACHE/*"}]}},"scope-cache-index":{"identifier":"scope-cache-index","description":"This scope permits to list all files and folders in the `$CACHE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CACHE"}]}},"scope-cache-recursive":{"identifier":"scope-cache-recursive","description":"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CACHE"},{"path":"$CACHE/**"}]}},"scope-config":{"identifier":"scope-config","description":"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CONFIG"},{"path":"$CONFIG/*"}]}},"scope-config-index":{"identifier":"scope-config-index","description":"This scope permits to list all files and folders in the `$CONFIG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CONFIG"}]}},"scope-config-recursive":{"identifier":"scope-config-recursive","description":"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CONFIG"},{"path":"$CONFIG/**"}]}},"scope-data":{"identifier":"scope-data","description":"This scope permits access to all files and list content of top level directories in the `$DATA` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DATA"},{"path":"$DATA/*"}]}},"scope-data-index":{"identifier":"scope-data-index","description":"This scope permits to list all files and folders in the `$DATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DATA"}]}},"scope-data-recursive":{"identifier":"scope-data-recursive","description":"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DATA"},{"path":"$DATA/**"}]}},"scope-desktop":{"identifier":"scope-desktop","description":"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DESKTOP"},{"path":"$DESKTOP/*"}]}},"scope-desktop-index":{"identifier":"scope-desktop-index","description":"This scope permits to list all files and folders in the `$DESKTOP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DESKTOP"}]}},"scope-desktop-recursive":{"identifier":"scope-desktop-recursive","description":"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DESKTOP"},{"path":"$DESKTOP/**"}]}},"scope-document":{"identifier":"scope-document","description":"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOCUMENT"},{"path":"$DOCUMENT/*"}]}},"scope-document-index":{"identifier":"scope-document-index","description":"This scope permits to list all files and folders in the `$DOCUMENT`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOCUMENT"}]}},"scope-document-recursive":{"identifier":"scope-document-recursive","description":"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOCUMENT"},{"path":"$DOCUMENT/**"}]}},"scope-download":{"identifier":"scope-download","description":"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOWNLOAD"},{"path":"$DOWNLOAD/*"}]}},"scope-download-index":{"identifier":"scope-download-index","description":"This scope permits to list all files and folders in the `$DOWNLOAD`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOWNLOAD"}]}},"scope-download-recursive":{"identifier":"scope-download-recursive","description":"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOWNLOAD"},{"path":"$DOWNLOAD/**"}]}},"scope-exe":{"identifier":"scope-exe","description":"This scope permits access to all files and list content of top level directories in the `$EXE` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$EXE"},{"path":"$EXE/*"}]}},"scope-exe-index":{"identifier":"scope-exe-index","description":"This scope permits to list all files and folders in the `$EXE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$EXE"}]}},"scope-exe-recursive":{"identifier":"scope-exe-recursive","description":"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$EXE"},{"path":"$EXE/**"}]}},"scope-font":{"identifier":"scope-font","description":"This scope permits access to all files and list content of top level directories in the `$FONT` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$FONT"},{"path":"$FONT/*"}]}},"scope-font-index":{"identifier":"scope-font-index","description":"This scope permits to list all files and folders in the `$FONT`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$FONT"}]}},"scope-font-recursive":{"identifier":"scope-font-recursive","description":"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$FONT"},{"path":"$FONT/**"}]}},"scope-home":{"identifier":"scope-home","description":"This scope permits access to all files and list content of top level directories in the `$HOME` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$HOME"},{"path":"$HOME/*"}]}},"scope-home-index":{"identifier":"scope-home-index","description":"This scope permits to list all files and folders in the `$HOME`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$HOME"}]}},"scope-home-recursive":{"identifier":"scope-home-recursive","description":"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$HOME"},{"path":"$HOME/**"}]}},"scope-localdata":{"identifier":"scope-localdata","description":"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOCALDATA"},{"path":"$LOCALDATA/*"}]}},"scope-localdata-index":{"identifier":"scope-localdata-index","description":"This scope permits to list all files and folders in the `$LOCALDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOCALDATA"}]}},"scope-localdata-recursive":{"identifier":"scope-localdata-recursive","description":"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOCALDATA"},{"path":"$LOCALDATA/**"}]}},"scope-log":{"identifier":"scope-log","description":"This scope permits access to all files and list content of top level directories in the `$LOG` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOG"},{"path":"$LOG/*"}]}},"scope-log-index":{"identifier":"scope-log-index","description":"This scope permits to list all files and folders in the `$LOG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOG"}]}},"scope-log-recursive":{"identifier":"scope-log-recursive","description":"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOG"},{"path":"$LOG/**"}]}},"scope-picture":{"identifier":"scope-picture","description":"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PICTURE"},{"path":"$PICTURE/*"}]}},"scope-picture-index":{"identifier":"scope-picture-index","description":"This scope permits to list all files and folders in the `$PICTURE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PICTURE"}]}},"scope-picture-recursive":{"identifier":"scope-picture-recursive","description":"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PICTURE"},{"path":"$PICTURE/**"}]}},"scope-public":{"identifier":"scope-public","description":"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PUBLIC"},{"path":"$PUBLIC/*"}]}},"scope-public-index":{"identifier":"scope-public-index","description":"This scope permits to list all files and folders in the `$PUBLIC`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PUBLIC"}]}},"scope-public-recursive":{"identifier":"scope-public-recursive","description":"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PUBLIC"},{"path":"$PUBLIC/**"}]}},"scope-resource":{"identifier":"scope-resource","description":"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RESOURCE"},{"path":"$RESOURCE/*"}]}},"scope-resource-index":{"identifier":"scope-resource-index","description":"This scope permits to list all files and folders in the `$RESOURCE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RESOURCE"}]}},"scope-resource-recursive":{"identifier":"scope-resource-recursive","description":"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RESOURCE"},{"path":"$RESOURCE/**"}]}},"scope-runtime":{"identifier":"scope-runtime","description":"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RUNTIME"},{"path":"$RUNTIME/*"}]}},"scope-runtime-index":{"identifier":"scope-runtime-index","description":"This scope permits to list all files and folders in the `$RUNTIME`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RUNTIME"}]}},"scope-runtime-recursive":{"identifier":"scope-runtime-recursive","description":"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RUNTIME"},{"path":"$RUNTIME/**"}]}},"scope-temp":{"identifier":"scope-temp","description":"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMP"},{"path":"$TEMP/*"}]}},"scope-temp-index":{"identifier":"scope-temp-index","description":"This scope permits to list all files and folders in the `$TEMP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMP"}]}},"scope-temp-recursive":{"identifier":"scope-temp-recursive","description":"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMP"},{"path":"$TEMP/**"}]}},"scope-template":{"identifier":"scope-template","description":"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMPLATE"},{"path":"$TEMPLATE/*"}]}},"scope-template-index":{"identifier":"scope-template-index","description":"This scope permits to list all files and folders in the `$TEMPLATE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMPLATE"}]}},"scope-template-recursive":{"identifier":"scope-template-recursive","description":"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMPLATE"},{"path":"$TEMPLATE/**"}]}},"scope-video":{"identifier":"scope-video","description":"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$VIDEO"},{"path":"$VIDEO/*"}]}},"scope-video-index":{"identifier":"scope-video-index","description":"This scope permits to list all files and folders in the `$VIDEO`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$VIDEO"}]}},"scope-video-recursive":{"identifier":"scope-video-recursive","description":"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$VIDEO"},{"path":"$VIDEO/**"}]}},"write-all":{"identifier":"write-all","description":"This enables all write related commands without any pre-configured accessible paths.","commands":{"allow":["mkdir","create","copy_file","remove","rename","truncate","ftruncate","write","write_file","write_text_file"],"deny":[]}},"write-files":{"identifier":"write-files","description":"This enables all file write related commands without any pre-configured accessible paths.","commands":{"allow":["create","copy_file","remove","rename","truncate","ftruncate","write","write_file","write_text_file"],"deny":[]}}},"permission_sets":{"allow-app-meta":{"identifier":"allow-app-meta","description":"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.","permissions":["read-meta","scope-app-index"]},"allow-app-meta-recursive":{"identifier":"allow-app-meta-recursive","description":"This allows full recursive read access to metadata of the application folders, including file listing and statistics.","permissions":["read-meta","scope-app-recursive"]},"allow-app-read":{"identifier":"allow-app-read","description":"This allows non-recursive read access to the application folders.","permissions":["read-all","scope-app"]},"allow-app-read-recursive":{"identifier":"allow-app-read-recursive","description":"This allows full recursive read access to the complete application folders, files and subdirectories.","permissions":["read-all","scope-app-recursive"]},"allow-app-write":{"identifier":"allow-app-write","description":"This allows non-recursive write access to the application folders.","permissions":["write-all","scope-app"]},"allow-app-write-recursive":{"identifier":"allow-app-write-recursive","description":"This allows full recursive write access to the complete application folders, files and subdirectories.","permissions":["write-all","scope-app-recursive"]},"allow-appcache-meta":{"identifier":"allow-appcache-meta","description":"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-appcache-index"]},"allow-appcache-meta-recursive":{"identifier":"allow-appcache-meta-recursive","description":"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-appcache-recursive"]},"allow-appcache-read":{"identifier":"allow-appcache-read","description":"This allows non-recursive read access to the `$APPCACHE` folder.","permissions":["read-all","scope-appcache"]},"allow-appcache-read-recursive":{"identifier":"allow-appcache-read-recursive","description":"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.","permissions":["read-all","scope-appcache-recursive"]},"allow-appcache-write":{"identifier":"allow-appcache-write","description":"This allows non-recursive write access to the `$APPCACHE` folder.","permissions":["write-all","scope-appcache"]},"allow-appcache-write-recursive":{"identifier":"allow-appcache-write-recursive","description":"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.","permissions":["write-all","scope-appcache-recursive"]},"allow-appconfig-meta":{"identifier":"allow-appconfig-meta","description":"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-appconfig-index"]},"allow-appconfig-meta-recursive":{"identifier":"allow-appconfig-meta-recursive","description":"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-appconfig-recursive"]},"allow-appconfig-read":{"identifier":"allow-appconfig-read","description":"This allows non-recursive read access to the `$APPCONFIG` folder.","permissions":["read-all","scope-appconfig"]},"allow-appconfig-read-recursive":{"identifier":"allow-appconfig-read-recursive","description":"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.","permissions":["read-all","scope-appconfig-recursive"]},"allow-appconfig-write":{"identifier":"allow-appconfig-write","description":"This allows non-recursive write access to the `$APPCONFIG` folder.","permissions":["write-all","scope-appconfig"]},"allow-appconfig-write-recursive":{"identifier":"allow-appconfig-write-recursive","description":"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.","permissions":["write-all","scope-appconfig-recursive"]},"allow-appdata-meta":{"identifier":"allow-appdata-meta","description":"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-appdata-index"]},"allow-appdata-meta-recursive":{"identifier":"allow-appdata-meta-recursive","description":"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-appdata-recursive"]},"allow-appdata-read":{"identifier":"allow-appdata-read","description":"This allows non-recursive read access to the `$APPDATA` folder.","permissions":["read-all","scope-appdata"]},"allow-appdata-read-recursive":{"identifier":"allow-appdata-read-recursive","description":"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.","permissions":["read-all","scope-appdata-recursive"]},"allow-appdata-write":{"identifier":"allow-appdata-write","description":"This allows non-recursive write access to the `$APPDATA` folder.","permissions":["write-all","scope-appdata"]},"allow-appdata-write-recursive":{"identifier":"allow-appdata-write-recursive","description":"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.","permissions":["write-all","scope-appdata-recursive"]},"allow-applocaldata-meta":{"identifier":"allow-applocaldata-meta","description":"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-applocaldata-index"]},"allow-applocaldata-meta-recursive":{"identifier":"allow-applocaldata-meta-recursive","description":"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-applocaldata-recursive"]},"allow-applocaldata-read":{"identifier":"allow-applocaldata-read","description":"This allows non-recursive read access to the `$APPLOCALDATA` folder.","permissions":["read-all","scope-applocaldata"]},"allow-applocaldata-read-recursive":{"identifier":"allow-applocaldata-read-recursive","description":"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.","permissions":["read-all","scope-applocaldata-recursive"]},"allow-applocaldata-write":{"identifier":"allow-applocaldata-write","description":"This allows non-recursive write access to the `$APPLOCALDATA` folder.","permissions":["write-all","scope-applocaldata"]},"allow-applocaldata-write-recursive":{"identifier":"allow-applocaldata-write-recursive","description":"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.","permissions":["write-all","scope-applocaldata-recursive"]},"allow-applog-meta":{"identifier":"allow-applog-meta","description":"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.","permissions":["read-meta","scope-applog-index"]},"allow-applog-meta-recursive":{"identifier":"allow-applog-meta-recursive","description":"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.","permissions":["read-meta","scope-applog-recursive"]},"allow-applog-read":{"identifier":"allow-applog-read","description":"This allows non-recursive read access to the `$APPLOG` folder.","permissions":["read-all","scope-applog"]},"allow-applog-read-recursive":{"identifier":"allow-applog-read-recursive","description":"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.","permissions":["read-all","scope-applog-recursive"]},"allow-applog-write":{"identifier":"allow-applog-write","description":"This allows non-recursive write access to the `$APPLOG` folder.","permissions":["write-all","scope-applog"]},"allow-applog-write-recursive":{"identifier":"allow-applog-write-recursive","description":"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.","permissions":["write-all","scope-applog-recursive"]},"allow-audio-meta":{"identifier":"allow-audio-meta","description":"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.","permissions":["read-meta","scope-audio-index"]},"allow-audio-meta-recursive":{"identifier":"allow-audio-meta-recursive","description":"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.","permissions":["read-meta","scope-audio-recursive"]},"allow-audio-read":{"identifier":"allow-audio-read","description":"This allows non-recursive read access to the `$AUDIO` folder.","permissions":["read-all","scope-audio"]},"allow-audio-read-recursive":{"identifier":"allow-audio-read-recursive","description":"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.","permissions":["read-all","scope-audio-recursive"]},"allow-audio-write":{"identifier":"allow-audio-write","description":"This allows non-recursive write access to the `$AUDIO` folder.","permissions":["write-all","scope-audio"]},"allow-audio-write-recursive":{"identifier":"allow-audio-write-recursive","description":"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.","permissions":["write-all","scope-audio-recursive"]},"allow-cache-meta":{"identifier":"allow-cache-meta","description":"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-cache-index"]},"allow-cache-meta-recursive":{"identifier":"allow-cache-meta-recursive","description":"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-cache-recursive"]},"allow-cache-read":{"identifier":"allow-cache-read","description":"This allows non-recursive read access to the `$CACHE` folder.","permissions":["read-all","scope-cache"]},"allow-cache-read-recursive":{"identifier":"allow-cache-read-recursive","description":"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.","permissions":["read-all","scope-cache-recursive"]},"allow-cache-write":{"identifier":"allow-cache-write","description":"This allows non-recursive write access to the `$CACHE` folder.","permissions":["write-all","scope-cache"]},"allow-cache-write-recursive":{"identifier":"allow-cache-write-recursive","description":"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.","permissions":["write-all","scope-cache-recursive"]},"allow-config-meta":{"identifier":"allow-config-meta","description":"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-config-index"]},"allow-config-meta-recursive":{"identifier":"allow-config-meta-recursive","description":"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-config-recursive"]},"allow-config-read":{"identifier":"allow-config-read","description":"This allows non-recursive read access to the `$CONFIG` folder.","permissions":["read-all","scope-config"]},"allow-config-read-recursive":{"identifier":"allow-config-read-recursive","description":"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.","permissions":["read-all","scope-config-recursive"]},"allow-config-write":{"identifier":"allow-config-write","description":"This allows non-recursive write access to the `$CONFIG` folder.","permissions":["write-all","scope-config"]},"allow-config-write-recursive":{"identifier":"allow-config-write-recursive","description":"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.","permissions":["write-all","scope-config-recursive"]},"allow-data-meta":{"identifier":"allow-data-meta","description":"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.","permissions":["read-meta","scope-data-index"]},"allow-data-meta-recursive":{"identifier":"allow-data-meta-recursive","description":"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.","permissions":["read-meta","scope-data-recursive"]},"allow-data-read":{"identifier":"allow-data-read","description":"This allows non-recursive read access to the `$DATA` folder.","permissions":["read-all","scope-data"]},"allow-data-read-recursive":{"identifier":"allow-data-read-recursive","description":"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.","permissions":["read-all","scope-data-recursive"]},"allow-data-write":{"identifier":"allow-data-write","description":"This allows non-recursive write access to the `$DATA` folder.","permissions":["write-all","scope-data"]},"allow-data-write-recursive":{"identifier":"allow-data-write-recursive","description":"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.","permissions":["write-all","scope-data-recursive"]},"allow-desktop-meta":{"identifier":"allow-desktop-meta","description":"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.","permissions":["read-meta","scope-desktop-index"]},"allow-desktop-meta-recursive":{"identifier":"allow-desktop-meta-recursive","description":"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.","permissions":["read-meta","scope-desktop-recursive"]},"allow-desktop-read":{"identifier":"allow-desktop-read","description":"This allows non-recursive read access to the `$DESKTOP` folder.","permissions":["read-all","scope-desktop"]},"allow-desktop-read-recursive":{"identifier":"allow-desktop-read-recursive","description":"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.","permissions":["read-all","scope-desktop-recursive"]},"allow-desktop-write":{"identifier":"allow-desktop-write","description":"This allows non-recursive write access to the `$DESKTOP` folder.","permissions":["write-all","scope-desktop"]},"allow-desktop-write-recursive":{"identifier":"allow-desktop-write-recursive","description":"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.","permissions":["write-all","scope-desktop-recursive"]},"allow-document-meta":{"identifier":"allow-document-meta","description":"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.","permissions":["read-meta","scope-document-index"]},"allow-document-meta-recursive":{"identifier":"allow-document-meta-recursive","description":"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.","permissions":["read-meta","scope-document-recursive"]},"allow-document-read":{"identifier":"allow-document-read","description":"This allows non-recursive read access to the `$DOCUMENT` folder.","permissions":["read-all","scope-document"]},"allow-document-read-recursive":{"identifier":"allow-document-read-recursive","description":"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.","permissions":["read-all","scope-document-recursive"]},"allow-document-write":{"identifier":"allow-document-write","description":"This allows non-recursive write access to the `$DOCUMENT` folder.","permissions":["write-all","scope-document"]},"allow-document-write-recursive":{"identifier":"allow-document-write-recursive","description":"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.","permissions":["write-all","scope-document-recursive"]},"allow-download-meta":{"identifier":"allow-download-meta","description":"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.","permissions":["read-meta","scope-download-index"]},"allow-download-meta-recursive":{"identifier":"allow-download-meta-recursive","description":"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.","permissions":["read-meta","scope-download-recursive"]},"allow-download-read":{"identifier":"allow-download-read","description":"This allows non-recursive read access to the `$DOWNLOAD` folder.","permissions":["read-all","scope-download"]},"allow-download-read-recursive":{"identifier":"allow-download-read-recursive","description":"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.","permissions":["read-all","scope-download-recursive"]},"allow-download-write":{"identifier":"allow-download-write","description":"This allows non-recursive write access to the `$DOWNLOAD` folder.","permissions":["write-all","scope-download"]},"allow-download-write-recursive":{"identifier":"allow-download-write-recursive","description":"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.","permissions":["write-all","scope-download-recursive"]},"allow-exe-meta":{"identifier":"allow-exe-meta","description":"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.","permissions":["read-meta","scope-exe-index"]},"allow-exe-meta-recursive":{"identifier":"allow-exe-meta-recursive","description":"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.","permissions":["read-meta","scope-exe-recursive"]},"allow-exe-read":{"identifier":"allow-exe-read","description":"This allows non-recursive read access to the `$EXE` folder.","permissions":["read-all","scope-exe"]},"allow-exe-read-recursive":{"identifier":"allow-exe-read-recursive","description":"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.","permissions":["read-all","scope-exe-recursive"]},"allow-exe-write":{"identifier":"allow-exe-write","description":"This allows non-recursive write access to the `$EXE` folder.","permissions":["write-all","scope-exe"]},"allow-exe-write-recursive":{"identifier":"allow-exe-write-recursive","description":"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.","permissions":["write-all","scope-exe-recursive"]},"allow-font-meta":{"identifier":"allow-font-meta","description":"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.","permissions":["read-meta","scope-font-index"]},"allow-font-meta-recursive":{"identifier":"allow-font-meta-recursive","description":"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.","permissions":["read-meta","scope-font-recursive"]},"allow-font-read":{"identifier":"allow-font-read","description":"This allows non-recursive read access to the `$FONT` folder.","permissions":["read-all","scope-font"]},"allow-font-read-recursive":{"identifier":"allow-font-read-recursive","description":"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.","permissions":["read-all","scope-font-recursive"]},"allow-font-write":{"identifier":"allow-font-write","description":"This allows non-recursive write access to the `$FONT` folder.","permissions":["write-all","scope-font"]},"allow-font-write-recursive":{"identifier":"allow-font-write-recursive","description":"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.","permissions":["write-all","scope-font-recursive"]},"allow-home-meta":{"identifier":"allow-home-meta","description":"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.","permissions":["read-meta","scope-home-index"]},"allow-home-meta-recursive":{"identifier":"allow-home-meta-recursive","description":"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.","permissions":["read-meta","scope-home-recursive"]},"allow-home-read":{"identifier":"allow-home-read","description":"This allows non-recursive read access to the `$HOME` folder.","permissions":["read-all","scope-home"]},"allow-home-read-recursive":{"identifier":"allow-home-read-recursive","description":"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.","permissions":["read-all","scope-home-recursive"]},"allow-home-write":{"identifier":"allow-home-write","description":"This allows non-recursive write access to the `$HOME` folder.","permissions":["write-all","scope-home"]},"allow-home-write-recursive":{"identifier":"allow-home-write-recursive","description":"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.","permissions":["write-all","scope-home-recursive"]},"allow-localdata-meta":{"identifier":"allow-localdata-meta","description":"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-localdata-index"]},"allow-localdata-meta-recursive":{"identifier":"allow-localdata-meta-recursive","description":"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-localdata-recursive"]},"allow-localdata-read":{"identifier":"allow-localdata-read","description":"This allows non-recursive read access to the `$LOCALDATA` folder.","permissions":["read-all","scope-localdata"]},"allow-localdata-read-recursive":{"identifier":"allow-localdata-read-recursive","description":"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.","permissions":["read-all","scope-localdata-recursive"]},"allow-localdata-write":{"identifier":"allow-localdata-write","description":"This allows non-recursive write access to the `$LOCALDATA` folder.","permissions":["write-all","scope-localdata"]},"allow-localdata-write-recursive":{"identifier":"allow-localdata-write-recursive","description":"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.","permissions":["write-all","scope-localdata-recursive"]},"allow-log-meta":{"identifier":"allow-log-meta","description":"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.","permissions":["read-meta","scope-log-index"]},"allow-log-meta-recursive":{"identifier":"allow-log-meta-recursive","description":"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.","permissions":["read-meta","scope-log-recursive"]},"allow-log-read":{"identifier":"allow-log-read","description":"This allows non-recursive read access to the `$LOG` folder.","permissions":["read-all","scope-log"]},"allow-log-read-recursive":{"identifier":"allow-log-read-recursive","description":"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.","permissions":["read-all","scope-log-recursive"]},"allow-log-write":{"identifier":"allow-log-write","description":"This allows non-recursive write access to the `$LOG` folder.","permissions":["write-all","scope-log"]},"allow-log-write-recursive":{"identifier":"allow-log-write-recursive","description":"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.","permissions":["write-all","scope-log-recursive"]},"allow-picture-meta":{"identifier":"allow-picture-meta","description":"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.","permissions":["read-meta","scope-picture-index"]},"allow-picture-meta-recursive":{"identifier":"allow-picture-meta-recursive","description":"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.","permissions":["read-meta","scope-picture-recursive"]},"allow-picture-read":{"identifier":"allow-picture-read","description":"This allows non-recursive read access to the `$PICTURE` folder.","permissions":["read-all","scope-picture"]},"allow-picture-read-recursive":{"identifier":"allow-picture-read-recursive","description":"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.","permissions":["read-all","scope-picture-recursive"]},"allow-picture-write":{"identifier":"allow-picture-write","description":"This allows non-recursive write access to the `$PICTURE` folder.","permissions":["write-all","scope-picture"]},"allow-picture-write-recursive":{"identifier":"allow-picture-write-recursive","description":"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.","permissions":["write-all","scope-picture-recursive"]},"allow-public-meta":{"identifier":"allow-public-meta","description":"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.","permissions":["read-meta","scope-public-index"]},"allow-public-meta-recursive":{"identifier":"allow-public-meta-recursive","description":"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.","permissions":["read-meta","scope-public-recursive"]},"allow-public-read":{"identifier":"allow-public-read","description":"This allows non-recursive read access to the `$PUBLIC` folder.","permissions":["read-all","scope-public"]},"allow-public-read-recursive":{"identifier":"allow-public-read-recursive","description":"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.","permissions":["read-all","scope-public-recursive"]},"allow-public-write":{"identifier":"allow-public-write","description":"This allows non-recursive write access to the `$PUBLIC` folder.","permissions":["write-all","scope-public"]},"allow-public-write-recursive":{"identifier":"allow-public-write-recursive","description":"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.","permissions":["write-all","scope-public-recursive"]},"allow-resource-meta":{"identifier":"allow-resource-meta","description":"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.","permissions":["read-meta","scope-resource-index"]},"allow-resource-meta-recursive":{"identifier":"allow-resource-meta-recursive","description":"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.","permissions":["read-meta","scope-resource-recursive"]},"allow-resource-read":{"identifier":"allow-resource-read","description":"This allows non-recursive read access to the `$RESOURCE` folder.","permissions":["read-all","scope-resource"]},"allow-resource-read-recursive":{"identifier":"allow-resource-read-recursive","description":"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.","permissions":["read-all","scope-resource-recursive"]},"allow-resource-write":{"identifier":"allow-resource-write","description":"This allows non-recursive write access to the `$RESOURCE` folder.","permissions":["write-all","scope-resource"]},"allow-resource-write-recursive":{"identifier":"allow-resource-write-recursive","description":"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.","permissions":["write-all","scope-resource-recursive"]},"allow-runtime-meta":{"identifier":"allow-runtime-meta","description":"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.","permissions":["read-meta","scope-runtime-index"]},"allow-runtime-meta-recursive":{"identifier":"allow-runtime-meta-recursive","description":"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.","permissions":["read-meta","scope-runtime-recursive"]},"allow-runtime-read":{"identifier":"allow-runtime-read","description":"This allows non-recursive read access to the `$RUNTIME` folder.","permissions":["read-all","scope-runtime"]},"allow-runtime-read-recursive":{"identifier":"allow-runtime-read-recursive","description":"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.","permissions":["read-all","scope-runtime-recursive"]},"allow-runtime-write":{"identifier":"allow-runtime-write","description":"This allows non-recursive write access to the `$RUNTIME` folder.","permissions":["write-all","scope-runtime"]},"allow-runtime-write-recursive":{"identifier":"allow-runtime-write-recursive","description":"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.","permissions":["write-all","scope-runtime-recursive"]},"allow-temp-meta":{"identifier":"allow-temp-meta","description":"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.","permissions":["read-meta","scope-temp-index"]},"allow-temp-meta-recursive":{"identifier":"allow-temp-meta-recursive","description":"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.","permissions":["read-meta","scope-temp-recursive"]},"allow-temp-read":{"identifier":"allow-temp-read","description":"This allows non-recursive read access to the `$TEMP` folder.","permissions":["read-all","scope-temp"]},"allow-temp-read-recursive":{"identifier":"allow-temp-read-recursive","description":"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.","permissions":["read-all","scope-temp-recursive"]},"allow-temp-write":{"identifier":"allow-temp-write","description":"This allows non-recursive write access to the `$TEMP` folder.","permissions":["write-all","scope-temp"]},"allow-temp-write-recursive":{"identifier":"allow-temp-write-recursive","description":"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.","permissions":["write-all","scope-temp-recursive"]},"allow-template-meta":{"identifier":"allow-template-meta","description":"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.","permissions":["read-meta","scope-template-index"]},"allow-template-meta-recursive":{"identifier":"allow-template-meta-recursive","description":"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.","permissions":["read-meta","scope-template-recursive"]},"allow-template-read":{"identifier":"allow-template-read","description":"This allows non-recursive read access to the `$TEMPLATE` folder.","permissions":["read-all","scope-template"]},"allow-template-read-recursive":{"identifier":"allow-template-read-recursive","description":"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.","permissions":["read-all","scope-template-recursive"]},"allow-template-write":{"identifier":"allow-template-write","description":"This allows non-recursive write access to the `$TEMPLATE` folder.","permissions":["write-all","scope-template"]},"allow-template-write-recursive":{"identifier":"allow-template-write-recursive","description":"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.","permissions":["write-all","scope-template-recursive"]},"allow-video-meta":{"identifier":"allow-video-meta","description":"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.","permissions":["read-meta","scope-video-index"]},"allow-video-meta-recursive":{"identifier":"allow-video-meta-recursive","description":"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.","permissions":["read-meta","scope-video-recursive"]},"allow-video-read":{"identifier":"allow-video-read","description":"This allows non-recursive read access to the `$VIDEO` folder.","permissions":["read-all","scope-video"]},"allow-video-read-recursive":{"identifier":"allow-video-read-recursive","description":"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.","permissions":["read-all","scope-video-recursive"]},"allow-video-write":{"identifier":"allow-video-write","description":"This allows non-recursive write access to the `$VIDEO` folder.","permissions":["write-all","scope-video"]},"allow-video-write-recursive":{"identifier":"allow-video-write-recursive","description":"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.","permissions":["write-all","scope-video-recursive"]},"deny-default":{"identifier":"deny-default","description":"This denies access to dangerous Tauri relevant files and folders by default.","permissions":["deny-webview-data-linux","deny-webview-data-windows"]}},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"description":"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},{"properties":{"path":{"description":"A path that can be accessed by the webview when using the fs APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"}},"required":["path"],"type":"object"}],"description":"FS scope entry.","title":"FsScopeEntry"}},"positioner":{"default_permission":{"identifier":"default","description":"Allows the moveWindow and handleIconState APIs","permissions":["allow-move-window","allow-move-window-constrained","allow-set-tray-icon-state"]},"permissions":{"allow-move-window":{"identifier":"allow-move-window","description":"Enables the move_window command without any pre-configured scope.","commands":{"allow":["move_window"],"deny":[]}},"allow-move-window-constrained":{"identifier":"allow-move-window-constrained","description":"Enables the move_window_constrained command without any pre-configured scope.","commands":{"allow":["move_window_constrained"],"deny":[]}},"allow-set-tray-icon-state":{"identifier":"allow-set-tray-icon-state","description":"Enables the set_tray_icon_state command without any pre-configured scope.","commands":{"allow":["set_tray_icon_state"],"deny":[]}},"deny-move-window":{"identifier":"deny-move-window","description":"Denies the move_window command without any pre-configured scope.","commands":{"allow":[],"deny":["move_window"]}},"deny-move-window-constrained":{"identifier":"deny-move-window-constrained","description":"Denies the move_window_constrained command without any pre-configured scope.","commands":{"allow":[],"deny":["move_window_constrained"]}},"deny-set-tray-icon-state":{"identifier":"deny-set-tray-icon-state","description":"Denies the set_tray_icon_state command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tray_icon_state"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json deleted file mode 100644 index f5273c7d7..000000000 --- a/src-tauri/gen/schemas/capabilities.json +++ /dev/null @@ -1 +0,0 @@ -{"main-capability":{"identifier":"main-capability","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","dialog:allow-ask","dialog:allow-confirm","dialog:allow-message","dialog:allow-open","dialog:allow-save","fs:allow-download-write","core:resources:default","core:menu:default","core:tray:default","shell:allow-open"],"platforms":["linux","macOS","windows","android","iOS"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json deleted file mode 100644 index 7ee3c87f4..000000000 --- a/src-tauri/gen/schemas/desktop-schema.json +++ /dev/null @@ -1,5181 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n", - "type": "string", - "const": "fs:default" - }, - { - "description": "This allows non-recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta" - }, - { - "description": "This allows full recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the application folders.", - "type": "string", - "const": "fs:allow-app-read" - }, - { - "description": "This allows full recursive read access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-read-recursive" - }, - { - "description": "This allows non-recursive write access to the application folders.", - "type": "string", - "const": "fs:allow-app-write" - }, - { - "description": "This allows full recursive write access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-read" - }, - { - "description": "This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-write" - }, - { - "description": "This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-read" - }, - { - "description": "This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-write" - }, - { - "description": "This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-read" - }, - { - "description": "This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-write" - }, - { - "description": "This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-read" - }, - { - "description": "This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-write" - }, - { - "description": "This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-read" - }, - { - "description": "This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-write" - }, - { - "description": "This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-read" - }, - { - "description": "This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-write" - }, - { - "description": "This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-read" - }, - { - "description": "This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-write" - }, - { - "description": "This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-read" - }, - { - "description": "This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-write" - }, - { - "description": "This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-read" - }, - { - "description": "This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-write" - }, - { - "description": "This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-read" - }, - { - "description": "This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-write" - }, - { - "description": "This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-read" - }, - { - "description": "This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-write" - }, - { - "description": "This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-read" - }, - { - "description": "This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-write" - }, - { - "description": "This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-read" - }, - { - "description": "This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-write" - }, - { - "description": "This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-write-recursive" - }, - { - "description": "This denies access to dangerous Tauri relevant files and folders by default.", - "type": "string", - "const": "fs:deny-default" - }, - { - "description": "Enables the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-copy-file" - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-create" - }, - { - "description": "Enables the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-exists" - }, - { - "description": "Enables the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-fstat" - }, - { - "description": "Enables the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-ftruncate" - }, - { - "description": "Enables the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-lstat" - }, - { - "description": "Enables the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-mkdir" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-open" - }, - { - "description": "Enables the read command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read" - }, - { - "description": "Enables the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-dir" - }, - { - "description": "Enables the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-file" - }, - { - "description": "Enables the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file" - }, - { - "description": "Enables the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines" - }, - { - "description": "Enables the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines-next" - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-remove" - }, - { - "description": "Enables the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-rename" - }, - { - "description": "Enables the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-seek" - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-size" - }, - { - "description": "Enables the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-stat" - }, - { - "description": "Enables the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-truncate" - }, - { - "description": "Enables the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-unwatch" - }, - { - "description": "Enables the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-watch" - }, - { - "description": "Enables the write command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write" - }, - { - "description": "Enables the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-file" - }, - { - "description": "Enables the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-text-file" - }, - { - "description": "This permissions allows to create the application specific directories.\n", - "type": "string", - "const": "fs:create-app-specific-dirs" - }, - { - "description": "Denies the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-copy-file" - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-create" - }, - { - "description": "Denies the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-exists" - }, - { - "description": "Denies the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-fstat" - }, - { - "description": "Denies the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-ftruncate" - }, - { - "description": "Denies the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-lstat" - }, - { - "description": "Denies the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-mkdir" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-open" - }, - { - "description": "Denies the read command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read" - }, - { - "description": "Denies the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-dir" - }, - { - "description": "Denies the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-file" - }, - { - "description": "Denies the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file" - }, - { - "description": "Denies the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines" - }, - { - "description": "Denies the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines-next" - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-remove" - }, - { - "description": "Denies the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-rename" - }, - { - "description": "Denies the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-seek" - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-size" - }, - { - "description": "Denies the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-stat" - }, - { - "description": "Denies the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-truncate" - }, - { - "description": "Denies the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-unwatch" - }, - { - "description": "Denies the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-watch" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-linux" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-windows" - }, - { - "description": "Denies the write command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write" - }, - { - "description": "Denies the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-file" - }, - { - "description": "Denies the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-text-file" - }, - { - "description": "This enables all read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-all" - }, - { - "description": "This permission allows recursive read functionality on the application\nspecific base directories. \n", - "type": "string", - "const": "fs:read-app-specific-dirs-recursive" - }, - { - "description": "This enables directory read and file metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-dirs" - }, - { - "description": "This enables file read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-files" - }, - { - "description": "This enables all index or metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-meta" - }, - { - "description": "An empty permission you can use to modify the global scope.", - "type": "string", - "const": "fs:scope" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the application folders.", - "type": "string", - "const": "fs:scope-app" - }, - { - "description": "This scope permits to list all files and folders in the application directories.", - "type": "string", - "const": "fs:scope-app-index" - }, - { - "description": "This scope permits recursive access to the complete application folders, including sub directories and files.", - "type": "string", - "const": "fs:scope-app-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.", - "type": "string", - "const": "fs:scope-appcache" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCACHE`folder.", - "type": "string", - "const": "fs:scope-appcache-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appcache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:scope-appconfig" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCONFIG`folder.", - "type": "string", - "const": "fs:scope-appconfig-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appconfig-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.", - "type": "string", - "const": "fs:scope-appdata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPDATA`folder.", - "type": "string", - "const": "fs:scope-appdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:scope-applocaldata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOCALDATA`folder.", - "type": "string", - "const": "fs:scope-applocaldata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applocaldata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.", - "type": "string", - "const": "fs:scope-applog" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOG`folder.", - "type": "string", - "const": "fs:scope-applog-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applog-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.", - "type": "string", - "const": "fs:scope-audio" - }, - { - "description": "This scope permits to list all files and folders in the `$AUDIO`folder.", - "type": "string", - "const": "fs:scope-audio-index" - }, - { - "description": "This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-audio-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CACHE` folder.", - "type": "string", - "const": "fs:scope-cache" - }, - { - "description": "This scope permits to list all files and folders in the `$CACHE`folder.", - "type": "string", - "const": "fs:scope-cache-index" - }, - { - "description": "This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-cache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.", - "type": "string", - "const": "fs:scope-config" - }, - { - "description": "This scope permits to list all files and folders in the `$CONFIG`folder.", - "type": "string", - "const": "fs:scope-config-index" - }, - { - "description": "This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-config-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DATA` folder.", - "type": "string", - "const": "fs:scope-data" - }, - { - "description": "This scope permits to list all files and folders in the `$DATA`folder.", - "type": "string", - "const": "fs:scope-data-index" - }, - { - "description": "This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-data-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.", - "type": "string", - "const": "fs:scope-desktop" - }, - { - "description": "This scope permits to list all files and folders in the `$DESKTOP`folder.", - "type": "string", - "const": "fs:scope-desktop-index" - }, - { - "description": "This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-desktop-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:scope-document" - }, - { - "description": "This scope permits to list all files and folders in the `$DOCUMENT`folder.", - "type": "string", - "const": "fs:scope-document-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-document-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:scope-download" - }, - { - "description": "This scope permits to list all files and folders in the `$DOWNLOAD`folder.", - "type": "string", - "const": "fs:scope-download-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-download-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$EXE` folder.", - "type": "string", - "const": "fs:scope-exe" - }, - { - "description": "This scope permits to list all files and folders in the `$EXE`folder.", - "type": "string", - "const": "fs:scope-exe-index" - }, - { - "description": "This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-exe-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$FONT` folder.", - "type": "string", - "const": "fs:scope-font" - }, - { - "description": "This scope permits to list all files and folders in the `$FONT`folder.", - "type": "string", - "const": "fs:scope-font-index" - }, - { - "description": "This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-font-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$HOME` folder.", - "type": "string", - "const": "fs:scope-home" - }, - { - "description": "This scope permits to list all files and folders in the `$HOME`folder.", - "type": "string", - "const": "fs:scope-home-index" - }, - { - "description": "This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-home-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:scope-localdata" - }, - { - "description": "This scope permits to list all files and folders in the `$LOCALDATA`folder.", - "type": "string", - "const": "fs:scope-localdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-localdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOG` folder.", - "type": "string", - "const": "fs:scope-log" - }, - { - "description": "This scope permits to list all files and folders in the `$LOG`folder.", - "type": "string", - "const": "fs:scope-log-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-log-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.", - "type": "string", - "const": "fs:scope-picture" - }, - { - "description": "This scope permits to list all files and folders in the `$PICTURE`folder.", - "type": "string", - "const": "fs:scope-picture-index" - }, - { - "description": "This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-picture-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.", - "type": "string", - "const": "fs:scope-public" - }, - { - "description": "This scope permits to list all files and folders in the `$PUBLIC`folder.", - "type": "string", - "const": "fs:scope-public-index" - }, - { - "description": "This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-public-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.", - "type": "string", - "const": "fs:scope-resource" - }, - { - "description": "This scope permits to list all files and folders in the `$RESOURCE`folder.", - "type": "string", - "const": "fs:scope-resource-index" - }, - { - "description": "This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-resource-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.", - "type": "string", - "const": "fs:scope-runtime" - }, - { - "description": "This scope permits to list all files and folders in the `$RUNTIME`folder.", - "type": "string", - "const": "fs:scope-runtime-index" - }, - { - "description": "This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-runtime-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMP` folder.", - "type": "string", - "const": "fs:scope-temp" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMP`folder.", - "type": "string", - "const": "fs:scope-temp-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-temp-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:scope-template" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMPLATE`folder.", - "type": "string", - "const": "fs:scope-template-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-template-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.", - "type": "string", - "const": "fs:scope-video" - }, - { - "description": "This scope permits to list all files and folders in the `$VIDEO`folder.", - "type": "string", - "const": "fs:scope-video-index" - }, - { - "description": "This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-video-recursive" - }, - { - "description": "This enables all write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-all" - }, - { - "description": "This enables all file write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-files" - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "FsScopeEntry", - "description": "FS scope entry.", - "anyOf": [ - { - "description": "A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - { - "type": "object", - "required": [ - "path" - ], - "properties": { - "path": { - "description": "A path that can be accessed by the webview when using the fs APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - } - } - } - ] - } - }, - "deny": { - "items": { - "title": "FsScopeEntry", - "description": "FS scope entry.", - "anyOf": [ - { - "description": "A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - { - "type": "object", - "required": [ - "path" - ], - "properties": { - "path": { - "description": "A path that can be accessed by the webview when using the fs APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - } - } - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", - "type": "string", - "const": "shell:default" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute" - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open" - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn" - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write" - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute" - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open" - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn" - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write" - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - }, - "deny": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n", - "type": "string", - "const": "core:default" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:app:default" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide" - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show" - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon" - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name" - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme" - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version" - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version" - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide" - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show" - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon" - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name" - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme" - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version" - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:event:default" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit" - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to" - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen" - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten" - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit" - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to" - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen" - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:image:default" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes" - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path" - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new" - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba" - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size" - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes" - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path" - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new" - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba" - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:menu:default" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append" - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default" - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get" - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert" - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked" - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled" - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items" - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new" - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup" - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend" - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove" - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at" - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator" - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu" - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp" - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu" - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp" - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked" - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled" - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon" - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text" - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text" - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append" - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default" - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get" - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert" - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked" - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled" - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items" - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new" - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup" - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend" - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove" - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at" - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator" - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu" - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp" - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu" - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp" - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked" - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled" - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon" - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text" - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:path:default" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename" - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname" - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname" - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute" - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join" - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize" - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve" - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory" - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename" - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname" - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname" - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute" - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join" - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize" - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve" - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:resources:default" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close" - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:tray:default" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id" - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new" - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id" - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon" - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template" - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu" - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click" - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path" - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title" - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip" - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible" - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id" - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new" - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id" - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon" - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template" - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu" - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click" - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path" - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title" - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip" - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:webview:default" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data" - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview" - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window" - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews" - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools" - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print" - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent" - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color" - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus" - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position" - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size" - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom" - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close" - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide" - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position" - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show" - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size" - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data" - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview" - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window" - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews" - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools" - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print" - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent" - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color" - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus" - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position" - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size" - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom" - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close" - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide" - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position" - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show" - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:window:default" - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors" - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close" - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create" - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor" - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position" - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy" - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows" - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide" - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position" - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size" - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize" - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable" - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated" - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled" - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused" - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen" - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable" - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized" - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable" - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized" - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable" - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible" - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize" - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize" - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point" - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position" - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size" - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor" - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention" - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor" - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom" - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top" - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color" - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count" - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label" - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable" - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected" - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab" - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon" - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position" - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible" - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations" - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects" - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled" - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus" - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen" - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon" - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events" - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size" - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable" - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size" - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable" - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon" - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position" - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar" - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable" - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow" - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size" - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints" - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar" - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme" - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title" - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style" - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces" - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show" - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging" - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging" - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme" - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title" - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize" - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize" - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize" - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors" - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center" - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close" - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create" - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor" - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position" - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy" - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows" - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide" - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position" - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size" - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize" - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable" - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated" - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled" - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused" - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen" - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable" - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized" - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable" - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized" - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable" - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible" - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize" - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize" - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point" - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position" - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size" - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor" - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention" - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor" - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom" - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top" - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color" - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count" - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label" - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable" - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected" - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab" - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon" - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position" - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible" - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations" - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects" - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled" - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus" - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen" - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon" - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events" - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size" - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable" - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size" - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable" - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon" - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position" - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar" - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable" - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow" - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size" - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints" - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar" - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme" - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title" - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style" - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces" - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show" - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging" - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging" - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme" - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title" - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize" - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize" - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize" - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n", - "type": "string", - "const": "dialog:default" - }, - { - "description": "Enables the ask command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-ask" - }, - { - "description": "Enables the confirm command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-confirm" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open" - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save" - }, - { - "description": "Denies the ask command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-ask" - }, - { - "description": "Denies the confirm command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-confirm" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open" - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save" - }, - { - "description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n", - "type": "string", - "const": "fs:default" - }, - { - "description": "This allows non-recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta" - }, - { - "description": "This allows full recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the application folders.", - "type": "string", - "const": "fs:allow-app-read" - }, - { - "description": "This allows full recursive read access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-read-recursive" - }, - { - "description": "This allows non-recursive write access to the application folders.", - "type": "string", - "const": "fs:allow-app-write" - }, - { - "description": "This allows full recursive write access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-read" - }, - { - "description": "This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-write" - }, - { - "description": "This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-read" - }, - { - "description": "This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-write" - }, - { - "description": "This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-read" - }, - { - "description": "This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-write" - }, - { - "description": "This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-read" - }, - { - "description": "This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-write" - }, - { - "description": "This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-read" - }, - { - "description": "This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-write" - }, - { - "description": "This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-read" - }, - { - "description": "This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-write" - }, - { - "description": "This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-read" - }, - { - "description": "This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-write" - }, - { - "description": "This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-read" - }, - { - "description": "This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-write" - }, - { - "description": "This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-read" - }, - { - "description": "This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-write" - }, - { - "description": "This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-read" - }, - { - "description": "This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-write" - }, - { - "description": "This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-read" - }, - { - "description": "This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-write" - }, - { - "description": "This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-read" - }, - { - "description": "This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-write" - }, - { - "description": "This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-read" - }, - { - "description": "This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-write" - }, - { - "description": "This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-write-recursive" - }, - { - "description": "This denies access to dangerous Tauri relevant files and folders by default.", - "type": "string", - "const": "fs:deny-default" - }, - { - "description": "Enables the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-copy-file" - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-create" - }, - { - "description": "Enables the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-exists" - }, - { - "description": "Enables the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-fstat" - }, - { - "description": "Enables the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-ftruncate" - }, - { - "description": "Enables the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-lstat" - }, - { - "description": "Enables the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-mkdir" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-open" - }, - { - "description": "Enables the read command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read" - }, - { - "description": "Enables the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-dir" - }, - { - "description": "Enables the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-file" - }, - { - "description": "Enables the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file" - }, - { - "description": "Enables the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines" - }, - { - "description": "Enables the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines-next" - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-remove" - }, - { - "description": "Enables the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-rename" - }, - { - "description": "Enables the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-seek" - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-size" - }, - { - "description": "Enables the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-stat" - }, - { - "description": "Enables the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-truncate" - }, - { - "description": "Enables the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-unwatch" - }, - { - "description": "Enables the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-watch" - }, - { - "description": "Enables the write command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write" - }, - { - "description": "Enables the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-file" - }, - { - "description": "Enables the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-text-file" - }, - { - "description": "This permissions allows to create the application specific directories.\n", - "type": "string", - "const": "fs:create-app-specific-dirs" - }, - { - "description": "Denies the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-copy-file" - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-create" - }, - { - "description": "Denies the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-exists" - }, - { - "description": "Denies the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-fstat" - }, - { - "description": "Denies the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-ftruncate" - }, - { - "description": "Denies the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-lstat" - }, - { - "description": "Denies the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-mkdir" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-open" - }, - { - "description": "Denies the read command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read" - }, - { - "description": "Denies the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-dir" - }, - { - "description": "Denies the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-file" - }, - { - "description": "Denies the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file" - }, - { - "description": "Denies the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines" - }, - { - "description": "Denies the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines-next" - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-remove" - }, - { - "description": "Denies the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-rename" - }, - { - "description": "Denies the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-seek" - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-size" - }, - { - "description": "Denies the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-stat" - }, - { - "description": "Denies the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-truncate" - }, - { - "description": "Denies the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-unwatch" - }, - { - "description": "Denies the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-watch" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-linux" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-windows" - }, - { - "description": "Denies the write command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write" - }, - { - "description": "Denies the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-file" - }, - { - "description": "Denies the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-text-file" - }, - { - "description": "This enables all read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-all" - }, - { - "description": "This permission allows recursive read functionality on the application\nspecific base directories. \n", - "type": "string", - "const": "fs:read-app-specific-dirs-recursive" - }, - { - "description": "This enables directory read and file metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-dirs" - }, - { - "description": "This enables file read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-files" - }, - { - "description": "This enables all index or metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-meta" - }, - { - "description": "An empty permission you can use to modify the global scope.", - "type": "string", - "const": "fs:scope" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the application folders.", - "type": "string", - "const": "fs:scope-app" - }, - { - "description": "This scope permits to list all files and folders in the application directories.", - "type": "string", - "const": "fs:scope-app-index" - }, - { - "description": "This scope permits recursive access to the complete application folders, including sub directories and files.", - "type": "string", - "const": "fs:scope-app-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.", - "type": "string", - "const": "fs:scope-appcache" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCACHE`folder.", - "type": "string", - "const": "fs:scope-appcache-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appcache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:scope-appconfig" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCONFIG`folder.", - "type": "string", - "const": "fs:scope-appconfig-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appconfig-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.", - "type": "string", - "const": "fs:scope-appdata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPDATA`folder.", - "type": "string", - "const": "fs:scope-appdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:scope-applocaldata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOCALDATA`folder.", - "type": "string", - "const": "fs:scope-applocaldata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applocaldata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.", - "type": "string", - "const": "fs:scope-applog" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOG`folder.", - "type": "string", - "const": "fs:scope-applog-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applog-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.", - "type": "string", - "const": "fs:scope-audio" - }, - { - "description": "This scope permits to list all files and folders in the `$AUDIO`folder.", - "type": "string", - "const": "fs:scope-audio-index" - }, - { - "description": "This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-audio-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CACHE` folder.", - "type": "string", - "const": "fs:scope-cache" - }, - { - "description": "This scope permits to list all files and folders in the `$CACHE`folder.", - "type": "string", - "const": "fs:scope-cache-index" - }, - { - "description": "This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-cache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.", - "type": "string", - "const": "fs:scope-config" - }, - { - "description": "This scope permits to list all files and folders in the `$CONFIG`folder.", - "type": "string", - "const": "fs:scope-config-index" - }, - { - "description": "This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-config-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DATA` folder.", - "type": "string", - "const": "fs:scope-data" - }, - { - "description": "This scope permits to list all files and folders in the `$DATA`folder.", - "type": "string", - "const": "fs:scope-data-index" - }, - { - "description": "This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-data-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.", - "type": "string", - "const": "fs:scope-desktop" - }, - { - "description": "This scope permits to list all files and folders in the `$DESKTOP`folder.", - "type": "string", - "const": "fs:scope-desktop-index" - }, - { - "description": "This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-desktop-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:scope-document" - }, - { - "description": "This scope permits to list all files and folders in the `$DOCUMENT`folder.", - "type": "string", - "const": "fs:scope-document-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-document-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:scope-download" - }, - { - "description": "This scope permits to list all files and folders in the `$DOWNLOAD`folder.", - "type": "string", - "const": "fs:scope-download-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-download-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$EXE` folder.", - "type": "string", - "const": "fs:scope-exe" - }, - { - "description": "This scope permits to list all files and folders in the `$EXE`folder.", - "type": "string", - "const": "fs:scope-exe-index" - }, - { - "description": "This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-exe-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$FONT` folder.", - "type": "string", - "const": "fs:scope-font" - }, - { - "description": "This scope permits to list all files and folders in the `$FONT`folder.", - "type": "string", - "const": "fs:scope-font-index" - }, - { - "description": "This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-font-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$HOME` folder.", - "type": "string", - "const": "fs:scope-home" - }, - { - "description": "This scope permits to list all files and folders in the `$HOME`folder.", - "type": "string", - "const": "fs:scope-home-index" - }, - { - "description": "This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-home-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:scope-localdata" - }, - { - "description": "This scope permits to list all files and folders in the `$LOCALDATA`folder.", - "type": "string", - "const": "fs:scope-localdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-localdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOG` folder.", - "type": "string", - "const": "fs:scope-log" - }, - { - "description": "This scope permits to list all files and folders in the `$LOG`folder.", - "type": "string", - "const": "fs:scope-log-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-log-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.", - "type": "string", - "const": "fs:scope-picture" - }, - { - "description": "This scope permits to list all files and folders in the `$PICTURE`folder.", - "type": "string", - "const": "fs:scope-picture-index" - }, - { - "description": "This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-picture-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.", - "type": "string", - "const": "fs:scope-public" - }, - { - "description": "This scope permits to list all files and folders in the `$PUBLIC`folder.", - "type": "string", - "const": "fs:scope-public-index" - }, - { - "description": "This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-public-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.", - "type": "string", - "const": "fs:scope-resource" - }, - { - "description": "This scope permits to list all files and folders in the `$RESOURCE`folder.", - "type": "string", - "const": "fs:scope-resource-index" - }, - { - "description": "This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-resource-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.", - "type": "string", - "const": "fs:scope-runtime" - }, - { - "description": "This scope permits to list all files and folders in the `$RUNTIME`folder.", - "type": "string", - "const": "fs:scope-runtime-index" - }, - { - "description": "This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-runtime-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMP` folder.", - "type": "string", - "const": "fs:scope-temp" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMP`folder.", - "type": "string", - "const": "fs:scope-temp-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-temp-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:scope-template" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMPLATE`folder.", - "type": "string", - "const": "fs:scope-template-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-template-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.", - "type": "string", - "const": "fs:scope-video" - }, - { - "description": "This scope permits to list all files and folders in the `$VIDEO`folder.", - "type": "string", - "const": "fs:scope-video-index" - }, - { - "description": "This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-video-recursive" - }, - { - "description": "This enables all write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-all" - }, - { - "description": "This enables all file write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-files" - }, - { - "description": "Allows the moveWindow and handleIconState APIs", - "type": "string", - "const": "positioner:default" - }, - { - "description": "Enables the move_window command without any pre-configured scope.", - "type": "string", - "const": "positioner:allow-move-window" - }, - { - "description": "Enables the move_window_constrained command without any pre-configured scope.", - "type": "string", - "const": "positioner:allow-move-window-constrained" - }, - { - "description": "Enables the set_tray_icon_state command without any pre-configured scope.", - "type": "string", - "const": "positioner:allow-set-tray-icon-state" - }, - { - "description": "Denies the move_window command without any pre-configured scope.", - "type": "string", - "const": "positioner:deny-move-window" - }, - { - "description": "Denies the move_window_constrained command without any pre-configured scope.", - "type": "string", - "const": "positioner:deny-move-window-constrained" - }, - { - "description": "Denies the set_tray_icon_state command without any pre-configured scope.", - "type": "string", - "const": "positioner:deny-set-tray-icon-state" - }, - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", - "type": "string", - "const": "shell:default" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute" - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open" - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn" - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write" - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute" - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open" - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn" - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write" - }, - { - "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n", - "type": "string", - "const": "window-state:default" - }, - { - "description": "Enables the filename command without any pre-configured scope.", - "type": "string", - "const": "window-state:allow-filename" - }, - { - "description": "Enables the restore_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:allow-restore-state" - }, - { - "description": "Enables the save_window_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:allow-save-window-state" - }, - { - "description": "Denies the filename command without any pre-configured scope.", - "type": "string", - "const": "window-state:deny-filename" - }, - { - "description": "Denies the restore_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:deny-restore-state" - }, - { - "description": "Denies the save_window_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:deny-save-window-state" - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - }, - "ShellScopeEntryAllowedArg": { - "description": "A command argument allowed to be executed by the webview API.", - "anyOf": [ - { - "description": "A non-configurable argument that is passed to the command in the order it was specified.", - "type": "string" - }, - { - "description": "A variable that is set while calling the command from the webview API.", - "type": "object", - "required": [ - "validator" - ], - "properties": { - "raw": { - "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", - "default": false, - "type": "boolean" - }, - "validator": { - "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "ShellScopeEntryAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", - "anyOf": [ - { - "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", - "type": "boolean" - }, - { - "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellScopeEntryAllowedArg" - } - } - ] - } - } -} \ No newline at end of file diff --git a/src-tauri/gen/schemas/macOS-schema.json b/src-tauri/gen/schemas/macOS-schema.json deleted file mode 100644 index 7ee3c87f4..000000000 --- a/src-tauri/gen/schemas/macOS-schema.json +++ /dev/null @@ -1,5181 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n", - "type": "string", - "const": "fs:default" - }, - { - "description": "This allows non-recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta" - }, - { - "description": "This allows full recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the application folders.", - "type": "string", - "const": "fs:allow-app-read" - }, - { - "description": "This allows full recursive read access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-read-recursive" - }, - { - "description": "This allows non-recursive write access to the application folders.", - "type": "string", - "const": "fs:allow-app-write" - }, - { - "description": "This allows full recursive write access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-read" - }, - { - "description": "This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-write" - }, - { - "description": "This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-read" - }, - { - "description": "This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-write" - }, - { - "description": "This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-read" - }, - { - "description": "This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-write" - }, - { - "description": "This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-read" - }, - { - "description": "This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-write" - }, - { - "description": "This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-read" - }, - { - "description": "This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-write" - }, - { - "description": "This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-read" - }, - { - "description": "This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-write" - }, - { - "description": "This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-read" - }, - { - "description": "This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-write" - }, - { - "description": "This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-read" - }, - { - "description": "This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-write" - }, - { - "description": "This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-read" - }, - { - "description": "This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-write" - }, - { - "description": "This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-read" - }, - { - "description": "This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-write" - }, - { - "description": "This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-read" - }, - { - "description": "This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-write" - }, - { - "description": "This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-read" - }, - { - "description": "This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-write" - }, - { - "description": "This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-read" - }, - { - "description": "This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-write" - }, - { - "description": "This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-write-recursive" - }, - { - "description": "This denies access to dangerous Tauri relevant files and folders by default.", - "type": "string", - "const": "fs:deny-default" - }, - { - "description": "Enables the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-copy-file" - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-create" - }, - { - "description": "Enables the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-exists" - }, - { - "description": "Enables the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-fstat" - }, - { - "description": "Enables the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-ftruncate" - }, - { - "description": "Enables the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-lstat" - }, - { - "description": "Enables the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-mkdir" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-open" - }, - { - "description": "Enables the read command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read" - }, - { - "description": "Enables the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-dir" - }, - { - "description": "Enables the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-file" - }, - { - "description": "Enables the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file" - }, - { - "description": "Enables the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines" - }, - { - "description": "Enables the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines-next" - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-remove" - }, - { - "description": "Enables the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-rename" - }, - { - "description": "Enables the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-seek" - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-size" - }, - { - "description": "Enables the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-stat" - }, - { - "description": "Enables the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-truncate" - }, - { - "description": "Enables the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-unwatch" - }, - { - "description": "Enables the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-watch" - }, - { - "description": "Enables the write command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write" - }, - { - "description": "Enables the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-file" - }, - { - "description": "Enables the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-text-file" - }, - { - "description": "This permissions allows to create the application specific directories.\n", - "type": "string", - "const": "fs:create-app-specific-dirs" - }, - { - "description": "Denies the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-copy-file" - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-create" - }, - { - "description": "Denies the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-exists" - }, - { - "description": "Denies the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-fstat" - }, - { - "description": "Denies the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-ftruncate" - }, - { - "description": "Denies the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-lstat" - }, - { - "description": "Denies the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-mkdir" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-open" - }, - { - "description": "Denies the read command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read" - }, - { - "description": "Denies the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-dir" - }, - { - "description": "Denies the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-file" - }, - { - "description": "Denies the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file" - }, - { - "description": "Denies the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines" - }, - { - "description": "Denies the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines-next" - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-remove" - }, - { - "description": "Denies the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-rename" - }, - { - "description": "Denies the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-seek" - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-size" - }, - { - "description": "Denies the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-stat" - }, - { - "description": "Denies the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-truncate" - }, - { - "description": "Denies the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-unwatch" - }, - { - "description": "Denies the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-watch" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-linux" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-windows" - }, - { - "description": "Denies the write command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write" - }, - { - "description": "Denies the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-file" - }, - { - "description": "Denies the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-text-file" - }, - { - "description": "This enables all read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-all" - }, - { - "description": "This permission allows recursive read functionality on the application\nspecific base directories. \n", - "type": "string", - "const": "fs:read-app-specific-dirs-recursive" - }, - { - "description": "This enables directory read and file metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-dirs" - }, - { - "description": "This enables file read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-files" - }, - { - "description": "This enables all index or metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-meta" - }, - { - "description": "An empty permission you can use to modify the global scope.", - "type": "string", - "const": "fs:scope" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the application folders.", - "type": "string", - "const": "fs:scope-app" - }, - { - "description": "This scope permits to list all files and folders in the application directories.", - "type": "string", - "const": "fs:scope-app-index" - }, - { - "description": "This scope permits recursive access to the complete application folders, including sub directories and files.", - "type": "string", - "const": "fs:scope-app-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.", - "type": "string", - "const": "fs:scope-appcache" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCACHE`folder.", - "type": "string", - "const": "fs:scope-appcache-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appcache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:scope-appconfig" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCONFIG`folder.", - "type": "string", - "const": "fs:scope-appconfig-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appconfig-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.", - "type": "string", - "const": "fs:scope-appdata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPDATA`folder.", - "type": "string", - "const": "fs:scope-appdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:scope-applocaldata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOCALDATA`folder.", - "type": "string", - "const": "fs:scope-applocaldata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applocaldata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.", - "type": "string", - "const": "fs:scope-applog" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOG`folder.", - "type": "string", - "const": "fs:scope-applog-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applog-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.", - "type": "string", - "const": "fs:scope-audio" - }, - { - "description": "This scope permits to list all files and folders in the `$AUDIO`folder.", - "type": "string", - "const": "fs:scope-audio-index" - }, - { - "description": "This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-audio-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CACHE` folder.", - "type": "string", - "const": "fs:scope-cache" - }, - { - "description": "This scope permits to list all files and folders in the `$CACHE`folder.", - "type": "string", - "const": "fs:scope-cache-index" - }, - { - "description": "This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-cache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.", - "type": "string", - "const": "fs:scope-config" - }, - { - "description": "This scope permits to list all files and folders in the `$CONFIG`folder.", - "type": "string", - "const": "fs:scope-config-index" - }, - { - "description": "This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-config-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DATA` folder.", - "type": "string", - "const": "fs:scope-data" - }, - { - "description": "This scope permits to list all files and folders in the `$DATA`folder.", - "type": "string", - "const": "fs:scope-data-index" - }, - { - "description": "This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-data-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.", - "type": "string", - "const": "fs:scope-desktop" - }, - { - "description": "This scope permits to list all files and folders in the `$DESKTOP`folder.", - "type": "string", - "const": "fs:scope-desktop-index" - }, - { - "description": "This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-desktop-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:scope-document" - }, - { - "description": "This scope permits to list all files and folders in the `$DOCUMENT`folder.", - "type": "string", - "const": "fs:scope-document-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-document-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:scope-download" - }, - { - "description": "This scope permits to list all files and folders in the `$DOWNLOAD`folder.", - "type": "string", - "const": "fs:scope-download-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-download-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$EXE` folder.", - "type": "string", - "const": "fs:scope-exe" - }, - { - "description": "This scope permits to list all files and folders in the `$EXE`folder.", - "type": "string", - "const": "fs:scope-exe-index" - }, - { - "description": "This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-exe-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$FONT` folder.", - "type": "string", - "const": "fs:scope-font" - }, - { - "description": "This scope permits to list all files and folders in the `$FONT`folder.", - "type": "string", - "const": "fs:scope-font-index" - }, - { - "description": "This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-font-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$HOME` folder.", - "type": "string", - "const": "fs:scope-home" - }, - { - "description": "This scope permits to list all files and folders in the `$HOME`folder.", - "type": "string", - "const": "fs:scope-home-index" - }, - { - "description": "This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-home-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:scope-localdata" - }, - { - "description": "This scope permits to list all files and folders in the `$LOCALDATA`folder.", - "type": "string", - "const": "fs:scope-localdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-localdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOG` folder.", - "type": "string", - "const": "fs:scope-log" - }, - { - "description": "This scope permits to list all files and folders in the `$LOG`folder.", - "type": "string", - "const": "fs:scope-log-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-log-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.", - "type": "string", - "const": "fs:scope-picture" - }, - { - "description": "This scope permits to list all files and folders in the `$PICTURE`folder.", - "type": "string", - "const": "fs:scope-picture-index" - }, - { - "description": "This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-picture-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.", - "type": "string", - "const": "fs:scope-public" - }, - { - "description": "This scope permits to list all files and folders in the `$PUBLIC`folder.", - "type": "string", - "const": "fs:scope-public-index" - }, - { - "description": "This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-public-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.", - "type": "string", - "const": "fs:scope-resource" - }, - { - "description": "This scope permits to list all files and folders in the `$RESOURCE`folder.", - "type": "string", - "const": "fs:scope-resource-index" - }, - { - "description": "This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-resource-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.", - "type": "string", - "const": "fs:scope-runtime" - }, - { - "description": "This scope permits to list all files and folders in the `$RUNTIME`folder.", - "type": "string", - "const": "fs:scope-runtime-index" - }, - { - "description": "This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-runtime-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMP` folder.", - "type": "string", - "const": "fs:scope-temp" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMP`folder.", - "type": "string", - "const": "fs:scope-temp-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-temp-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:scope-template" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMPLATE`folder.", - "type": "string", - "const": "fs:scope-template-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-template-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.", - "type": "string", - "const": "fs:scope-video" - }, - { - "description": "This scope permits to list all files and folders in the `$VIDEO`folder.", - "type": "string", - "const": "fs:scope-video-index" - }, - { - "description": "This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-video-recursive" - }, - { - "description": "This enables all write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-all" - }, - { - "description": "This enables all file write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-files" - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "FsScopeEntry", - "description": "FS scope entry.", - "anyOf": [ - { - "description": "A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - { - "type": "object", - "required": [ - "path" - ], - "properties": { - "path": { - "description": "A path that can be accessed by the webview when using the fs APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - } - } - } - ] - } - }, - "deny": { - "items": { - "title": "FsScopeEntry", - "description": "FS scope entry.", - "anyOf": [ - { - "description": "A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - { - "type": "object", - "required": [ - "path" - ], - "properties": { - "path": { - "description": "A path that can be accessed by the webview when using the fs APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - } - } - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", - "type": "string", - "const": "shell:default" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute" - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open" - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn" - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write" - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute" - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open" - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn" - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write" - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - }, - "deny": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set which includes:\n- 'core:path:default'\n- 'core:event:default'\n- 'core:window:default'\n- 'core:webview:default'\n- 'core:app:default'\n- 'core:image:default'\n- 'core:resources:default'\n- 'core:menu:default'\n- 'core:tray:default'\n", - "type": "string", - "const": "core:default" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:app:default" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide" - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show" - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon" - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name" - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme" - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version" - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version" - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide" - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show" - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon" - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name" - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme" - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version" - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:event:default" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit" - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to" - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen" - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten" - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit" - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to" - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen" - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:image:default" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes" - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path" - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new" - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba" - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size" - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes" - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path" - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new" - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba" - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:menu:default" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append" - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default" - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get" - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert" - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked" - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled" - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items" - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new" - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup" - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend" - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove" - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at" - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator" - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu" - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp" - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu" - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp" - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked" - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled" - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon" - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text" - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text" - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append" - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default" - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get" - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert" - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked" - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled" - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items" - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new" - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup" - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend" - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove" - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at" - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator" - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu" - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp" - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu" - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp" - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked" - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled" - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon" - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text" - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:path:default" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename" - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname" - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname" - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute" - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join" - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize" - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve" - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory" - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename" - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname" - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname" - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute" - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join" - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize" - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve" - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:resources:default" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close" - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:tray:default" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id" - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new" - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id" - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon" - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template" - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu" - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click" - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path" - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title" - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip" - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible" - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id" - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new" - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id" - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon" - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template" - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu" - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click" - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path" - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title" - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip" - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:webview:default" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data" - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview" - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window" - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews" - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools" - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print" - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent" - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color" - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus" - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position" - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size" - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom" - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close" - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide" - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position" - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show" - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size" - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data" - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview" - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window" - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews" - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools" - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print" - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent" - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color" - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus" - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position" - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size" - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom" - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close" - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide" - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position" - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show" - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size" - }, - { - "description": "Default permissions for the plugin.", - "type": "string", - "const": "core:window:default" - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors" - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close" - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create" - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor" - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position" - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy" - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows" - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide" - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position" - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size" - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize" - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable" - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated" - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled" - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused" - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen" - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable" - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized" - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable" - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized" - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable" - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible" - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize" - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize" - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point" - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position" - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size" - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor" - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention" - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor" - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom" - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top" - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color" - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count" - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label" - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable" - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected" - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab" - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon" - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position" - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible" - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations" - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects" - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled" - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus" - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen" - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon" - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events" - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size" - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable" - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size" - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable" - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon" - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position" - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar" - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable" - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow" - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size" - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints" - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar" - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme" - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title" - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style" - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces" - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show" - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging" - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging" - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme" - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title" - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize" - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize" - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize" - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors" - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center" - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close" - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create" - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor" - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position" - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy" - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows" - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide" - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position" - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size" - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize" - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable" - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated" - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled" - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused" - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen" - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable" - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized" - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable" - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized" - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable" - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible" - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize" - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize" - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point" - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position" - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size" - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor" - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention" - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor" - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom" - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top" - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color" - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count" - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label" - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable" - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected" - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab" - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon" - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position" - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible" - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations" - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects" - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled" - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus" - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen" - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon" - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events" - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size" - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable" - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size" - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable" - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon" - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position" - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar" - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable" - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow" - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size" - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints" - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar" - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme" - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title" - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style" - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces" - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show" - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging" - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging" - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme" - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title" - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize" - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize" - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize" - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n", - "type": "string", - "const": "dialog:default" - }, - { - "description": "Enables the ask command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-ask" - }, - { - "description": "Enables the confirm command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-confirm" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open" - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save" - }, - { - "description": "Denies the ask command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-ask" - }, - { - "description": "Denies the confirm command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-confirm" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open" - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save" - }, - { - "description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n", - "type": "string", - "const": "fs:default" - }, - { - "description": "This allows non-recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta" - }, - { - "description": "This allows full recursive read access to metadata of the application folders, including file listing and statistics.", - "type": "string", - "const": "fs:allow-app-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the application folders.", - "type": "string", - "const": "fs:allow-app-read" - }, - { - "description": "This allows full recursive read access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-read-recursive" - }, - { - "description": "This allows non-recursive write access to the application folders.", - "type": "string", - "const": "fs:allow-app-write" - }, - { - "description": "This allows full recursive write access to the complete application folders, files and subdirectories.", - "type": "string", - "const": "fs:allow-app-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appcache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCACHE` folder.", - "type": "string", - "const": "fs:allow-appcache-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appcache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appconfig-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:allow-appconfig-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appconfig-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-appdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPDATA` folder.", - "type": "string", - "const": "fs:allow-appdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-appdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applocaldata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:allow-applocaldata-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applocaldata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-applog-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-read" - }, - { - "description": "This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$APPLOG` folder.", - "type": "string", - "const": "fs:allow-applog-write" - }, - { - "description": "This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-applog-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-audio-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-read" - }, - { - "description": "This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$AUDIO` folder.", - "type": "string", - "const": "fs:allow-audio-write" - }, - { - "description": "This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-audio-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-cache-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-read" - }, - { - "description": "This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CACHE` folder.", - "type": "string", - "const": "fs:allow-cache-write" - }, - { - "description": "This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-cache-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-config-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-read" - }, - { - "description": "This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$CONFIG` folder.", - "type": "string", - "const": "fs:allow-config-write" - }, - { - "description": "This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-config-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-data-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-read" - }, - { - "description": "This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DATA` folder.", - "type": "string", - "const": "fs:allow-data-write" - }, - { - "description": "This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-data-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-desktop-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-read" - }, - { - "description": "This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DESKTOP` folder.", - "type": "string", - "const": "fs:allow-desktop-write" - }, - { - "description": "This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-desktop-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-document-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:allow-document-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-document-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-download-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-read" - }, - { - "description": "This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:allow-download-write" - }, - { - "description": "This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-download-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-exe-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-read" - }, - { - "description": "This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$EXE` folder.", - "type": "string", - "const": "fs:allow-exe-write" - }, - { - "description": "This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-exe-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-font-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-read" - }, - { - "description": "This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$FONT` folder.", - "type": "string", - "const": "fs:allow-font-write" - }, - { - "description": "This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-font-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-home-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-read" - }, - { - "description": "This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$HOME` folder.", - "type": "string", - "const": "fs:allow-home-write" - }, - { - "description": "This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-home-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-localdata-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:allow-localdata-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-localdata-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-log-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-read" - }, - { - "description": "This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$LOG` folder.", - "type": "string", - "const": "fs:allow-log-write" - }, - { - "description": "This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-log-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-picture-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-read" - }, - { - "description": "This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PICTURE` folder.", - "type": "string", - "const": "fs:allow-picture-write" - }, - { - "description": "This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-picture-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-public-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-read" - }, - { - "description": "This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$PUBLIC` folder.", - "type": "string", - "const": "fs:allow-public-write" - }, - { - "description": "This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-public-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-resource-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-read" - }, - { - "description": "This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RESOURCE` folder.", - "type": "string", - "const": "fs:allow-resource-write" - }, - { - "description": "This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-resource-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-runtime-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-read" - }, - { - "description": "This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$RUNTIME` folder.", - "type": "string", - "const": "fs:allow-runtime-write" - }, - { - "description": "This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-runtime-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-temp-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMP` folder.", - "type": "string", - "const": "fs:allow-temp-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-temp-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-template-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-read" - }, - { - "description": "This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:allow-template-write" - }, - { - "description": "This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-template-write-recursive" - }, - { - "description": "This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta" - }, - { - "description": "This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.", - "type": "string", - "const": "fs:allow-video-meta-recursive" - }, - { - "description": "This allows non-recursive read access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-read" - }, - { - "description": "This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-read-recursive" - }, - { - "description": "This allows non-recursive write access to the `$VIDEO` folder.", - "type": "string", - "const": "fs:allow-video-write" - }, - { - "description": "This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.", - "type": "string", - "const": "fs:allow-video-write-recursive" - }, - { - "description": "This denies access to dangerous Tauri relevant files and folders by default.", - "type": "string", - "const": "fs:deny-default" - }, - { - "description": "Enables the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-copy-file" - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-create" - }, - { - "description": "Enables the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-exists" - }, - { - "description": "Enables the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-fstat" - }, - { - "description": "Enables the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-ftruncate" - }, - { - "description": "Enables the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-lstat" - }, - { - "description": "Enables the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-mkdir" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-open" - }, - { - "description": "Enables the read command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read" - }, - { - "description": "Enables the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-dir" - }, - { - "description": "Enables the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-file" - }, - { - "description": "Enables the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file" - }, - { - "description": "Enables the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines" - }, - { - "description": "Enables the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-read-text-file-lines-next" - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-remove" - }, - { - "description": "Enables the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-rename" - }, - { - "description": "Enables the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-seek" - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-size" - }, - { - "description": "Enables the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-stat" - }, - { - "description": "Enables the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-truncate" - }, - { - "description": "Enables the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-unwatch" - }, - { - "description": "Enables the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-watch" - }, - { - "description": "Enables the write command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write" - }, - { - "description": "Enables the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-file" - }, - { - "description": "Enables the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:allow-write-text-file" - }, - { - "description": "This permissions allows to create the application specific directories.\n", - "type": "string", - "const": "fs:create-app-specific-dirs" - }, - { - "description": "Denies the copy_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-copy-file" - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-create" - }, - { - "description": "Denies the exists command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-exists" - }, - { - "description": "Denies the fstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-fstat" - }, - { - "description": "Denies the ftruncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-ftruncate" - }, - { - "description": "Denies the lstat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-lstat" - }, - { - "description": "Denies the mkdir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-mkdir" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-open" - }, - { - "description": "Denies the read command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read" - }, - { - "description": "Denies the read_dir command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-dir" - }, - { - "description": "Denies the read_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-file" - }, - { - "description": "Denies the read_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file" - }, - { - "description": "Denies the read_text_file_lines command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines" - }, - { - "description": "Denies the read_text_file_lines_next command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-read-text-file-lines-next" - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-remove" - }, - { - "description": "Denies the rename command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-rename" - }, - { - "description": "Denies the seek command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-seek" - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-size" - }, - { - "description": "Denies the stat command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-stat" - }, - { - "description": "Denies the truncate command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-truncate" - }, - { - "description": "Denies the unwatch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-unwatch" - }, - { - "description": "Denies the watch command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-watch" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-linux" - }, - { - "description": "This denies read access to the\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.", - "type": "string", - "const": "fs:deny-webview-data-windows" - }, - { - "description": "Denies the write command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write" - }, - { - "description": "Denies the write_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-file" - }, - { - "description": "Denies the write_text_file command without any pre-configured scope.", - "type": "string", - "const": "fs:deny-write-text-file" - }, - { - "description": "This enables all read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-all" - }, - { - "description": "This permission allows recursive read functionality on the application\nspecific base directories. \n", - "type": "string", - "const": "fs:read-app-specific-dirs-recursive" - }, - { - "description": "This enables directory read and file metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-dirs" - }, - { - "description": "This enables file read related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-files" - }, - { - "description": "This enables all index or metadata related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:read-meta" - }, - { - "description": "An empty permission you can use to modify the global scope.", - "type": "string", - "const": "fs:scope" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the application folders.", - "type": "string", - "const": "fs:scope-app" - }, - { - "description": "This scope permits to list all files and folders in the application directories.", - "type": "string", - "const": "fs:scope-app-index" - }, - { - "description": "This scope permits recursive access to the complete application folders, including sub directories and files.", - "type": "string", - "const": "fs:scope-app-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.", - "type": "string", - "const": "fs:scope-appcache" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCACHE`folder.", - "type": "string", - "const": "fs:scope-appcache-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appcache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.", - "type": "string", - "const": "fs:scope-appconfig" - }, - { - "description": "This scope permits to list all files and folders in the `$APPCONFIG`folder.", - "type": "string", - "const": "fs:scope-appconfig-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appconfig-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.", - "type": "string", - "const": "fs:scope-appdata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPDATA`folder.", - "type": "string", - "const": "fs:scope-appdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-appdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.", - "type": "string", - "const": "fs:scope-applocaldata" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOCALDATA`folder.", - "type": "string", - "const": "fs:scope-applocaldata-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applocaldata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.", - "type": "string", - "const": "fs:scope-applog" - }, - { - "description": "This scope permits to list all files and folders in the `$APPLOG`folder.", - "type": "string", - "const": "fs:scope-applog-index" - }, - { - "description": "This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-applog-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.", - "type": "string", - "const": "fs:scope-audio" - }, - { - "description": "This scope permits to list all files and folders in the `$AUDIO`folder.", - "type": "string", - "const": "fs:scope-audio-index" - }, - { - "description": "This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-audio-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CACHE` folder.", - "type": "string", - "const": "fs:scope-cache" - }, - { - "description": "This scope permits to list all files and folders in the `$CACHE`folder.", - "type": "string", - "const": "fs:scope-cache-index" - }, - { - "description": "This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-cache-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.", - "type": "string", - "const": "fs:scope-config" - }, - { - "description": "This scope permits to list all files and folders in the `$CONFIG`folder.", - "type": "string", - "const": "fs:scope-config-index" - }, - { - "description": "This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-config-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DATA` folder.", - "type": "string", - "const": "fs:scope-data" - }, - { - "description": "This scope permits to list all files and folders in the `$DATA`folder.", - "type": "string", - "const": "fs:scope-data-index" - }, - { - "description": "This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-data-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.", - "type": "string", - "const": "fs:scope-desktop" - }, - { - "description": "This scope permits to list all files and folders in the `$DESKTOP`folder.", - "type": "string", - "const": "fs:scope-desktop-index" - }, - { - "description": "This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-desktop-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.", - "type": "string", - "const": "fs:scope-document" - }, - { - "description": "This scope permits to list all files and folders in the `$DOCUMENT`folder.", - "type": "string", - "const": "fs:scope-document-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-document-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.", - "type": "string", - "const": "fs:scope-download" - }, - { - "description": "This scope permits to list all files and folders in the `$DOWNLOAD`folder.", - "type": "string", - "const": "fs:scope-download-index" - }, - { - "description": "This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-download-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$EXE` folder.", - "type": "string", - "const": "fs:scope-exe" - }, - { - "description": "This scope permits to list all files and folders in the `$EXE`folder.", - "type": "string", - "const": "fs:scope-exe-index" - }, - { - "description": "This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-exe-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$FONT` folder.", - "type": "string", - "const": "fs:scope-font" - }, - { - "description": "This scope permits to list all files and folders in the `$FONT`folder.", - "type": "string", - "const": "fs:scope-font-index" - }, - { - "description": "This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-font-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$HOME` folder.", - "type": "string", - "const": "fs:scope-home" - }, - { - "description": "This scope permits to list all files and folders in the `$HOME`folder.", - "type": "string", - "const": "fs:scope-home-index" - }, - { - "description": "This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-home-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.", - "type": "string", - "const": "fs:scope-localdata" - }, - { - "description": "This scope permits to list all files and folders in the `$LOCALDATA`folder.", - "type": "string", - "const": "fs:scope-localdata-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-localdata-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$LOG` folder.", - "type": "string", - "const": "fs:scope-log" - }, - { - "description": "This scope permits to list all files and folders in the `$LOG`folder.", - "type": "string", - "const": "fs:scope-log-index" - }, - { - "description": "This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-log-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.", - "type": "string", - "const": "fs:scope-picture" - }, - { - "description": "This scope permits to list all files and folders in the `$PICTURE`folder.", - "type": "string", - "const": "fs:scope-picture-index" - }, - { - "description": "This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-picture-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.", - "type": "string", - "const": "fs:scope-public" - }, - { - "description": "This scope permits to list all files and folders in the `$PUBLIC`folder.", - "type": "string", - "const": "fs:scope-public-index" - }, - { - "description": "This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-public-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.", - "type": "string", - "const": "fs:scope-resource" - }, - { - "description": "This scope permits to list all files and folders in the `$RESOURCE`folder.", - "type": "string", - "const": "fs:scope-resource-index" - }, - { - "description": "This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-resource-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.", - "type": "string", - "const": "fs:scope-runtime" - }, - { - "description": "This scope permits to list all files and folders in the `$RUNTIME`folder.", - "type": "string", - "const": "fs:scope-runtime-index" - }, - { - "description": "This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-runtime-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMP` folder.", - "type": "string", - "const": "fs:scope-temp" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMP`folder.", - "type": "string", - "const": "fs:scope-temp-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-temp-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.", - "type": "string", - "const": "fs:scope-template" - }, - { - "description": "This scope permits to list all files and folders in the `$TEMPLATE`folder.", - "type": "string", - "const": "fs:scope-template-index" - }, - { - "description": "This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-template-recursive" - }, - { - "description": "This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.", - "type": "string", - "const": "fs:scope-video" - }, - { - "description": "This scope permits to list all files and folders in the `$VIDEO`folder.", - "type": "string", - "const": "fs:scope-video-index" - }, - { - "description": "This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.", - "type": "string", - "const": "fs:scope-video-recursive" - }, - { - "description": "This enables all write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-all" - }, - { - "description": "This enables all file write related commands without any pre-configured accessible paths.", - "type": "string", - "const": "fs:write-files" - }, - { - "description": "Allows the moveWindow and handleIconState APIs", - "type": "string", - "const": "positioner:default" - }, - { - "description": "Enables the move_window command without any pre-configured scope.", - "type": "string", - "const": "positioner:allow-move-window" - }, - { - "description": "Enables the move_window_constrained command without any pre-configured scope.", - "type": "string", - "const": "positioner:allow-move-window-constrained" - }, - { - "description": "Enables the set_tray_icon_state command without any pre-configured scope.", - "type": "string", - "const": "positioner:allow-set-tray-icon-state" - }, - { - "description": "Denies the move_window command without any pre-configured scope.", - "type": "string", - "const": "positioner:deny-move-window" - }, - { - "description": "Denies the move_window_constrained command without any pre-configured scope.", - "type": "string", - "const": "positioner:deny-move-window-constrained" - }, - { - "description": "Denies the set_tray_icon_state command without any pre-configured scope.", - "type": "string", - "const": "positioner:deny-set-tray-icon-state" - }, - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality without any specific\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n", - "type": "string", - "const": "shell:default" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute" - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill" - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open" - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn" - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write" - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute" - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill" - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open" - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn" - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write" - }, - { - "description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n", - "type": "string", - "const": "window-state:default" - }, - { - "description": "Enables the filename command without any pre-configured scope.", - "type": "string", - "const": "window-state:allow-filename" - }, - { - "description": "Enables the restore_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:allow-restore-state" - }, - { - "description": "Enables the save_window_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:allow-save-window-state" - }, - { - "description": "Denies the filename command without any pre-configured scope.", - "type": "string", - "const": "window-state:deny-filename" - }, - { - "description": "Denies the restore_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:deny-restore-state" - }, - { - "description": "Denies the save_window_state command without any pre-configured scope.", - "type": "string", - "const": "window-state:deny-save-window-state" - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - }, - "ShellScopeEntryAllowedArg": { - "description": "A command argument allowed to be executed by the webview API.", - "anyOf": [ - { - "description": "A non-configurable argument that is passed to the command in the order it was specified.", - "type": "string" - }, - { - "description": "A variable that is set while calling the command from the webview API.", - "type": "object", - "required": [ - "validator" - ], - "properties": { - "raw": { - "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", - "default": false, - "type": "boolean" - }, - "validator": { - "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "ShellScopeEntryAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", - "anyOf": [ - { - "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", - "type": "boolean" - }, - { - "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellScopeEntryAllowedArg" - } - } - ] - } - } -} \ No newline at end of file diff --git a/src-tauri/gen/schemas/plugin-manifests.json b/src-tauri/gen/schemas/plugin-manifests.json deleted file mode 100644 index 8af6336fc..000000000 --- a/src-tauri/gen/schemas/plugin-manifests.json +++ /dev/null @@ -1 +0,0 @@ -{"app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version"]},"permissions":{"allow-app-hide":{"version":null,"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]},"scope":{}},"allow-app-show":{"version":null,"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]},"scope":{}},"allow-name":{"version":null,"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]},"scope":{}},"allow-tauri-version":{"version":null,"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]},"scope":{}},"allow-version":{"version":null,"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]},"scope":{}},"deny-app-hide":{"version":null,"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]},"scope":{}},"deny-app-show":{"version":null,"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]},"scope":{}},"deny-name":{"version":null,"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]},"scope":{}},"deny-tauri-version":{"version":null,"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]},"scope":{}},"deny-version":{"version":null,"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":null,"permissions":{"allow-ask":{"version":null,"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]},"scope":{}},"allow-confirm":{"version":null,"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]},"scope":{}},"allow-message":{"version":null,"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]},"scope":{}},"allow-open":{"version":null,"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]},"scope":{}},"allow-save":{"version":null,"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]},"scope":{}},"deny-ask":{"version":null,"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]},"scope":{}},"deny-confirm":{"version":null,"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]},"scope":{}},"deny-message":{"version":null,"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]},"scope":{}},"deny-open":{"version":null,"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]},"scope":{}},"deny-save":{"version":null,"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"version":null,"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]},"scope":{}},"allow-emit-to":{"version":null,"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]},"scope":{}},"allow-listen":{"version":null,"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]},"scope":{}},"allow-unlisten":{"version":null,"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]},"scope":{}},"deny-emit":{"version":null,"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]},"scope":{}},"deny-emit-to":{"version":null,"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]},"scope":{}},"deny-listen":{"version":null,"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]},"scope":{}},"deny-unlisten":{"version":null,"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"fs":{"default_permission":{"identifier":"default","description":"# Tauri `fs` default permissions\n\nThis configuration file defines the default permissions granted\nto the filesystem.\n\n### Granted Permissions\n\nThis default permission set enables all read-related commands and\nallows access to the `$APP` folder and sub directories created in it.\nThe location of the `$APP` folder depends on the operating system,\nwhere the application is run.\n\nIn general the `$APP` folder needs to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\n### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n","permissions":["read-all","scope-app-recursive","deny-default"]},"permissions":{"allow-copy-file":{"version":null,"identifier":"allow-copy-file","description":"Enables the copy_file command without any pre-configured scope.","commands":{"allow":["copy_file"],"deny":[]},"scope":{}},"allow-create":{"version":null,"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]},"scope":{}},"allow-exists":{"version":null,"identifier":"allow-exists","description":"Enables the exists command without any pre-configured scope.","commands":{"allow":["exists"],"deny":[]},"scope":{}},"allow-fstat":{"version":null,"identifier":"allow-fstat","description":"Enables the fstat command without any pre-configured scope.","commands":{"allow":["fstat"],"deny":[]},"scope":{}},"allow-ftruncate":{"version":null,"identifier":"allow-ftruncate","description":"Enables the ftruncate command without any pre-configured scope.","commands":{"allow":["ftruncate"],"deny":[]},"scope":{}},"allow-lstat":{"version":null,"identifier":"allow-lstat","description":"Enables the lstat command without any pre-configured scope.","commands":{"allow":["lstat"],"deny":[]},"scope":{}},"allow-mkdir":{"version":null,"identifier":"allow-mkdir","description":"Enables the mkdir command without any pre-configured scope.","commands":{"allow":["mkdir"],"deny":[]},"scope":{}},"allow-open":{"version":null,"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]},"scope":{}},"allow-read":{"version":null,"identifier":"allow-read","description":"Enables the read command without any pre-configured scope.","commands":{"allow":["read"],"deny":[]},"scope":{}},"allow-read-dir":{"version":null,"identifier":"allow-read-dir","description":"Enables the read_dir command without any pre-configured scope.","commands":{"allow":["read_dir"],"deny":[]},"scope":{}},"allow-read-file":{"version":null,"identifier":"allow-read-file","description":"Enables the read_file command without any pre-configured scope.","commands":{"allow":["read_file"],"deny":[]},"scope":{}},"allow-read-text-file":{"version":null,"identifier":"allow-read-text-file","description":"Enables the read_text_file command without any pre-configured scope.","commands":{"allow":["read_text_file"],"deny":[]},"scope":{}},"allow-read-text-file-lines":{"version":null,"identifier":"allow-read-text-file-lines","description":"Enables the read_text_file_lines command without any pre-configured scope.","commands":{"allow":["read_text_file_lines"],"deny":[]},"scope":{}},"allow-read-text-file-lines-next":{"version":null,"identifier":"allow-read-text-file-lines-next","description":"Enables the read_text_file_lines_next command without any pre-configured scope.","commands":{"allow":["read_text_file_lines_next"],"deny":[]},"scope":{}},"allow-remove":{"version":null,"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]},"scope":{}},"allow-rename":{"version":null,"identifier":"allow-rename","description":"Enables the rename command without any pre-configured scope.","commands":{"allow":["rename"],"deny":[]},"scope":{}},"allow-seek":{"version":null,"identifier":"allow-seek","description":"Enables the seek command without any pre-configured scope.","commands":{"allow":["seek"],"deny":[]},"scope":{}},"allow-stat":{"version":null,"identifier":"allow-stat","description":"Enables the stat command without any pre-configured scope.","commands":{"allow":["stat"],"deny":[]},"scope":{}},"allow-truncate":{"version":null,"identifier":"allow-truncate","description":"Enables the truncate command without any pre-configured scope.","commands":{"allow":["truncate"],"deny":[]},"scope":{}},"allow-unwatch":{"version":null,"identifier":"allow-unwatch","description":"Enables the unwatch command without any pre-configured scope.","commands":{"allow":["unwatch"],"deny":[]},"scope":{}},"allow-watch":{"version":null,"identifier":"allow-watch","description":"Enables the watch command without any pre-configured scope.","commands":{"allow":["watch"],"deny":[]},"scope":{}},"allow-write":{"version":null,"identifier":"allow-write","description":"Enables the write command without any pre-configured scope.","commands":{"allow":["write"],"deny":[]},"scope":{}},"allow-write-file":{"version":null,"identifier":"allow-write-file","description":"Enables the write_file command without any pre-configured scope.","commands":{"allow":["write_file"],"deny":[]},"scope":{}},"allow-write-text-file":{"version":null,"identifier":"allow-write-text-file","description":"Enables the write_text_file command without any pre-configured scope.","commands":{"allow":["write_text_file"],"deny":[]},"scope":{}},"deny-copy-file":{"version":null,"identifier":"deny-copy-file","description":"Denies the copy_file command without any pre-configured scope.","commands":{"allow":[],"deny":["copy_file"]},"scope":{}},"deny-create":{"version":null,"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]},"scope":{}},"deny-exists":{"version":null,"identifier":"deny-exists","description":"Denies the exists command without any pre-configured scope.","commands":{"allow":[],"deny":["exists"]},"scope":{}},"deny-fstat":{"version":null,"identifier":"deny-fstat","description":"Denies the fstat command without any pre-configured scope.","commands":{"allow":[],"deny":["fstat"]},"scope":{}},"deny-ftruncate":{"version":null,"identifier":"deny-ftruncate","description":"Denies the ftruncate command without any pre-configured scope.","commands":{"allow":[],"deny":["ftruncate"]},"scope":{}},"deny-lstat":{"version":null,"identifier":"deny-lstat","description":"Denies the lstat command without any pre-configured scope.","commands":{"allow":[],"deny":["lstat"]},"scope":{}},"deny-mkdir":{"version":null,"identifier":"deny-mkdir","description":"Denies the mkdir command without any pre-configured scope.","commands":{"allow":[],"deny":["mkdir"]},"scope":{}},"deny-open":{"version":null,"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]},"scope":{}},"deny-read":{"version":null,"identifier":"deny-read","description":"Denies the read command without any pre-configured scope.","commands":{"allow":[],"deny":["read"]},"scope":{}},"deny-read-dir":{"version":null,"identifier":"deny-read-dir","description":"Denies the read_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["read_dir"]},"scope":{}},"deny-read-file":{"version":null,"identifier":"deny-read-file","description":"Denies the read_file command without any pre-configured scope.","commands":{"allow":[],"deny":["read_file"]},"scope":{}},"deny-read-text-file":{"version":null,"identifier":"deny-read-text-file","description":"Denies the read_text_file command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text_file"]},"scope":{}},"deny-read-text-file-lines":{"version":null,"identifier":"deny-read-text-file-lines","description":"Denies the read_text_file_lines command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text_file_lines"]},"scope":{}},"deny-read-text-file-lines-next":{"version":null,"identifier":"deny-read-text-file-lines-next","description":"Denies the read_text_file_lines_next command without any pre-configured scope.","commands":{"allow":[],"deny":["read_text_file_lines_next"]},"scope":{}},"deny-remove":{"version":null,"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]},"scope":{}},"deny-rename":{"version":null,"identifier":"deny-rename","description":"Denies the rename command without any pre-configured scope.","commands":{"allow":[],"deny":["rename"]},"scope":{}},"deny-seek":{"version":null,"identifier":"deny-seek","description":"Denies the seek command without any pre-configured scope.","commands":{"allow":[],"deny":["seek"]},"scope":{}},"deny-stat":{"version":null,"identifier":"deny-stat","description":"Denies the stat command without any pre-configured scope.","commands":{"allow":[],"deny":["stat"]},"scope":{}},"deny-truncate":{"version":null,"identifier":"deny-truncate","description":"Denies the truncate command without any pre-configured scope.","commands":{"allow":[],"deny":["truncate"]},"scope":{}},"deny-unwatch":{"version":null,"identifier":"deny-unwatch","description":"Denies the unwatch command without any pre-configured scope.","commands":{"allow":[],"deny":["unwatch"]},"scope":{}},"deny-watch":{"version":null,"identifier":"deny-watch","description":"Denies the watch command without any pre-configured scope.","commands":{"allow":[],"deny":["watch"]},"scope":{}},"deny-webview-data-linux":{"version":null,"identifier":"deny-webview-data-linux","description":"This denies read access to the\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.","commands":{"allow":[],"deny":[]},"scope":{}},"deny-webview-data-windows":{"version":null,"identifier":"deny-webview-data-windows","description":"This denies read access to the\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\nAllowing access can lead to sensitive information disclosure and should be well considered.","commands":{"allow":[],"deny":[]},"scope":{}},"deny-write":{"version":null,"identifier":"deny-write","description":"Denies the write command without any pre-configured scope.","commands":{"allow":[],"deny":["write"]},"scope":{}},"deny-write-file":{"version":null,"identifier":"deny-write-file","description":"Denies the write_file command without any pre-configured scope.","commands":{"allow":[],"deny":["write_file"]},"scope":{}},"deny-write-text-file":{"version":null,"identifier":"deny-write-text-file","description":"Denies the write_text_file command without any pre-configured scope.","commands":{"allow":[],"deny":["write_text_file"]},"scope":{}},"read-all":{"version":null,"identifier":"read-all","description":"This enables all read related commands without any pre-configured accessible paths.","commands":{"allow":["read_dir","read_file","read","open","read_text_file","read_text_file_lines","read_text_file_lines_next","seek","stat","lstat","fstat","exists","watch","unwatch"],"deny":[]},"scope":{}},"read-dirs":{"version":null,"identifier":"read-dirs","description":"This enables directory read and file metadata related commands without any pre-configured accessible paths.","commands":{"allow":["read_dir","stat","lstat","fstat","exists"],"deny":[]},"scope":{}},"read-files":{"version":null,"identifier":"read-files","description":"This enables file read related commands without any pre-configured accessible paths.","commands":{"allow":["read_file","read","open","read_text_file","read_text_file_lines","read_text_file_lines_next","seek","stat","lstat","fstat","exists"],"deny":[]},"scope":{}},"read-meta":{"version":null,"identifier":"read-meta","description":"This enables all index or metadata related commands without any pre-configured accessible paths.","commands":{"allow":["read_dir","stat","lstat","fstat","exists"],"deny":[]},"scope":{}},"scope":{"version":null,"identifier":"scope","description":"An empty permission you can use to modify the global scope.","commands":{"allow":[],"deny":[]},"scope":{}},"scope-app":{"version":null,"identifier":"scope-app","description":"This scope permits access to all files and list content of top level directories in the `$APP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APP/*"}]}},"scope-app-index":{"version":null,"identifier":"scope-app-index","description":"This scope permits to list all files and folders in the `$APP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APP/"}]}},"scope-app-recursive":{"version":null,"identifier":"scope-app-recursive","description":"This scope recursive access to the complete `$APP` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APP/**"}]}},"scope-appcache":{"version":null,"identifier":"scope-appcache","description":"This scope permits access to all files and list content of top level directories in the `$APPCACHE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCACHE/*"}]}},"scope-appcache-index":{"version":null,"identifier":"scope-appcache-index","description":"This scope permits to list all files and folders in the `$APPCACHE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCACHE/"}]}},"scope-appcache-recursive":{"version":null,"identifier":"scope-appcache-recursive","description":"This scope recursive access to the complete `$APPCACHE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCACHE/**"}]}},"scope-appconfig":{"version":null,"identifier":"scope-appconfig","description":"This scope permits access to all files and list content of top level directories in the `$APPCONFIG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG/*"}]}},"scope-appconfig-index":{"version":null,"identifier":"scope-appconfig-index","description":"This scope permits to list all files and folders in the `$APPCONFIG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG/"}]}},"scope-appconfig-recursive":{"version":null,"identifier":"scope-appconfig-recursive","description":"This scope recursive access to the complete `$APPCONFIG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPCONFIG/**"}]}},"scope-appdata":{"version":null,"identifier":"scope-appdata","description":"This scope permits access to all files and list content of top level directories in the `$APPDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPDATA/*"}]}},"scope-appdata-index":{"version":null,"identifier":"scope-appdata-index","description":"This scope permits to list all files and folders in the `$APPDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPDATA/"}]}},"scope-appdata-recursive":{"version":null,"identifier":"scope-appdata-recursive","description":"This scope recursive access to the complete `$APPDATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPDATA/**"}]}},"scope-applocaldata":{"version":null,"identifier":"scope-applocaldata","description":"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOCALDATA/*"}]}},"scope-applocaldata-index":{"version":null,"identifier":"scope-applocaldata-index","description":"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOCALDATA/"}]}},"scope-applocaldata-recursive":{"version":null,"identifier":"scope-applocaldata-recursive","description":"This scope recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOCALDATA/**"}]}},"scope-applog":{"version":null,"identifier":"scope-applog","description":"This scope permits access to all files and list content of top level directories in the `$APPLOG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOG/*"}]}},"scope-applog-index":{"version":null,"identifier":"scope-applog-index","description":"This scope permits to list all files and folders in the `$APPLOG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOG/"}]}},"scope-applog-recursive":{"version":null,"identifier":"scope-applog-recursive","description":"This scope recursive access to the complete `$APPLOG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$APPLOG/**"}]}},"scope-audio":{"version":null,"identifier":"scope-audio","description":"This scope permits access to all files and list content of top level directories in the `$AUDIO`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$AUDIO/*"}]}},"scope-audio-index":{"version":null,"identifier":"scope-audio-index","description":"This scope permits to list all files and folders in the `$AUDIO`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$AUDIO/"}]}},"scope-audio-recursive":{"version":null,"identifier":"scope-audio-recursive","description":"This scope recursive access to the complete `$AUDIO` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$AUDIO/**"}]}},"scope-cache":{"version":null,"identifier":"scope-cache","description":"This scope permits access to all files and list content of top level directories in the `$CACHE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CACHE/*"}]}},"scope-cache-index":{"version":null,"identifier":"scope-cache-index","description":"This scope permits to list all files and folders in the `$CACHE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CACHE/"}]}},"scope-cache-recursive":{"version":null,"identifier":"scope-cache-recursive","description":"This scope recursive access to the complete `$CACHE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CACHE/**"}]}},"scope-config":{"version":null,"identifier":"scope-config","description":"This scope permits access to all files and list content of top level directories in the `$CONFIG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CONFIG/*"}]}},"scope-config-index":{"version":null,"identifier":"scope-config-index","description":"This scope permits to list all files and folders in the `$CONFIG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CONFIG/"}]}},"scope-config-recursive":{"version":null,"identifier":"scope-config-recursive","description":"This scope recursive access to the complete `$CONFIG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$CONFIG/**"}]}},"scope-data":{"version":null,"identifier":"scope-data","description":"This scope permits access to all files and list content of top level directories in the `$DATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DATA/*"}]}},"scope-data-index":{"version":null,"identifier":"scope-data-index","description":"This scope permits to list all files and folders in the `$DATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DATA/"}]}},"scope-data-recursive":{"version":null,"identifier":"scope-data-recursive","description":"This scope recursive access to the complete `$DATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DATA/**"}]}},"scope-desktop":{"version":null,"identifier":"scope-desktop","description":"This scope permits access to all files and list content of top level directories in the `$DESKTOP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DESKTOP/*"}]}},"scope-desktop-index":{"version":null,"identifier":"scope-desktop-index","description":"This scope permits to list all files and folders in the `$DESKTOP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DESKTOP/"}]}},"scope-desktop-recursive":{"version":null,"identifier":"scope-desktop-recursive","description":"This scope recursive access to the complete `$DESKTOP` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DESKTOP/**"}]}},"scope-document":{"version":null,"identifier":"scope-document","description":"This scope permits access to all files and list content of top level directories in the `$DOCUMENT`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOCUMENT/*"}]}},"scope-document-index":{"version":null,"identifier":"scope-document-index","description":"This scope permits to list all files and folders in the `$DOCUMENT`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOCUMENT/"}]}},"scope-document-recursive":{"version":null,"identifier":"scope-document-recursive","description":"This scope recursive access to the complete `$DOCUMENT` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOCUMENT/**"}]}},"scope-download":{"version":null,"identifier":"scope-download","description":"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOWNLOAD/*"}]}},"scope-download-index":{"version":null,"identifier":"scope-download-index","description":"This scope permits to list all files and folders in the `$DOWNLOAD`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOWNLOAD/"}]}},"scope-download-recursive":{"version":null,"identifier":"scope-download-recursive","description":"This scope recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$DOWNLOAD/**"}]}},"scope-exe":{"version":null,"identifier":"scope-exe","description":"This scope permits access to all files and list content of top level directories in the `$EXE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$EXE/*"}]}},"scope-exe-index":{"version":null,"identifier":"scope-exe-index","description":"This scope permits to list all files and folders in the `$EXE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$EXE/"}]}},"scope-exe-recursive":{"version":null,"identifier":"scope-exe-recursive","description":"This scope recursive access to the complete `$EXE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$EXE/**"}]}},"scope-font":{"version":null,"identifier":"scope-font","description":"This scope permits access to all files and list content of top level directories in the `$FONT`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$FONT/*"}]}},"scope-font-index":{"version":null,"identifier":"scope-font-index","description":"This scope permits to list all files and folders in the `$FONT`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$FONT/"}]}},"scope-font-recursive":{"version":null,"identifier":"scope-font-recursive","description":"This scope recursive access to the complete `$FONT` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$FONT/**"}]}},"scope-home":{"version":null,"identifier":"scope-home","description":"This scope permits access to all files and list content of top level directories in the `$HOME`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$HOME/*"}]}},"scope-home-index":{"version":null,"identifier":"scope-home-index","description":"This scope permits to list all files and folders in the `$HOME`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$HOME/"}]}},"scope-home-recursive":{"version":null,"identifier":"scope-home-recursive","description":"This scope recursive access to the complete `$HOME` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$HOME/**"}]}},"scope-localdata":{"version":null,"identifier":"scope-localdata","description":"This scope permits access to all files and list content of top level directories in the `$LOCALDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOCALDATA/*"}]}},"scope-localdata-index":{"version":null,"identifier":"scope-localdata-index","description":"This scope permits to list all files and folders in the `$LOCALDATA`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOCALDATA/"}]}},"scope-localdata-recursive":{"version":null,"identifier":"scope-localdata-recursive","description":"This scope recursive access to the complete `$LOCALDATA` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOCALDATA/**"}]}},"scope-log":{"version":null,"identifier":"scope-log","description":"This scope permits access to all files and list content of top level directories in the `$LOG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOG/*"}]}},"scope-log-index":{"version":null,"identifier":"scope-log-index","description":"This scope permits to list all files and folders in the `$LOG`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOG/"}]}},"scope-log-recursive":{"version":null,"identifier":"scope-log-recursive","description":"This scope recursive access to the complete `$LOG` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$LOG/**"}]}},"scope-picture":{"version":null,"identifier":"scope-picture","description":"This scope permits access to all files and list content of top level directories in the `$PICTURE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PICTURE/*"}]}},"scope-picture-index":{"version":null,"identifier":"scope-picture-index","description":"This scope permits to list all files and folders in the `$PICTURE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PICTURE/"}]}},"scope-picture-recursive":{"version":null,"identifier":"scope-picture-recursive","description":"This scope recursive access to the complete `$PICTURE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PICTURE/**"}]}},"scope-public":{"version":null,"identifier":"scope-public","description":"This scope permits access to all files and list content of top level directories in the `$PUBLIC`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PUBLIC/*"}]}},"scope-public-index":{"version":null,"identifier":"scope-public-index","description":"This scope permits to list all files and folders in the `$PUBLIC`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PUBLIC/"}]}},"scope-public-recursive":{"version":null,"identifier":"scope-public-recursive","description":"This scope recursive access to the complete `$PUBLIC` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$PUBLIC/**"}]}},"scope-resource":{"version":null,"identifier":"scope-resource","description":"This scope permits access to all files and list content of top level directories in the `$RESOURCE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RESOURCE/*"}]}},"scope-resource-index":{"version":null,"identifier":"scope-resource-index","description":"This scope permits to list all files and folders in the `$RESOURCE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RESOURCE/"}]}},"scope-resource-recursive":{"version":null,"identifier":"scope-resource-recursive","description":"This scope recursive access to the complete `$RESOURCE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RESOURCE/**"}]}},"scope-runtime":{"version":null,"identifier":"scope-runtime","description":"This scope permits access to all files and list content of top level directories in the `$RUNTIME`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RUNTIME/*"}]}},"scope-runtime-index":{"version":null,"identifier":"scope-runtime-index","description":"This scope permits to list all files and folders in the `$RUNTIME`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RUNTIME/"}]}},"scope-runtime-recursive":{"version":null,"identifier":"scope-runtime-recursive","description":"This scope recursive access to the complete `$RUNTIME` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$RUNTIME/**"}]}},"scope-temp":{"version":null,"identifier":"scope-temp","description":"This scope permits access to all files and list content of top level directories in the `$TEMP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMP/*"}]}},"scope-temp-index":{"version":null,"identifier":"scope-temp-index","description":"This scope permits to list all files and folders in the `$TEMP`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMP/"}]}},"scope-temp-recursive":{"version":null,"identifier":"scope-temp-recursive","description":"This scope recursive access to the complete `$TEMP` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMP/**"}]}},"scope-template":{"version":null,"identifier":"scope-template","description":"This scope permits access to all files and list content of top level directories in the `$TEMPLATE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMPLATE/*"}]}},"scope-template-index":{"version":null,"identifier":"scope-template-index","description":"This scope permits to list all files and folders in the `$TEMPLATE`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMPLATE/"}]}},"scope-template-recursive":{"version":null,"identifier":"scope-template-recursive","description":"This scope recursive access to the complete `$TEMPLATE` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$TEMPLATE/**"}]}},"scope-video":{"version":null,"identifier":"scope-video","description":"This scope permits access to all files and list content of top level directories in the `$VIDEO`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$VIDEO/*"}]}},"scope-video-index":{"version":null,"identifier":"scope-video-index","description":"This scope permits to list all files and folders in the `$VIDEO`folder.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$VIDEO/"}]}},"scope-video-recursive":{"version":null,"identifier":"scope-video-recursive","description":"This scope recursive access to the complete `$VIDEO` folder, including sub directories and files.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"path":"$VIDEO/**"}]}},"write-all":{"version":null,"identifier":"write-all","description":"This enables all write related commands without any pre-configured accessible paths.","commands":{"allow":["mkdir","create","copy_file","remove","rename","truncate","ftruncate","write","write_file","write_text_file"],"deny":[]},"scope":{}},"write-files":{"version":null,"identifier":"write-files","description":"This enables all file write related commands without any pre-configured accessible paths.","commands":{"allow":["create","copy_file","remove","rename","truncate","ftruncate","write","write_file","write_text_file"],"deny":[]},"scope":{}}},"permission_sets":{"allow-app-meta":{"identifier":"allow-app-meta","description":"This allows read access to metadata of the `$APP` folder, including file listing and statistics.","permissions":["read-meta","scope-app-index"]},"allow-app-meta-recursive":{"identifier":"allow-app-meta-recursive","description":"This allows read access to metadata of the `$APP` folder, including file listing and statistics.","permissions":["read-meta","scope-app-recursive"]},"allow-app-read":{"identifier":"allow-app-read","description":"This allows non-recursive read access to the `$APP` folder.","permissions":["read-all","scope-app"]},"allow-app-read-recursive":{"identifier":"allow-app-read-recursive","description":"This allows full recursive read access to the complete `$APP` folder, files and subdirectories.","permissions":["read-all","scope-app-recursive"]},"allow-app-write":{"identifier":"allow-app-write","description":"This allows non-recursive write access to the `$APP` folder.","permissions":["write-all","scope-app"]},"allow-app-write-recursive":{"identifier":"allow-app-write-recursive","description":"This allows full recusrive write access to the complete `$APP` folder, files and subdirectories.","permissions":["write-all","scope-app-recursive"]},"allow-appcache-meta":{"identifier":"allow-appcache-meta","description":"This allows read access to metadata of the `$APPCACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-appcache-index"]},"allow-appcache-meta-recursive":{"identifier":"allow-appcache-meta-recursive","description":"This allows read access to metadata of the `$APPCACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-appcache-recursive"]},"allow-appcache-read":{"identifier":"allow-appcache-read","description":"This allows non-recursive read access to the `$APPCACHE` folder.","permissions":["read-all","scope-appcache"]},"allow-appcache-read-recursive":{"identifier":"allow-appcache-read-recursive","description":"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.","permissions":["read-all","scope-appcache-recursive"]},"allow-appcache-write":{"identifier":"allow-appcache-write","description":"This allows non-recursive write access to the `$APPCACHE` folder.","permissions":["write-all","scope-appcache"]},"allow-appcache-write-recursive":{"identifier":"allow-appcache-write-recursive","description":"This allows full recusrive write access to the complete `$APPCACHE` folder, files and subdirectories.","permissions":["write-all","scope-appcache-recursive"]},"allow-appconfig-meta":{"identifier":"allow-appconfig-meta","description":"This allows read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-appconfig-index"]},"allow-appconfig-meta-recursive":{"identifier":"allow-appconfig-meta-recursive","description":"This allows read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-appconfig-recursive"]},"allow-appconfig-read":{"identifier":"allow-appconfig-read","description":"This allows non-recursive read access to the `$APPCONFIG` folder.","permissions":["read-all","scope-appconfig"]},"allow-appconfig-read-recursive":{"identifier":"allow-appconfig-read-recursive","description":"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.","permissions":["read-all","scope-appconfig-recursive"]},"allow-appconfig-write":{"identifier":"allow-appconfig-write","description":"This allows non-recursive write access to the `$APPCONFIG` folder.","permissions":["write-all","scope-appconfig"]},"allow-appconfig-write-recursive":{"identifier":"allow-appconfig-write-recursive","description":"This allows full recusrive write access to the complete `$APPCONFIG` folder, files and subdirectories.","permissions":["write-all","scope-appconfig-recursive"]},"allow-appdata-meta":{"identifier":"allow-appdata-meta","description":"This allows read access to metadata of the `$APPDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-appdata-index"]},"allow-appdata-meta-recursive":{"identifier":"allow-appdata-meta-recursive","description":"This allows read access to metadata of the `$APPDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-appdata-recursive"]},"allow-appdata-read":{"identifier":"allow-appdata-read","description":"This allows non-recursive read access to the `$APPDATA` folder.","permissions":["read-all","scope-appdata"]},"allow-appdata-read-recursive":{"identifier":"allow-appdata-read-recursive","description":"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.","permissions":["read-all","scope-appdata-recursive"]},"allow-appdata-write":{"identifier":"allow-appdata-write","description":"This allows non-recursive write access to the `$APPDATA` folder.","permissions":["write-all","scope-appdata"]},"allow-appdata-write-recursive":{"identifier":"allow-appdata-write-recursive","description":"This allows full recusrive write access to the complete `$APPDATA` folder, files and subdirectories.","permissions":["write-all","scope-appdata-recursive"]},"allow-applocaldata-meta":{"identifier":"allow-applocaldata-meta","description":"This allows read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-applocaldata-index"]},"allow-applocaldata-meta-recursive":{"identifier":"allow-applocaldata-meta-recursive","description":"This allows read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-applocaldata-recursive"]},"allow-applocaldata-read":{"identifier":"allow-applocaldata-read","description":"This allows non-recursive read access to the `$APPLOCALDATA` folder.","permissions":["read-all","scope-applocaldata"]},"allow-applocaldata-read-recursive":{"identifier":"allow-applocaldata-read-recursive","description":"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.","permissions":["read-all","scope-applocaldata-recursive"]},"allow-applocaldata-write":{"identifier":"allow-applocaldata-write","description":"This allows non-recursive write access to the `$APPLOCALDATA` folder.","permissions":["write-all","scope-applocaldata"]},"allow-applocaldata-write-recursive":{"identifier":"allow-applocaldata-write-recursive","description":"This allows full recusrive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.","permissions":["write-all","scope-applocaldata-recursive"]},"allow-applog-meta":{"identifier":"allow-applog-meta","description":"This allows read access to metadata of the `$APPLOG` folder, including file listing and statistics.","permissions":["read-meta","scope-applog-index"]},"allow-applog-meta-recursive":{"identifier":"allow-applog-meta-recursive","description":"This allows read access to metadata of the `$APPLOG` folder, including file listing and statistics.","permissions":["read-meta","scope-applog-recursive"]},"allow-applog-read":{"identifier":"allow-applog-read","description":"This allows non-recursive read access to the `$APPLOG` folder.","permissions":["read-all","scope-applog"]},"allow-applog-read-recursive":{"identifier":"allow-applog-read-recursive","description":"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.","permissions":["read-all","scope-applog-recursive"]},"allow-applog-write":{"identifier":"allow-applog-write","description":"This allows non-recursive write access to the `$APPLOG` folder.","permissions":["write-all","scope-applog"]},"allow-applog-write-recursive":{"identifier":"allow-applog-write-recursive","description":"This allows full recusrive write access to the complete `$APPLOG` folder, files and subdirectories.","permissions":["write-all","scope-applog-recursive"]},"allow-audio-meta":{"identifier":"allow-audio-meta","description":"This allows read access to metadata of the `$AUDIO` folder, including file listing and statistics.","permissions":["read-meta","scope-audio-index"]},"allow-audio-meta-recursive":{"identifier":"allow-audio-meta-recursive","description":"This allows read access to metadata of the `$AUDIO` folder, including file listing and statistics.","permissions":["read-meta","scope-audio-recursive"]},"allow-audio-read":{"identifier":"allow-audio-read","description":"This allows non-recursive read access to the `$AUDIO` folder.","permissions":["read-all","scope-audio"]},"allow-audio-read-recursive":{"identifier":"allow-audio-read-recursive","description":"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.","permissions":["read-all","scope-audio-recursive"]},"allow-audio-write":{"identifier":"allow-audio-write","description":"This allows non-recursive write access to the `$AUDIO` folder.","permissions":["write-all","scope-audio"]},"allow-audio-write-recursive":{"identifier":"allow-audio-write-recursive","description":"This allows full recusrive write access to the complete `$AUDIO` folder, files and subdirectories.","permissions":["write-all","scope-audio-recursive"]},"allow-cache-meta":{"identifier":"allow-cache-meta","description":"This allows read access to metadata of the `$CACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-cache-index"]},"allow-cache-meta-recursive":{"identifier":"allow-cache-meta-recursive","description":"This allows read access to metadata of the `$CACHE` folder, including file listing and statistics.","permissions":["read-meta","scope-cache-recursive"]},"allow-cache-read":{"identifier":"allow-cache-read","description":"This allows non-recursive read access to the `$CACHE` folder.","permissions":["read-all","scope-cache"]},"allow-cache-read-recursive":{"identifier":"allow-cache-read-recursive","description":"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.","permissions":["read-all","scope-cache-recursive"]},"allow-cache-write":{"identifier":"allow-cache-write","description":"This allows non-recursive write access to the `$CACHE` folder.","permissions":["write-all","scope-cache"]},"allow-cache-write-recursive":{"identifier":"allow-cache-write-recursive","description":"This allows full recusrive write access to the complete `$CACHE` folder, files and subdirectories.","permissions":["write-all","scope-cache-recursive"]},"allow-config-meta":{"identifier":"allow-config-meta","description":"This allows read access to metadata of the `$CONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-config-index"]},"allow-config-meta-recursive":{"identifier":"allow-config-meta-recursive","description":"This allows read access to metadata of the `$CONFIG` folder, including file listing and statistics.","permissions":["read-meta","scope-config-recursive"]},"allow-config-read":{"identifier":"allow-config-read","description":"This allows non-recursive read access to the `$CONFIG` folder.","permissions":["read-all","scope-config"]},"allow-config-read-recursive":{"identifier":"allow-config-read-recursive","description":"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.","permissions":["read-all","scope-config-recursive"]},"allow-config-write":{"identifier":"allow-config-write","description":"This allows non-recursive write access to the `$CONFIG` folder.","permissions":["write-all","scope-config"]},"allow-config-write-recursive":{"identifier":"allow-config-write-recursive","description":"This allows full recusrive write access to the complete `$CONFIG` folder, files and subdirectories.","permissions":["write-all","scope-config-recursive"]},"allow-data-meta":{"identifier":"allow-data-meta","description":"This allows read access to metadata of the `$DATA` folder, including file listing and statistics.","permissions":["read-meta","scope-data-index"]},"allow-data-meta-recursive":{"identifier":"allow-data-meta-recursive","description":"This allows read access to metadata of the `$DATA` folder, including file listing and statistics.","permissions":["read-meta","scope-data-recursive"]},"allow-data-read":{"identifier":"allow-data-read","description":"This allows non-recursive read access to the `$DATA` folder.","permissions":["read-all","scope-data"]},"allow-data-read-recursive":{"identifier":"allow-data-read-recursive","description":"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.","permissions":["read-all","scope-data-recursive"]},"allow-data-write":{"identifier":"allow-data-write","description":"This allows non-recursive write access to the `$DATA` folder.","permissions":["write-all","scope-data"]},"allow-data-write-recursive":{"identifier":"allow-data-write-recursive","description":"This allows full recusrive write access to the complete `$DATA` folder, files and subdirectories.","permissions":["write-all","scope-data-recursive"]},"allow-desktop-meta":{"identifier":"allow-desktop-meta","description":"This allows read access to metadata of the `$DESKTOP` folder, including file listing and statistics.","permissions":["read-meta","scope-desktop-index"]},"allow-desktop-meta-recursive":{"identifier":"allow-desktop-meta-recursive","description":"This allows read access to metadata of the `$DESKTOP` folder, including file listing and statistics.","permissions":["read-meta","scope-desktop-recursive"]},"allow-desktop-read":{"identifier":"allow-desktop-read","description":"This allows non-recursive read access to the `$DESKTOP` folder.","permissions":["read-all","scope-desktop"]},"allow-desktop-read-recursive":{"identifier":"allow-desktop-read-recursive","description":"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.","permissions":["read-all","scope-desktop-recursive"]},"allow-desktop-write":{"identifier":"allow-desktop-write","description":"This allows non-recursive write access to the `$DESKTOP` folder.","permissions":["write-all","scope-desktop"]},"allow-desktop-write-recursive":{"identifier":"allow-desktop-write-recursive","description":"This allows full recusrive write access to the complete `$DESKTOP` folder, files and subdirectories.","permissions":["write-all","scope-desktop-recursive"]},"allow-document-meta":{"identifier":"allow-document-meta","description":"This allows read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.","permissions":["read-meta","scope-document-index"]},"allow-document-meta-recursive":{"identifier":"allow-document-meta-recursive","description":"This allows read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.","permissions":["read-meta","scope-document-recursive"]},"allow-document-read":{"identifier":"allow-document-read","description":"This allows non-recursive read access to the `$DOCUMENT` folder.","permissions":["read-all","scope-document"]},"allow-document-read-recursive":{"identifier":"allow-document-read-recursive","description":"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.","permissions":["read-all","scope-document-recursive"]},"allow-document-write":{"identifier":"allow-document-write","description":"This allows non-recursive write access to the `$DOCUMENT` folder.","permissions":["write-all","scope-document"]},"allow-document-write-recursive":{"identifier":"allow-document-write-recursive","description":"This allows full recusrive write access to the complete `$DOCUMENT` folder, files and subdirectories.","permissions":["write-all","scope-document-recursive"]},"allow-download-meta":{"identifier":"allow-download-meta","description":"This allows read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.","permissions":["read-meta","scope-download-index"]},"allow-download-meta-recursive":{"identifier":"allow-download-meta-recursive","description":"This allows read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.","permissions":["read-meta","scope-download-recursive"]},"allow-download-read":{"identifier":"allow-download-read","description":"This allows non-recursive read access to the `$DOWNLOAD` folder.","permissions":["read-all","scope-download"]},"allow-download-read-recursive":{"identifier":"allow-download-read-recursive","description":"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.","permissions":["read-all","scope-download-recursive"]},"allow-download-write":{"identifier":"allow-download-write","description":"This allows non-recursive write access to the `$DOWNLOAD` folder.","permissions":["write-all","scope-download"]},"allow-download-write-recursive":{"identifier":"allow-download-write-recursive","description":"This allows full recusrive write access to the complete `$DOWNLOAD` folder, files and subdirectories.","permissions":["write-all","scope-download-recursive"]},"allow-exe-meta":{"identifier":"allow-exe-meta","description":"This allows read access to metadata of the `$EXE` folder, including file listing and statistics.","permissions":["read-meta","scope-exe-index"]},"allow-exe-meta-recursive":{"identifier":"allow-exe-meta-recursive","description":"This allows read access to metadata of the `$EXE` folder, including file listing and statistics.","permissions":["read-meta","scope-exe-recursive"]},"allow-exe-read":{"identifier":"allow-exe-read","description":"This allows non-recursive read access to the `$EXE` folder.","permissions":["read-all","scope-exe"]},"allow-exe-read-recursive":{"identifier":"allow-exe-read-recursive","description":"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.","permissions":["read-all","scope-exe-recursive"]},"allow-exe-write":{"identifier":"allow-exe-write","description":"This allows non-recursive write access to the `$EXE` folder.","permissions":["write-all","scope-exe"]},"allow-exe-write-recursive":{"identifier":"allow-exe-write-recursive","description":"This allows full recusrive write access to the complete `$EXE` folder, files and subdirectories.","permissions":["write-all","scope-exe-recursive"]},"allow-font-meta":{"identifier":"allow-font-meta","description":"This allows read access to metadata of the `$FONT` folder, including file listing and statistics.","permissions":["read-meta","scope-font-index"]},"allow-font-meta-recursive":{"identifier":"allow-font-meta-recursive","description":"This allows read access to metadata of the `$FONT` folder, including file listing and statistics.","permissions":["read-meta","scope-font-recursive"]},"allow-font-read":{"identifier":"allow-font-read","description":"This allows non-recursive read access to the `$FONT` folder.","permissions":["read-all","scope-font"]},"allow-font-read-recursive":{"identifier":"allow-font-read-recursive","description":"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.","permissions":["read-all","scope-font-recursive"]},"allow-font-write":{"identifier":"allow-font-write","description":"This allows non-recursive write access to the `$FONT` folder.","permissions":["write-all","scope-font"]},"allow-font-write-recursive":{"identifier":"allow-font-write-recursive","description":"This allows full recusrive write access to the complete `$FONT` folder, files and subdirectories.","permissions":["write-all","scope-font-recursive"]},"allow-home-meta":{"identifier":"allow-home-meta","description":"This allows read access to metadata of the `$HOME` folder, including file listing and statistics.","permissions":["read-meta","scope-home-index"]},"allow-home-meta-recursive":{"identifier":"allow-home-meta-recursive","description":"This allows read access to metadata of the `$HOME` folder, including file listing and statistics.","permissions":["read-meta","scope-home-recursive"]},"allow-home-read":{"identifier":"allow-home-read","description":"This allows non-recursive read access to the `$HOME` folder.","permissions":["read-all","scope-home"]},"allow-home-read-recursive":{"identifier":"allow-home-read-recursive","description":"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.","permissions":["read-all","scope-home-recursive"]},"allow-home-write":{"identifier":"allow-home-write","description":"This allows non-recursive write access to the `$HOME` folder.","permissions":["write-all","scope-home"]},"allow-home-write-recursive":{"identifier":"allow-home-write-recursive","description":"This allows full recusrive write access to the complete `$HOME` folder, files and subdirectories.","permissions":["write-all","scope-home-recursive"]},"allow-localdata-meta":{"identifier":"allow-localdata-meta","description":"This allows read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-localdata-index"]},"allow-localdata-meta-recursive":{"identifier":"allow-localdata-meta-recursive","description":"This allows read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.","permissions":["read-meta","scope-localdata-recursive"]},"allow-localdata-read":{"identifier":"allow-localdata-read","description":"This allows non-recursive read access to the `$LOCALDATA` folder.","permissions":["read-all","scope-localdata"]},"allow-localdata-read-recursive":{"identifier":"allow-localdata-read-recursive","description":"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.","permissions":["read-all","scope-localdata-recursive"]},"allow-localdata-write":{"identifier":"allow-localdata-write","description":"This allows non-recursive write access to the `$LOCALDATA` folder.","permissions":["write-all","scope-localdata"]},"allow-localdata-write-recursive":{"identifier":"allow-localdata-write-recursive","description":"This allows full recusrive write access to the complete `$LOCALDATA` folder, files and subdirectories.","permissions":["write-all","scope-localdata-recursive"]},"allow-log-meta":{"identifier":"allow-log-meta","description":"This allows read access to metadata of the `$LOG` folder, including file listing and statistics.","permissions":["read-meta","scope-log-index"]},"allow-log-meta-recursive":{"identifier":"allow-log-meta-recursive","description":"This allows read access to metadata of the `$LOG` folder, including file listing and statistics.","permissions":["read-meta","scope-log-recursive"]},"allow-log-read":{"identifier":"allow-log-read","description":"This allows non-recursive read access to the `$LOG` folder.","permissions":["read-all","scope-log"]},"allow-log-read-recursive":{"identifier":"allow-log-read-recursive","description":"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.","permissions":["read-all","scope-log-recursive"]},"allow-log-write":{"identifier":"allow-log-write","description":"This allows non-recursive write access to the `$LOG` folder.","permissions":["write-all","scope-log"]},"allow-log-write-recursive":{"identifier":"allow-log-write-recursive","description":"This allows full recusrive write access to the complete `$LOG` folder, files and subdirectories.","permissions":["write-all","scope-log-recursive"]},"allow-picture-meta":{"identifier":"allow-picture-meta","description":"This allows read access to metadata of the `$PICTURE` folder, including file listing and statistics.","permissions":["read-meta","scope-picture-index"]},"allow-picture-meta-recursive":{"identifier":"allow-picture-meta-recursive","description":"This allows read access to metadata of the `$PICTURE` folder, including file listing and statistics.","permissions":["read-meta","scope-picture-recursive"]},"allow-picture-read":{"identifier":"allow-picture-read","description":"This allows non-recursive read access to the `$PICTURE` folder.","permissions":["read-all","scope-picture"]},"allow-picture-read-recursive":{"identifier":"allow-picture-read-recursive","description":"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.","permissions":["read-all","scope-picture-recursive"]},"allow-picture-write":{"identifier":"allow-picture-write","description":"This allows non-recursive write access to the `$PICTURE` folder.","permissions":["write-all","scope-picture"]},"allow-picture-write-recursive":{"identifier":"allow-picture-write-recursive","description":"This allows full recusrive write access to the complete `$PICTURE` folder, files and subdirectories.","permissions":["write-all","scope-picture-recursive"]},"allow-public-meta":{"identifier":"allow-public-meta","description":"This allows read access to metadata of the `$PUBLIC` folder, including file listing and statistics.","permissions":["read-meta","scope-public-index"]},"allow-public-meta-recursive":{"identifier":"allow-public-meta-recursive","description":"This allows read access to metadata of the `$PUBLIC` folder, including file listing and statistics.","permissions":["read-meta","scope-public-recursive"]},"allow-public-read":{"identifier":"allow-public-read","description":"This allows non-recursive read access to the `$PUBLIC` folder.","permissions":["read-all","scope-public"]},"allow-public-read-recursive":{"identifier":"allow-public-read-recursive","description":"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.","permissions":["read-all","scope-public-recursive"]},"allow-public-write":{"identifier":"allow-public-write","description":"This allows non-recursive write access to the `$PUBLIC` folder.","permissions":["write-all","scope-public"]},"allow-public-write-recursive":{"identifier":"allow-public-write-recursive","description":"This allows full recusrive write access to the complete `$PUBLIC` folder, files and subdirectories.","permissions":["write-all","scope-public-recursive"]},"allow-resource-meta":{"identifier":"allow-resource-meta","description":"This allows read access to metadata of the `$RESOURCE` folder, including file listing and statistics.","permissions":["read-meta","scope-resource-index"]},"allow-resource-meta-recursive":{"identifier":"allow-resource-meta-recursive","description":"This allows read access to metadata of the `$RESOURCE` folder, including file listing and statistics.","permissions":["read-meta","scope-resource-recursive"]},"allow-resource-read":{"identifier":"allow-resource-read","description":"This allows non-recursive read access to the `$RESOURCE` folder.","permissions":["read-all","scope-resource"]},"allow-resource-read-recursive":{"identifier":"allow-resource-read-recursive","description":"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.","permissions":["read-all","scope-resource-recursive"]},"allow-resource-write":{"identifier":"allow-resource-write","description":"This allows non-recursive write access to the `$RESOURCE` folder.","permissions":["write-all","scope-resource"]},"allow-resource-write-recursive":{"identifier":"allow-resource-write-recursive","description":"This allows full recusrive write access to the complete `$RESOURCE` folder, files and subdirectories.","permissions":["write-all","scope-resource-recursive"]},"allow-runtime-meta":{"identifier":"allow-runtime-meta","description":"This allows read access to metadata of the `$RUNTIME` folder, including file listing and statistics.","permissions":["read-meta","scope-runtime-index"]},"allow-runtime-meta-recursive":{"identifier":"allow-runtime-meta-recursive","description":"This allows read access to metadata of the `$RUNTIME` folder, including file listing and statistics.","permissions":["read-meta","scope-runtime-recursive"]},"allow-runtime-read":{"identifier":"allow-runtime-read","description":"This allows non-recursive read access to the `$RUNTIME` folder.","permissions":["read-all","scope-runtime"]},"allow-runtime-read-recursive":{"identifier":"allow-runtime-read-recursive","description":"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.","permissions":["read-all","scope-runtime-recursive"]},"allow-runtime-write":{"identifier":"allow-runtime-write","description":"This allows non-recursive write access to the `$RUNTIME` folder.","permissions":["write-all","scope-runtime"]},"allow-runtime-write-recursive":{"identifier":"allow-runtime-write-recursive","description":"This allows full recusrive write access to the complete `$RUNTIME` folder, files and subdirectories.","permissions":["write-all","scope-runtime-recursive"]},"allow-temp-meta":{"identifier":"allow-temp-meta","description":"This allows read access to metadata of the `$TEMP` folder, including file listing and statistics.","permissions":["read-meta","scope-temp-index"]},"allow-temp-meta-recursive":{"identifier":"allow-temp-meta-recursive","description":"This allows read access to metadata of the `$TEMP` folder, including file listing and statistics.","permissions":["read-meta","scope-temp-recursive"]},"allow-temp-read":{"identifier":"allow-temp-read","description":"This allows non-recursive read access to the `$TEMP` folder.","permissions":["read-all","scope-temp"]},"allow-temp-read-recursive":{"identifier":"allow-temp-read-recursive","description":"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.","permissions":["read-all","scope-temp-recursive"]},"allow-temp-write":{"identifier":"allow-temp-write","description":"This allows non-recursive write access to the `$TEMP` folder.","permissions":["write-all","scope-temp"]},"allow-temp-write-recursive":{"identifier":"allow-temp-write-recursive","description":"This allows full recusrive write access to the complete `$TEMP` folder, files and subdirectories.","permissions":["write-all","scope-temp-recursive"]},"allow-template-meta":{"identifier":"allow-template-meta","description":"This allows read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.","permissions":["read-meta","scope-template-index"]},"allow-template-meta-recursive":{"identifier":"allow-template-meta-recursive","description":"This allows read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.","permissions":["read-meta","scope-template-recursive"]},"allow-template-read":{"identifier":"allow-template-read","description":"This allows non-recursive read access to the `$TEMPLATE` folder.","permissions":["read-all","scope-template"]},"allow-template-read-recursive":{"identifier":"allow-template-read-recursive","description":"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.","permissions":["read-all","scope-template-recursive"]},"allow-template-write":{"identifier":"allow-template-write","description":"This allows non-recursive write access to the `$TEMPLATE` folder.","permissions":["write-all","scope-template"]},"allow-template-write-recursive":{"identifier":"allow-template-write-recursive","description":"This allows full recusrive write access to the complete `$TEMPLATE` folder, files and subdirectories.","permissions":["write-all","scope-template-recursive"]},"allow-video-meta":{"identifier":"allow-video-meta","description":"This allows read access to metadata of the `$VIDEO` folder, including file listing and statistics.","permissions":["read-meta","scope-video-index"]},"allow-video-meta-recursive":{"identifier":"allow-video-meta-recursive","description":"This allows read access to metadata of the `$VIDEO` folder, including file listing and statistics.","permissions":["read-meta","scope-video-recursive"]},"allow-video-read":{"identifier":"allow-video-read","description":"This allows non-recursive read access to the `$VIDEO` folder.","permissions":["read-all","scope-video"]},"allow-video-read-recursive":{"identifier":"allow-video-read-recursive","description":"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.","permissions":["read-all","scope-video-recursive"]},"allow-video-write":{"identifier":"allow-video-write","description":"This allows non-recursive write access to the `$VIDEO` folder.","permissions":["write-all","scope-video"]},"allow-video-write-recursive":{"identifier":"allow-video-write-recursive","description":"This allows full recusrive write access to the complete `$VIDEO` folder, files and subdirectories.","permissions":["write-all","scope-video-recursive"]},"deny-default":{"identifier":"deny-default","description":"This denies access to dangerous Tauri relevant files and folders by default.","permissions":["deny-webview-data-linux","deny-webview-data-windows"]}},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","properties":{"path":{"type":"string"}},"required":["path"],"title":"Entry","type":"object"}},"menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":[]},"permissions":{"allow-append":{"version":null,"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]},"scope":{}},"allow-create-default":{"version":null,"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]},"scope":{}},"allow-get":{"version":null,"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]},"scope":{}},"allow-insert":{"version":null,"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]},"scope":{}},"allow-is-checked":{"version":null,"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]},"scope":{}},"allow-is-enabled":{"version":null,"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]},"scope":{}},"allow-items":{"version":null,"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]},"scope":{}},"allow-new":{"version":null,"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]},"scope":{}},"allow-popup":{"version":null,"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]},"scope":{}},"allow-prepend":{"version":null,"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]},"scope":{}},"allow-remove":{"version":null,"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]},"scope":{}},"allow-remove-at":{"version":null,"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]},"scope":{}},"allow-set-accelerator":{"version":null,"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]},"scope":{}},"allow-set-as-app-menu":{"version":null,"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]},"scope":{}},"allow-set-as-help-menu-for-nsapp":{"version":null,"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]},"scope":{}},"allow-set-as-window-menu":{"version":null,"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]},"scope":{}},"allow-set-as-windows-menu-for-nsapp":{"version":null,"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]},"scope":{}},"allow-set-checked":{"version":null,"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]},"scope":{}},"allow-set-enabled":{"version":null,"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]},"scope":{}},"allow-set-icon":{"version":null,"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]},"scope":{}},"allow-set-text":{"version":null,"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]},"scope":{}},"allow-text":{"version":null,"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]},"scope":{}},"deny-append":{"version":null,"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]},"scope":{}},"deny-create-default":{"version":null,"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]},"scope":{}},"deny-get":{"version":null,"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]},"scope":{}},"deny-insert":{"version":null,"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]},"scope":{}},"deny-is-checked":{"version":null,"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]},"scope":{}},"deny-is-enabled":{"version":null,"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]},"scope":{}},"deny-items":{"version":null,"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]},"scope":{}},"deny-new":{"version":null,"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]},"scope":{}},"deny-popup":{"version":null,"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]},"scope":{}},"deny-prepend":{"version":null,"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]},"scope":{}},"deny-remove":{"version":null,"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]},"scope":{}},"deny-remove-at":{"version":null,"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]},"scope":{}},"deny-set-accelerator":{"version":null,"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]},"scope":{}},"deny-set-as-app-menu":{"version":null,"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]},"scope":{}},"deny-set-as-help-menu-for-nsapp":{"version":null,"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]},"scope":{}},"deny-set-as-window-menu":{"version":null,"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]},"scope":{}},"deny-set-as-windows-menu-for-nsapp":{"version":null,"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]},"scope":{}},"deny-set-checked":{"version":null,"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]},"scope":{}},"deny-set-enabled":{"version":null,"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]},"scope":{}},"deny-set-icon":{"version":null,"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]},"scope":{}},"deny-set-text":{"version":null,"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]},"scope":{}},"deny-text":{"version":null,"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"version":null,"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]},"scope":{}},"allow-dirname":{"version":null,"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]},"scope":{}},"allow-extname":{"version":null,"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]},"scope":{}},"allow-is-absolute":{"version":null,"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]},"scope":{}},"allow-join":{"version":null,"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]},"scope":{}},"allow-normalize":{"version":null,"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]},"scope":{}},"allow-resolve":{"version":null,"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]},"scope":{}},"allow-resolve-directory":{"version":null,"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]},"scope":{}},"deny-basename":{"version":null,"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]},"scope":{}},"deny-dirname":{"version":null,"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]},"scope":{}},"deny-extname":{"version":null,"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]},"scope":{}},"deny-is-absolute":{"version":null,"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]},"scope":{}},"deny-join":{"version":null,"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]},"scope":{}},"deny-normalize":{"version":null,"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]},"scope":{}},"deny-resolve":{"version":null,"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]},"scope":{}},"deny-resolve-directory":{"version":null,"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"positioner":{"default_permission":{"identifier":"default","description":"Allows the move_window command","permissions":["allow-move-window"]},"permissions":{"allow-move-window":{"version":null,"identifier":"allow-move-window","description":"Enables the move_window command without any pre-configured scope.","commands":{"allow":["move_window"],"deny":[]},"scope":{}},"deny-move-window":{"version":null,"identifier":"deny-move-window","description":"Denies the move_window command without any pre-configured scope.","commands":{"allow":[],"deny":["move_window"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-close"]},"permissions":{"allow-close":{"version":null,"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]},"scope":{}},"deny-close":{"version":null,"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":null,"permissions":{"allow-execute":{"version":null,"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]},"scope":{}},"allow-kill":{"version":null,"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]},"scope":{}},"allow-open":{"version":null,"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]},"scope":{}},"allow-stdin-write":{"version":null,"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]},"scope":{}},"deny-execute":{"version":null,"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]},"scope":{}},"deny-kill":{"version":null,"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]},"scope":{}},"deny-open":{"version":null,"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]},"scope":{}},"deny-stdin-write":{"version":null,"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]},"scope":{}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"ShellAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"A command allowed to be executed by the webview API.","properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellAllowedArgs"}],"description":"The allowed arguments for the command execution."},"command":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["args","command","name","sidecar"],"title":"Entry","type":"object"}},"tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":[]},"permissions":{"allow-new":{"version":null,"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]},"scope":{}},"allow-set-icon":{"version":null,"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]},"scope":{}},"allow-set-icon-as-template":{"version":null,"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]},"scope":{}},"allow-set-menu":{"version":null,"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]},"scope":{}},"allow-set-show-menu-on-left-click":{"version":null,"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]},"scope":{}},"allow-set-temp-dir-path":{"version":null,"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]},"scope":{}},"allow-set-title":{"version":null,"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]},"scope":{}},"allow-set-tooltip":{"version":null,"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]},"scope":{}},"allow-set-visible":{"version":null,"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]},"scope":{}},"deny-new":{"version":null,"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]},"scope":{}},"deny-set-icon":{"version":null,"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]},"scope":{}},"deny-set-icon-as-template":{"version":null,"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]},"scope":{}},"deny-set-menu":{"version":null,"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]},"scope":{}},"deny-set-show-menu-on-left-click":{"version":null,"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]},"scope":{}},"deny-set-temp-dir-path":{"version":null,"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]},"scope":{}},"deny-set-title":{"version":null,"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]},"scope":{}},"deny-set-tooltip":{"version":null,"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]},"scope":{}},"deny-set-visible":{"version":null,"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-create-webview":{"version":null,"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]},"scope":{}},"allow-create-webview-window":{"version":null,"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]},"scope":{}},"allow-internal-toggle-devtools":{"version":null,"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]},"scope":{}},"allow-print":{"version":null,"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]},"scope":{}},"allow-set-webview-focus":{"version":null,"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]},"scope":{}},"allow-set-webview-position":{"version":null,"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]},"scope":{}},"allow-set-webview-size":{"version":null,"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]},"scope":{}},"allow-webview-close":{"version":null,"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]},"scope":{}},"allow-webview-position":{"version":null,"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]},"scope":{}},"allow-webview-size":{"version":null,"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]},"scope":{}},"deny-create-webview":{"version":null,"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]},"scope":{}},"deny-create-webview-window":{"version":null,"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]},"scope":{}},"deny-internal-toggle-devtools":{"version":null,"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]},"scope":{}},"deny-print":{"version":null,"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]},"scope":{}},"deny-set-webview-focus":{"version":null,"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]},"scope":{}},"deny-set-webview-position":{"version":null,"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]},"scope":{}},"deny-set-webview-size":{"version":null,"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]},"scope":{}},"deny-webview-close":{"version":null,"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]},"scope":{}},"deny-webview-position":{"version":null,"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]},"scope":{}},"deny-webview-size":{"version":null,"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-title","allow-current-monitor","allow-primary-monitor","allow-available-monitors","allow-theme","allow-internal-toggle-maximize","allow-internal-on-mousemove","allow-internal-on-mousedown"]},"permissions":{"allow-available-monitors":{"version":null,"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]},"scope":{}},"allow-center":{"version":null,"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]},"scope":{}},"allow-close":{"version":null,"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]},"scope":{}},"allow-create":{"version":null,"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]},"scope":{}},"allow-current-monitor":{"version":null,"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]},"scope":{}},"allow-destroy":{"version":null,"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]},"scope":{}},"allow-hide":{"version":null,"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]},"scope":{}},"allow-inner-position":{"version":null,"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]},"scope":{}},"allow-inner-size":{"version":null,"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]},"scope":{}},"allow-internal-on-mousedown":{"version":null,"identifier":"allow-internal-on-mousedown","description":"Enables the internal_on_mousedown command without any pre-configured scope.","commands":{"allow":["internal_on_mousedown"],"deny":[]},"scope":{}},"allow-internal-on-mousemove":{"version":null,"identifier":"allow-internal-on-mousemove","description":"Enables the internal_on_mousemove command without any pre-configured scope.","commands":{"allow":["internal_on_mousemove"],"deny":[]},"scope":{}},"allow-internal-toggle-maximize":{"version":null,"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]},"scope":{}},"allow-is-closable":{"version":null,"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]},"scope":{}},"allow-is-decorated":{"version":null,"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]},"scope":{}},"allow-is-focused":{"version":null,"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]},"scope":{}},"allow-is-fullscreen":{"version":null,"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]},"scope":{}},"allow-is-maximizable":{"version":null,"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]},"scope":{}},"allow-is-maximized":{"version":null,"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]},"scope":{}},"allow-is-minimizable":{"version":null,"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]},"scope":{}},"allow-is-minimized":{"version":null,"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]},"scope":{}},"allow-is-resizable":{"version":null,"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]},"scope":{}},"allow-is-visible":{"version":null,"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]},"scope":{}},"allow-maximize":{"version":null,"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]},"scope":{}},"allow-minimize":{"version":null,"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]},"scope":{}},"allow-outer-position":{"version":null,"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]},"scope":{}},"allow-outer-size":{"version":null,"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]},"scope":{}},"allow-primary-monitor":{"version":null,"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]},"scope":{}},"allow-request-user-attention":{"version":null,"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]},"scope":{}},"allow-scale-factor":{"version":null,"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]},"scope":{}},"allow-set-always-on-bottom":{"version":null,"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]},"scope":{}},"allow-set-always-on-top":{"version":null,"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]},"scope":{}},"allow-set-closable":{"version":null,"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]},"scope":{}},"allow-set-content-protected":{"version":null,"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]},"scope":{}},"allow-set-cursor-grab":{"version":null,"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]},"scope":{}},"allow-set-cursor-icon":{"version":null,"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]},"scope":{}},"allow-set-cursor-position":{"version":null,"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]},"scope":{}},"allow-set-cursor-visible":{"version":null,"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]},"scope":{}},"allow-set-decorations":{"version":null,"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]},"scope":{}},"allow-set-effects":{"version":null,"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]},"scope":{}},"allow-set-focus":{"version":null,"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]},"scope":{}},"allow-set-fullscreen":{"version":null,"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]},"scope":{}},"allow-set-icon":{"version":null,"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]},"scope":{}},"allow-set-ignore-cursor-events":{"version":null,"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]},"scope":{}},"allow-set-max-size":{"version":null,"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]},"scope":{}},"allow-set-maximizable":{"version":null,"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]},"scope":{}},"allow-set-min-size":{"version":null,"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]},"scope":{}},"allow-set-minimizable":{"version":null,"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]},"scope":{}},"allow-set-position":{"version":null,"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]},"scope":{}},"allow-set-progress-bar":{"version":null,"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]},"scope":{}},"allow-set-resizable":{"version":null,"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]},"scope":{}},"allow-set-shadow":{"version":null,"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]},"scope":{}},"allow-set-size":{"version":null,"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]},"scope":{}},"allow-set-skip-taskbar":{"version":null,"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]},"scope":{}},"allow-set-title":{"version":null,"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]},"scope":{}},"allow-set-visible-on-all-workspaces":{"version":null,"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]},"scope":{}},"allow-show":{"version":null,"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]},"scope":{}},"allow-start-dragging":{"version":null,"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]},"scope":{}},"allow-theme":{"version":null,"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]},"scope":{}},"allow-title":{"version":null,"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]},"scope":{}},"allow-toggle-maximize":{"version":null,"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]},"scope":{}},"allow-unmaximize":{"version":null,"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]},"scope":{}},"allow-unminimize":{"version":null,"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]},"scope":{}},"deny-available-monitors":{"version":null,"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]},"scope":{}},"deny-center":{"version":null,"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]},"scope":{}},"deny-close":{"version":null,"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]},"scope":{}},"deny-create":{"version":null,"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]},"scope":{}},"deny-current-monitor":{"version":null,"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]},"scope":{}},"deny-destroy":{"version":null,"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]},"scope":{}},"deny-hide":{"version":null,"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]},"scope":{}},"deny-inner-position":{"version":null,"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]},"scope":{}},"deny-inner-size":{"version":null,"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]},"scope":{}},"deny-internal-on-mousedown":{"version":null,"identifier":"deny-internal-on-mousedown","description":"Denies the internal_on_mousedown command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_on_mousedown"]},"scope":{}},"deny-internal-on-mousemove":{"version":null,"identifier":"deny-internal-on-mousemove","description":"Denies the internal_on_mousemove command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_on_mousemove"]},"scope":{}},"deny-internal-toggle-maximize":{"version":null,"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]},"scope":{}},"deny-is-closable":{"version":null,"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]},"scope":{}},"deny-is-decorated":{"version":null,"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]},"scope":{}},"deny-is-focused":{"version":null,"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]},"scope":{}},"deny-is-fullscreen":{"version":null,"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]},"scope":{}},"deny-is-maximizable":{"version":null,"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]},"scope":{}},"deny-is-maximized":{"version":null,"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]},"scope":{}},"deny-is-minimizable":{"version":null,"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]},"scope":{}},"deny-is-minimized":{"version":null,"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]},"scope":{}},"deny-is-resizable":{"version":null,"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]},"scope":{}},"deny-is-visible":{"version":null,"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]},"scope":{}},"deny-maximize":{"version":null,"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]},"scope":{}},"deny-minimize":{"version":null,"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]},"scope":{}},"deny-outer-position":{"version":null,"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]},"scope":{}},"deny-outer-size":{"version":null,"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]},"scope":{}},"deny-primary-monitor":{"version":null,"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]},"scope":{}},"deny-request-user-attention":{"version":null,"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]},"scope":{}},"deny-scale-factor":{"version":null,"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]},"scope":{}},"deny-set-always-on-bottom":{"version":null,"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]},"scope":{}},"deny-set-always-on-top":{"version":null,"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]},"scope":{}},"deny-set-closable":{"version":null,"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]},"scope":{}},"deny-set-content-protected":{"version":null,"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]},"scope":{}},"deny-set-cursor-grab":{"version":null,"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]},"scope":{}},"deny-set-cursor-icon":{"version":null,"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]},"scope":{}},"deny-set-cursor-position":{"version":null,"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]},"scope":{}},"deny-set-cursor-visible":{"version":null,"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]},"scope":{}},"deny-set-decorations":{"version":null,"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]},"scope":{}},"deny-set-effects":{"version":null,"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]},"scope":{}},"deny-set-focus":{"version":null,"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]},"scope":{}},"deny-set-fullscreen":{"version":null,"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]},"scope":{}},"deny-set-icon":{"version":null,"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]},"scope":{}},"deny-set-ignore-cursor-events":{"version":null,"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]},"scope":{}},"deny-set-max-size":{"version":null,"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]},"scope":{}},"deny-set-maximizable":{"version":null,"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]},"scope":{}},"deny-set-min-size":{"version":null,"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]},"scope":{}},"deny-set-minimizable":{"version":null,"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]},"scope":{}},"deny-set-position":{"version":null,"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]},"scope":{}},"deny-set-progress-bar":{"version":null,"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]},"scope":{}},"deny-set-resizable":{"version":null,"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]},"scope":{}},"deny-set-shadow":{"version":null,"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]},"scope":{}},"deny-set-size":{"version":null,"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]},"scope":{}},"deny-set-skip-taskbar":{"version":null,"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]},"scope":{}},"deny-set-title":{"version":null,"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]},"scope":{}},"deny-set-visible-on-all-workspaces":{"version":null,"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]},"scope":{}},"deny-show":{"version":null,"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]},"scope":{}},"deny-start-dragging":{"version":null,"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]},"scope":{}},"deny-theme":{"version":null,"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]},"scope":{}},"deny-title":{"version":null,"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]},"scope":{}},"deny-toggle-maximize":{"version":null,"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]},"scope":{}},"deny-unmaximize":{"version":null,"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]},"scope":{}},"deny-unminimize":{"version":null,"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null},"window-state":{"default_permission":null,"permissions":{"allow-restore-window-state":{"version":null,"identifier":"allow-restore-window-state","description":"Enables the restore_window_state command without any pre-configured scope.","commands":{"allow":["restore_window_state"],"deny":[]},"scope":{}},"allow-save-window-state":{"version":null,"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]},"scope":{}},"deny-restore-window-state":{"version":null,"identifier":"deny-restore-window-state","description":"Denies the restore_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_window_state"]},"scope":{}},"deny-save-window-state":{"version":null,"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]},"scope":{}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns deleted file mode 100644 index 6ef70f3ad..000000000 Binary files a/src-tauri/icons/icon.icns and /dev/null differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico deleted file mode 100644 index 2171bb312..000000000 Binary files a/src-tauri/icons/icon.ico and /dev/null differ diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs deleted file mode 100644 index 34962a1b1..000000000 --- a/src-tauri/src/main.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use tauri::{webview::Url, AppHandle, WebviewBuilder, WebviewUrl, WindowBuilder}; -use tauri::{Manager, Runtime}; -use tauri_plugin_positioner::{Position, WindowExt}; -use tauri_plugin_window_state::{StateFlags, WindowExt as WindowStateExt}; - -// 🚀 PRODUCTION - -#[cfg(not(dev))] -fn main() { - let port = 44999; - let url = format!("http://localhost:{}", port).parse().unwrap(); - - let builder = default_builder().plugin(tauri_plugin_localhost::Builder::new(port).build()); - - setup(url, builder) -} - -// 💣 DEVELOPMENT - -#[cfg(dev)] -fn main() { - let port = 8000; - let url = format!("http://localhost:{}", port).parse().unwrap(); - - let builder = default_builder(); - - setup(url, builder) -} - -// BUILDER - -fn default_builder() -> tauri::Builder { - tauri::Builder::default() - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_window_state::Builder::default().build()) -} - -// WINDOWS - -fn build_window(app: &AppHandle, url: Url) { - let monitor = app.primary_monitor().unwrap(); - - let height; - let width; - - match monitor { - Some(m) => { - height = (m.size().height as f64 / m.scale_factor()) - 80.0; - width = (m.size().width as f64 / m.scale_factor()) - 40.0; - } - - None => { - height = 675.0; - width = 1080.0; - } - } - - let mut window_builder = WindowBuilder::new(app, "main") - .title("Diffuse") - .theme(None) - .inner_size(width, height); - - window_builder = title_styles(window_builder); - - let window = window_builder.build().unwrap(); - window.move_window(Position::Center).unwrap(); - window.restore_state(StateFlags::all()).unwrap(); - - let webview_builder = WebviewBuilder::new("main", WebviewUrl::External(url)) - .auto_resize() - .enable_clipboard_access() - .user_agent("Chrome"); - - window - .add_child( - webview_builder, - tauri::LogicalPosition::new(0, 0), - window.inner_size().unwrap(), - ) - .unwrap(); - - window.set_resizable(true).unwrap(); -} - -#[cfg(target_os = "macos")] -fn title_styles>(builder: WindowBuilder) -> WindowBuilder { - return builder - .title_bar_style(tauri::TitleBarStyle::Overlay) - .hidden_title(true); -} - -#[cfg(not(target_os = "macos"))] -fn title_styles>(builder: WindowBuilder) -> WindowBuilder { - return builder; -} - -// SETUP - -fn setup(url: Url, builder: tauri::Builder) { - builder - .setup(move |app| { - build_window(app.handle(), url); - Ok(()) - }) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json deleted file mode 100644 index c4006b07e..000000000 --- a/src-tauri/tauri.conf.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "app": { - "security": { - "csp": null - }, - "windows": [], - "withGlobalTauri": true - }, - "build": { - "beforeBuildCommand": "", - "beforeDevCommand": "", - "devUrl": "http://localhost:8000", - "frontendDist": "../dist" - }, - "bundle": { - "active": true, - "category": "Music", - "icon": [ - "icons/icon.png", - "icons/icon.icns", - "icons/icon.ico" - ], - "targets": "all" - }, - "identifier": "com.icidasset.diffuse", - "plugins": {}, - "productName": "Diffuse", - "version": "3.5.0" -} \ No newline at end of file diff --git a/src/Core/Brain.elm b/src/Core/Brain.elm deleted file mode 100644 index 081e1d0be..000000000 --- a/src/Core/Brain.elm +++ /dev/null @@ -1,273 +0,0 @@ -module Brain exposing (main) - -import Alien -import Brain.Common.State as Common -import Brain.Other.State as Other -import Brain.Ports as Ports -import Brain.Sources.Processing.State as Processing -import Brain.Sources.Processing.Types as Processing -import Brain.Tracks.State as Tracks -import Brain.Types exposing (..) -import Brain.User.State as User -import Brain.User.Types as User -import Debouncer.Basic as Debouncer -import Json.Decode as Json -import Return -import Return.Ext as Return -import Sources.Processing as Processing -import Task -import Time -import Time.Ext as Time -import Url -import User.Layer as User - - - --- 🧠 - - -main : Program Flags Model Msg -main = - Platform.worker - { init = init - , update = update - , subscriptions = subscriptions - } - - - --- 🌳 - - -init : Flags -> ( Model, Cmd Msg ) -init flags = - let - hypDebouncer = - 2.5 - |> Debouncer.fromSeconds - |> Debouncer.debounce - |> Debouncer.accumulateWith Debouncer.allInputs - |> Debouncer.toDebouncer - - initialUrl = - flags.initialUrl - |> Url.fromString - |> Maybe.withDefault - { protocol = Url.Http - , host = "" - , port_ = Nothing - , path = "" - , query = Nothing - , fragment = Nothing - } - in - ( ----------------------------------------- - -- Initial model - ----------------------------------------- - { currentTime = Time.default - , hypaethralDebouncer = hypDebouncer - , hypaethralRetrieval = Nothing - , hypaethralStorage = [] - , hypaethralUserData = User.emptyHypaethralData - , origin = "ORIGIN_UNKNOWN" - , processingStatus = Processing.NotProcessing - , userSyncMethod = Nothing - } - ----------------------------------------- - -- Initial command - ----------------------------------------- - , Cmd.batch - [ Task.perform SetCurrentTime Time.now - , User.initialCommand initialUrl - ] - ) - - - --- 📣 - - -update : Msg -> Manager -update msg = - case msg of - Bypass -> - Return.singleton - - Cmd a -> - Return.communicate a - - ----------------------------------------- - -- Tracks - ----------------------------------------- - DownloadTracks a -> - Tracks.download a - - GotSearchResults a -> - Tracks.gotSearchResults a - - MakeArtworkTrackUrls a -> - Tracks.makeArtworkTrackUrls a - - RemoveTracksBySourceId a -> - Tracks.removeBySourceId a - - RemoveTracksFromCache a -> - Tracks.removeFromCache a - - ReplaceTrackTags a -> - Tracks.replaceTags a - - Search a -> - Tracks.search a - - StoreTracksInCache a -> - Tracks.storeInCache a - - SyncTrackTags a -> - Tracks.syncTrackTags a - - UpdateSearchIndex a -> - Tracks.updateSearchIndex a - - ----------------------------------------- - -- 🦉 Nested - ----------------------------------------- - ProcessingMsg a -> - Processing.update a - - UserMsg a -> - User.update a - - ----------------------------------------- - -- 📭 Other - ----------------------------------------- - RefreshedAccessToken a -> - Other.refreshedAccessToken a - - SetCurrentTime a -> - Other.setCurrentTime a - - ToCache a -> - Other.toCache a - - - --- 📰 - - -subscriptions : Model -> Sub Msg -subscriptions _ = - Sub.batch - [ Ports.fromAlien alien - , Ports.makeArtworkTrackUrls MakeArtworkTrackUrls - , Ports.refreshedAccessToken RefreshedAccessToken - , Ports.receiveSearchResults GotSearchResults - , Ports.receiveTags (ProcessingMsg << Processing.TagsStep) - , Ports.replaceTags ReplaceTrackTags - - -- - , Time.every (60 * 1000) SetCurrentTime - ] - - - --- 👽 - - -alien : Alien.Event -> Msg -alien event = - case ( event.error, Alien.tagFromString event.tag ) of - ( Nothing, Just tag ) -> - translateAlienData tag event.data - - ( Just err, Just tag ) -> - translateAlienError tag event.data err - - _ -> - Bypass - - -translateAlienData : Alien.Tag -> Json.Value -> Msg -translateAlienData tag data = - case tag of - Alien.EnclosedData -> - UserMsg (User.EnclosedDataRetrieved data) - - Alien.SearchTracks -> - Search data - - ----------------------------------------- - -- From UI - ----------------------------------------- - Alien.DownloadTracks -> - DownloadTracks data - - Alien.ProcessSources -> - ProcessingMsg (Processing.Process data) - - Alien.RefreshedAccessToken -> - RefreshedAccessToken data - - Alien.RemoveEncryptionKey -> - UserMsg User.RemoveEncryptionKey - - Alien.RemoveTracksBySourceId -> - RemoveTracksBySourceId data - - Alien.RemoveTracksFromCache -> - RemoveTracksFromCache data - - Alien.SaveEnclosedUserData -> - UserMsg (User.SaveEnclosedData data) - - Alien.SaveFavourites -> - UserMsg (User.SaveFavourites data) - - Alien.SavePlaylists -> - UserMsg (User.SavePlaylists data) - - Alien.SaveProgress -> - UserMsg (User.SaveProgress data) - - Alien.SaveSettings -> - UserMsg (User.SaveSettings data) - - Alien.SaveSources -> - UserMsg (User.SaveSources data) - - Alien.SaveTracks -> - UserMsg (User.SaveTracks data) - - Alien.SetSyncMethod -> - UserMsg (User.SetSyncMethod data) - - Alien.StopProcessing -> - ProcessingMsg Processing.StopProcessing - - Alien.StoreTracksInCache -> - StoreTracksInCache data - - Alien.SyncTrackTags -> - SyncTrackTags data - - Alien.ToCache -> - ToCache data - - Alien.UnsetSyncMethod -> - UserMsg User.UnsetSyncMethod - - Alien.UpdateEncryptionKey -> - UserMsg (User.UpdateEncryptionKey data) - - _ -> - Bypass - - -translateAlienError : Alien.Tag -> Json.Value -> String -> Msg -translateAlienError tag _ err = - case err of - "db is undefined" -> - Common.reportUICmdMsg tag "Can't connect to the browser's IndexedDB. FYI, this is __not supported in Firefox's private mode__." - - _ -> - Common.reportUICmdMsg tag err diff --git a/src/Core/Brain/Common/State.elm b/src/Core/Brain/Common/State.elm deleted file mode 100644 index ea7ffe8aa..000000000 --- a/src/Core/Brain/Common/State.elm +++ /dev/null @@ -1,110 +0,0 @@ -module Brain.Common.State exposing (..) - -import Alien -import Brain.Ports as Ports -import Brain.Types exposing (..) -import Json.Decode as Json -import Return.Ext as Return -import Task -import TaskPort -import TaskPort.Extra as TaskPort - - - --- 🛠 - - -attemptPortTask : (a -> Msg) -> Task.Task TaskPort.Error a -> Cmd Msg -attemptPortTask mapFn = - Task.attempt (reportPortErrorToUI mapFn) - - -attemptTask : (a -> Msg) -> Task.Task String a -> Cmd Msg -attemptTask mapFn = - Task.attempt (reportErrorToUI mapFn) - - - --- GIVE - - -giveUI : Alien.Tag -> Json.Value -> Manager -giveUI tag data = - data - |> giveUICmd tag - |> Return.communicate - - -giveUICmd : Alien.Tag -> Json.Value -> Cmd Msg -giveUICmd tag data = - data - |> Alien.broadcast tag - |> Ports.toUI - - -giveUICmdMsg : Alien.Tag -> Json.Value -> Msg -giveUICmdMsg tag data = - data - |> giveUICmd tag - |> Cmd - - - --- NUDGE - - -nudgeUI : Alien.Tag -> Manager -nudgeUI = - nudgeUICmd >> Return.communicate - - -nudgeUICmd : Alien.Tag -> Cmd Msg -nudgeUICmd tag = - tag - |> Alien.trigger - |> Ports.toUI - - -nudgeUICmdMsg : Alien.Tag -> Msg -nudgeUICmdMsg = - nudgeUICmd >> Cmd - - - --- REPORT - - -reportErrorToUI : (a -> Msg) -> Result String a -> Msg -reportErrorToUI mapFn result = - case result of - Ok value -> - mapFn value - - Err error -> - reportUICmdMsg Alien.ReportError error - - -reportPortErrorToUI : (a -> Msg) -> Result TaskPort.Error a -> Msg -reportPortErrorToUI mapFn = - Result.mapError TaskPort.errorToStringCustom >> reportErrorToUI mapFn - - -reportUI : Alien.Tag -> String -> Manager -reportUI tag error = - error - |> reportUICmd tag - |> Return.communicate - - -reportUICmd : Alien.Tag -> String -> Cmd Msg -reportUICmd tag error = - error - |> Alien.report tag - |> Ports.toUI - - -reportUICmdMsg : Alien.Tag -> String -> Msg -reportUICmdMsg tag error = - error - |> reportUICmd tag - |> Cmd diff --git a/src/Core/Brain/Other/State.elm b/src/Core/Brain/Other/State.elm deleted file mode 100644 index 16a3ffabf..000000000 --- a/src/Core/Brain/Other/State.elm +++ /dev/null @@ -1,79 +0,0 @@ -module Brain.Other.State exposing (..) - -import Alien -import Brain.Common.State as Common -import Brain.Ports as Ports -import Brain.Task.Ports -import Brain.Types exposing (..) -import Dict -import Json.Decode as Json -import List.Extra as List -import Return exposing (return) -import Return.Ext as Return -import Sources exposing (Service(..)) -import Sources.Encoding -import Sources.Refresh.AccessToken -import Time - - - --- 🔱 - - -refreshedAccessToken : Json.Value -> Manager -refreshedAccessToken value model = - case Json.decodeValue Sources.Refresh.AccessToken.portArgumentsDecoder value of - Ok portArguments -> - case portArguments.service of - Google -> - model.hypaethralUserData.sources - |> List.find (.id >> (==) portArguments.sourceId) - |> Maybe.map - (\source -> - source.data - |> Dict.insert "accessToken" portArguments.accessToken - |> Dict.insert "expiresAt" (String.fromInt portArguments.expiresAt) - |> (\newData -> { source | data = newData }) - ) - |> Maybe.map - (\source -> - source - |> Sources.Encoding.encode - |> Alien.broadcast Alien.UpdateSourceData - |> Ports.toUI - ) - |> Maybe.withDefault Cmd.none - |> return model - - _ -> - Return.singleton model - - Err err -> - Common.reportUI Alien.ToCache (Json.errorToString err) model - - -setCurrentTime : Time.Posix -> Manager -setCurrentTime time model = - Return.singleton { model | currentTime = time } - - -{-| Save alien data to cache. --} -toCache : Json.Value -> Manager -toCache data = - case Json.decodeValue Alien.hostDecoder data of - Ok alienEvent -> - case Alien.tagFromString alienEvent.tag of - Just tag -> - alienEvent.data - |> Brain.Task.Ports.toCache tag - |> Common.attemptPortTask (always Bypass) - |> Return.communicate - - Nothing -> - Common.reportUI Alien.ToCache "Failed to decode alien tag" - - Err err -> - err - |> Json.errorToString - |> Common.reportUI Alien.ToCache diff --git a/src/Core/Brain/Ports.elm b/src/Core/Brain/Ports.elm deleted file mode 100644 index 14526a926..000000000 --- a/src/Core/Brain/Ports.elm +++ /dev/null @@ -1,83 +0,0 @@ -port module Brain.Ports exposing (..) - -import Alien -import Json.Encode as Json -import Sources.Processing exposing (ContextForTags, ContextForTagsSync) - - - --- 📣 - - -port downloadTracks : Json.Value -> Cmd msg - - -port removeTracksFromCache : Json.Value -> Cmd msg - - -port requestSearch : String -> Cmd msg - - -port requestTags : ContextForTags -> Cmd msg - - -port storeTracksInCache : Json.Value -> Cmd msg - - -port syncTags : ContextForTagsSync -> Cmd msg - - -port toUI : Alien.Event -> Cmd msg - - -port updateSearchIndex : Json.Value -> Cmd msg - - - --- 📣 ░░ USER LAYER SERVICES - - -port deconstructRemoteStorage : () -> Cmd msg - - -port provideArtworkTrackUrls : Json.Value -> Cmd msg - - -port requestDropbox : Alien.Event -> Cmd msg - - -port requestIpfs : Alien.Event -> Cmd msg - - -port requestRemoteStorage : Alien.Event -> Cmd msg - - -port toDropbox : Alien.Event -> Cmd msg - - -port toIpfs : Alien.Event -> Cmd msg - - -port toRemoteStorage : Alien.Event -> Cmd msg - - - --- 📰 - - -port fromAlien : (Alien.Event -> msg) -> Sub msg - - -port makeArtworkTrackUrls : (Json.Value -> msg) -> Sub msg - - -port receiveSearchResults : (List String -> msg) -> Sub msg - - -port refreshedAccessToken : (Json.Value -> msg) -> Sub msg - - -port receiveTags : (ContextForTags -> msg) -> Sub msg - - -port replaceTags : (ContextForTagsSync -> msg) -> Sub msg diff --git a/src/Core/Brain/Sources/Processing/Common.elm b/src/Core/Brain/Sources/Processing/Common.elm deleted file mode 100644 index 39b501ff2..000000000 --- a/src/Core/Brain/Sources/Processing/Common.elm +++ /dev/null @@ -1,86 +0,0 @@ -module Brain.Sources.Processing.Common exposing (..) - -import Alien -import Brain.Common.State as Common -import Brain.Types exposing (Manager) -import Dict.Ext as Dict -import Http exposing (Error(..)) -import Json.Encode as Encode -import List.Extra as List -import Maybe.Extra as Maybe -import Sources exposing (Service, Source) -import Sources.Processing exposing (..) -import Sources.Services as Services -import Tracks exposing (Track) - - - --- 🔱 - - -contextToTagsContext : Context -> ContextForTags -contextToTagsContext context = - { amount = List.length context.filePaths - , nextFilePaths = context.filePaths - , receivedFilePaths = [] - , receivedTags = [] - , sourceId = context.source.id - , urlsForTags = [] - } - - -isProcessing : Status -> Bool -isProcessing status = - case status of - Processing _ _ -> - True - - NotProcessing -> - False - - -reportHttpError : Source -> Http.Error -> Manager -reportHttpError source err = - reportError - source - (translateHttpError source.service err) - - -reportError : Source -> String -> Manager -reportError source error = - [ ( "sourceId", Encode.string source.id ) - , ( "sourceName", Encode.string (Dict.fetch "name" "Unnamed" source.data) ) - , ( "error", Encode.string error ) - ] - |> Encode.object - |> Common.giveUI Alien.ReportProcessingError - - -tracksFromTagsContext : ContextForTags -> List Track -tracksFromTagsContext context = - context.receivedTags - |> List.zip context.receivedFilePaths - |> List.filter (Tuple.second >> Maybe.isJust) - |> List.map (Tuple.mapSecond (Maybe.withDefault Tracks.emptyTags)) - |> List.map (Tracks.makeTrack context.sourceId) - - -translateHttpError : Service -> Http.Error -> String -translateHttpError service err = - case err of - NetworkError -> - "Cannot connect to this source" - - Timeout -> - "Source did not respond (timeout)" - - BadUrl _ -> - "Diffuse error, invalid url was used" - - BadStatus _ -> - "Got a faulty response from this source. Use the developer console to get more info." - - BadBody response -> - response - |> Services.parseErrorResponse service - |> Maybe.withDefault (translateHttpError service <| BadStatus 0) diff --git a/src/Core/Brain/Sources/Processing/State.elm b/src/Core/Brain/Sources/Processing/State.elm deleted file mode 100644 index 53982918b..000000000 --- a/src/Core/Brain/Sources/Processing/State.elm +++ /dev/null @@ -1,236 +0,0 @@ -module Brain.Sources.Processing.State exposing (..) - -import Alien -import Brain.Common.State as Common -import Brain.Sources.Processing.Common exposing (..) -import Brain.Sources.Processing.Steps as Steps -import Brain.Sources.Processing.Types as Processing exposing (..) -import Brain.Tracks.State as Tracks -import Brain.Types exposing (..) -import Dict.Ext as Dict -import Http -import Json.Decode as Json -import Json.Encode as Encode -import List.Extra as List -import Return exposing (..) -import Sources exposing (Source) -import Sources.Processing exposing (..) -import Sources.Processing.Encoding as Processing - - - --- 📣 - - -update : Processing.Msg -> Manager -update msg = - case msg of - Process a -> - process a - - NextInLine -> - nextInLine - - StopProcessing -> - stopProcessing - - ----------------------------------------- - -- Steps - ----------------------------------------- - PrepareStep a b -> - prepareStep a b - - TreeStep a b -> - treeStep a b - - TreeStepRemoveTracks a b -> - treeStepRemoveTracks a b - - TagsStep a -> - tagsStep a - - - --- 🔱 - - -process : Json.Value -> Manager -process json = - -- Only proceed to the processing if we got all the necessary data, - -- otherwise report an error in the UI. - case Json.decodeValue Processing.argumentsDecoder json of - Ok arguments -> - process_ arguments - - Err err -> - Common.reportUI Alien.ProcessSources (Json.errorToString err) - - - -{- If already processing, do nothing. - If there are no sources, do nothing. - If there are sources, start processing the first source. --} - - -process_ : { origin : String, sources : List Source } -> Manager -process_ { origin, sources } model = - let - tracks = - model.hypaethralUserData.tracks - - filter s = - List.filter (.sourceId >> (==) s.id) tracks - - all = - sources - |> List.sortBy (.data >> Dict.fetch "name" "") - |> List.map (\s -> ( s, filter s )) - in - case - ( isProcessing model.processingStatus || List.isEmpty sources - , List.uncons all - ) - of - ( False, Just ( ( s, t ), future ) ) -> - return - { model | origin = origin, processingStatus = Processing ( s, t ) future } - (Steps.takeFirstStep origin model.currentTime s) - - _ -> - Return.singleton model - - - -{- If not processing, do nothing. - If there are no sources left, do nothing. - If there are sources left, start processing the next source in line. --} - - -nextInLine : Manager -nextInLine model = - case model.processingStatus of - Processing ( processedSource, _ ) (( source, tracks ) :: rest) -> - source - |> Steps.takeFirstStep model.origin model.currentTime - |> return - { model | processingStatus = Processing ( source, tracks ) rest } - |> andThen - (processedSource.id - |> Encode.string - |> Common.giveUI Alien.FinishedProcessingSource - ) - - _ -> - Common.nudgeUI - Alien.FinishedProcessingSources - { model | processingStatus = NotProcessing } - - - -{- STOP! -} - - -stopProcessing : Manager -stopProcessing model = - Return.singleton { model | processingStatus = NotProcessing } - - - --- PHASE 1 ----------- --- Prepare for processing. - - -prepareStep : Context -> Result Http.Error String -> Manager -prepareStep context result model = - case result of - Ok response -> - model.currentTime - |> Steps.takePrepareStep context response - |> return model - - Err err -> - model - |> nextInLine - |> andThen (reportHttpError context.source err) - - - --- PHASE 2 ----------- --- Make a file list/tree. - - -treeStep : Context -> Result Http.Error String -> Manager -treeStep context result model = - case result of - Ok response -> - case model.processingStatus of - Processing ( _, tracks ) rest -> - return - { model | processingStatus = Processing ( context.source, tracks ) rest } - (Steps.takeTreeStep context response tracks model.currentTime) - - NotProcessing -> - Return.singleton model - - Err err -> - model - |> nextInLine - |> andThen (reportHttpError context.source err) - - -treeStepRemoveTracks : String -> List String -> Manager -treeStepRemoveTracks sourceId filePaths model = - let - encodedData = - Encode.object - [ ( "filePaths", Encode.list Encode.string filePaths ) - , ( "sourceId", Encode.string sourceId ) - ] - in - model - |> Common.giveUI Alien.RemoveTracksByPath encodedData - |> andThen (Tracks.removeByPaths { sourceId = sourceId, paths = filePaths }) - - - --- PHASE 3 ----------- --- Get the tags for each file in the file list. - - -tagsStep : ContextForTags -> Manager -tagsStep tagsContext model = - let - maybeCmd = - case model.processingStatus of - Processing ( source, _ ) _ -> - Steps.takeTagsStep model.currentTime tagsContext source - - NotProcessing -> - Just Cmd.none - - tracksToAdd = - tagsContext - |> tracksFromTagsContext - |> List.map (\track -> { track | insertedAt = model.currentTime }) - - amountLeft = - List.length tagsContext.nextFilePaths - - progressPercentage = - 0.05 + 0.95 * (1 - toFloat amountLeft / toFloat tagsContext.amount) - - progress = - [ ( "progress", Encode.float progressPercentage ) - , ( "sourceId", Encode.string tagsContext.sourceId ) - ] - in - maybeCmd - |> Maybe.map (return model) - |> Maybe.withDefault (nextInLine model) - |> andThen (Tracks.add tracksToAdd) - |> andThen (Common.giveUI Alien.ReportProcessingProgress <| Encode.object progress) diff --git a/src/Core/Brain/Sources/Processing/Steps.elm b/src/Core/Brain/Sources/Processing/Steps.elm deleted file mode 100644 index 61f1d55ab..000000000 --- a/src/Core/Brain/Sources/Processing/Steps.elm +++ /dev/null @@ -1,327 +0,0 @@ -module Brain.Sources.Processing.Steps exposing - ( takeFirstStep - , takePrepareStep - , takeTagsStep - , takeTreeStep - ) - -{-| Processing. - - ## How it works - - This describes the process for a single source. - - 1. Prepare the source for processing. - -> For example, a source could get an access token first. - 2. Get a file tree/list from the source - -> This can happen in multiple steps as with Amazon S3. - A command is issued for each step of this process. - 3. Get the tags (ie. metadata) for each file that we found. - -> This also happens in multiple steps, so that we can flush - every x tracks while processing. - A command is issued for each step of this process. - --} - -import Alien -import Brain.Ports as Ports -import Brain.Sources.Processing.Common exposing (..) -import Brain.Sources.Processing.Types exposing (..) -import Brain.Types as Brain exposing (..) -import List.Extra as List -import Set -import Sources exposing (Source) -import Sources.Encoding -import Sources.Processing exposing (..) -import Sources.Services as Services -import Task.Extra exposing (do) -import Time -import Tracks exposing (Track) - - - --- SETTINGS - - -{-| How much tags do we want to process -before we send them back to Elm. - - eg. After we got the tags for 20 tracks, - we store these and continue with the rest. - --} -tagsBatchSize : Int -tagsBatchSize = - 20 - - - --- 1st STEP - - -takeFirstStep : String -> Time.Posix -> Source -> Cmd Brain.Msg -takeFirstStep origin currentTime source = - let - initialContext = - { filePaths = [] - , origin = origin - , preparationMarker = TheBeginning - , source = source - , treeMarker = TheBeginning - } - in - prepare initialContext currentTime - - - --- 2nd STEP - - -takePrepareStep : Context -> String -> Time.Posix -> Cmd Brain.Msg -takePrepareStep context response currentTime = - context - |> handlePreparationResponse response currentTime - |> intoPreparationCommands currentTime - - - --- 3rd STEP - - -takeTreeStep : Context -> String -> List Track -> Time.Posix -> Cmd Brain.Msg -takeTreeStep context response associatedTracks currentTime = - context - |> handleTreeResponse response - |> intoTreeCommand associatedTracks currentTime - - - --- 4th STEP - - -takeTagsStep : Time.Posix -> ContextForTags -> Source -> Maybe (Cmd Brain.Msg) -takeTagsStep currentTime tagsCtx source = - let - ( filesToProcess, nextFiles ) = - List.splitAt tagsBatchSize tagsCtx.nextFilePaths - - newTagsCtx = - { amount = tagsCtx.amount - , nextFilePaths = nextFiles - , receivedFilePaths = filesToProcess - , receivedTags = [] - , sourceId = source.id - , urlsForTags = makeTrackUrls currentTime source filesToProcess - } - in - if List.isEmpty filesToProcess then - Nothing - - else - Just (getTags newTagsCtx) - - - ------------------------------------------ --- ㊙️ ------------------------------------------ --- PREPARE - - -prepare : Context -> Time.Posix -> Cmd Brain.Msg -prepare context currentTime = - let - maybePreparationCommand = - Services.prepare - context.source.service - context.origin - context.source.data - context.preparationMarker - (ProcessingMsg << PrepareStep context) - in - case maybePreparationCommand of - Just cmd -> - cmd - - Nothing -> - -- Some services don't need to prepare for processing. - -- 🚀 - makeTree context currentTime - - -handlePreparationResponse : String -> Time.Posix -> Context -> Context -handlePreparationResponse response currentTime context = - let - answer = - Services.parsePreparationResponse - context.source.service - response - currentTime - context.source.data - context.preparationMarker - - source = - context.source - in - { context - | preparationMarker = answer.marker - , source = { source | data = answer.sourceData } - } - - -intoPreparationCommands : Time.Posix -> Context -> Cmd Brain.Msg -intoPreparationCommands currentTime context = - case context.preparationMarker of - TheBeginning -> - Cmd.none - - -- Still preparing, - -- carry on. - -- - InProgress _ -> - prepare context currentTime - - -- The preparation is completed, - -- continue to the next step. - -- - TheEnd -> - let - updatedSource = - context.source - in - Cmd.batch - [ -- Make a file tree, the next step. - -- 🚀 - makeTree context currentTime - - -- Update source data. - , updatedSource - |> Sources.Encoding.encode - |> Alien.broadcast Alien.UpdateSourceData - |> Ports.toUI - ] - - - --- TREE - - -makeTree : Context -> Time.Posix -> Cmd Brain.Msg -makeTree context currentTime = - Services.makeTree - context.source.service - context.source.data - context.treeMarker - currentTime - (ProcessingMsg << TreeStep context) - - -handleTreeResponse : String -> Context -> Context -handleTreeResponse response context = - let - parsingFunc = - Services.parseTreeResponse context.source.service - - parsedResponse = - parsingFunc response context.treeMarker - in - { context - | filePaths = context.filePaths ++ parsedResponse.filePaths - , treeMarker = parsedResponse.marker - } - - -intoTreeCommand : List Track -> Time.Posix -> Context -> Cmd Brain.Msg -intoTreeCommand associatedTracks currentTime context = - case context.treeMarker of - TheBeginning -> - Cmd.none - - -- Still building the tree, - -- carry on. - -- - InProgress _ -> - makeTree context currentTime - - -- The tree's been build, - -- continue to the next step. - -- - TheEnd -> - let - filteredFiles = - Services.postProcessTree context.source.service context.filePaths - - postContext = - { context | filePaths = filteredFiles } - - pathsSourceOfTruth = - postContext.filePaths - - pathsCurrent = - List.map .path associatedTracks - - ( pathsAdded, pathsRemoved ) = - separate pathsCurrent pathsSourceOfTruth - in - Cmd.batch - [ -- Get tags from tracks, the next step. - -- 🚀 - postContext - |> (\ctx -> { ctx | filePaths = pathsAdded }) - |> contextToTagsContext - |> TagsStep - |> ProcessingMsg - |> do - - -- Remove tracks - , if not (List.isEmpty pathsRemoved) then - pathsRemoved - |> TreeStepRemoveTracks context.source.id - |> ProcessingMsg - |> do - - else - Cmd.none - ] - - -separate : List String -> List String -> ( List String, List String ) -separate current srcOfTruth = - let - setCurrent = - Set.fromList current - - setSrcOfTruth = - Set.fromList srcOfTruth - in - ( -- Added - -------- - Set.diff setSrcOfTruth setCurrent |> Set.toList - , -- Removed - ---------- - Set.diff setCurrent setSrcOfTruth |> Set.toList - ) - - - --- TAGS - - -getTags : ContextForTags -> Cmd Brain.Msg -getTags = - Ports.requestTags - - -makeTrackUrls : Time.Posix -> Source -> List String -> List TagUrls -makeTrackUrls currentTime source filePaths = - let - maker = - Services.makeTrackUrl source.service - - mapFn = - \path -> - { getUrl = maker currentTime source.id source.data Get path - , headUrl = maker currentTime source.id source.data Head path - } - in - List.map mapFn filePaths diff --git a/src/Core/Brain/Sources/Processing/Types.elm b/src/Core/Brain/Sources/Processing/Types.elm deleted file mode 100644 index 843003bd7..000000000 --- a/src/Core/Brain/Sources/Processing/Types.elm +++ /dev/null @@ -1,22 +0,0 @@ -module Brain.Sources.Processing.Types exposing (..) - -import Http -import Json.Decode as Json -import Sources.Processing exposing (..) - - - --- 📣 - - -type Msg - = Process Json.Value - | NextInLine - | StopProcessing - ----------------------------------------- - -- Steps - ----------------------------------------- - | PrepareStep Context (Result Http.Error String) - | TreeStep Context (Result Http.Error String) - | TreeStepRemoveTracks String (List String) - | TagsStep ContextForTags diff --git a/src/Core/Brain/Task/Ports.elm b/src/Core/Brain/Task/Ports.elm deleted file mode 100644 index 452d5891a..000000000 --- a/src/Core/Brain/Task/Ports.elm +++ /dev/null @@ -1,79 +0,0 @@ -module Brain.Task.Ports exposing (..) - -import Alien -import Json.Decode -import Json.Encode -import TaskPort - - - --- CACHE - - -fromCache : Alien.Tag -> Json.Decode.Decoder value -> TaskPort.Task (Maybe value) -fromCache tag decoder = - TaskPort.call - { function = "fromCache" - , valueDecoder = Json.Decode.maybe decoder - , argsEncoder = Json.Encode.string - } - (Alien.tagToString tag) - - -fromCacheWithSuffix : Alien.Tag -> String -> Json.Decode.Decoder value -> TaskPort.Task (Maybe value) -fromCacheWithSuffix tag suffix decoder = - TaskPort.call - { function = "fromCache" - , valueDecoder = Json.Decode.maybe decoder - , argsEncoder = Json.Encode.string - } - (Alien.tagToString tag ++ "_" ++ suffix) - - -removeCache : Alien.Tag -> TaskPort.Task () -removeCache tag = - TaskPort.call - { function = "removeCache" - , valueDecoder = TaskPort.ignoreValue - , argsEncoder = Json.Encode.string - } - (Alien.tagToString tag) - - -toCache : Alien.Tag -> Json.Encode.Value -> TaskPort.Task () -toCache tag = - let - key = - Alien.tagToString tag - in - TaskPort.call - { function = "toCache" - , valueDecoder = TaskPort.ignoreValue - , argsEncoder = \v -> Json.Encode.object [ ( "key", Json.Encode.string key ), ( "value", v ) ] - } - - -toCacheWithSuffix : Alien.Tag -> String -> Json.Encode.Value -> TaskPort.Task () -toCacheWithSuffix tag suffix = - let - key = - Alien.tagToString tag ++ "_" ++ suffix - in - TaskPort.call - { function = "toCache" - , valueDecoder = TaskPort.ignoreValue - , argsEncoder = \v -> Json.Encode.object [ ( "key", Json.Encode.string key ), ( "value", v ) ] - } - - - --- CRYPTO - - -fabricateSecretKey : String -> TaskPort.Task () -fabricateSecretKey = - TaskPort.call - { function = "fabricateSecretKey" - , valueDecoder = Json.Decode.succeed () - , argsEncoder = Json.Encode.string - } diff --git a/src/Core/Brain/Tracks/State.elm b/src/Core/Brain/Tracks/State.elm deleted file mode 100644 index 966065a54..000000000 --- a/src/Core/Brain/Tracks/State.elm +++ /dev/null @@ -1,347 +0,0 @@ -module Brain.Tracks.State exposing (..) - -import Alien -import Brain.Common.State as Common -import Brain.Ports as Ports -import Brain.Types exposing (..) -import Brain.User.State as User -import Dict -import Dict.Ext as Dict -import Json.Decode as Json exposing (Decoder) -import Json.Encode -import List.Extra as List -import Queue -import Return exposing (andThen, return) -import Return.Ext as Return -import Sources exposing (Source) -import Sources.Processing exposing (ContextForTagsSync, HttpMethod(..), TagUrls) -import Sources.Services -import Time -import Tracks exposing (Track) -import Tracks.Encoding - - - --- 🔱 - - -add : List Track -> Manager -add list model = - case list of - [] -> - Return.singleton model - - tracks -> - model - |> User.saveTracksAndUpdateSearchIndex - (List.append model.hypaethralUserData.tracks tracks) - |> andThen - (tracks - |> Json.Encode.list Tracks.Encoding.encodeTrack - |> Common.giveUI Alien.AddTracks - ) - - -download : Json.Value -> Manager -download json model = - let - { prefixTrackNumber, trackIds, zipName } = - json - |> Json.decodeValue downloadParamsDecoder - |> Result.withDefault - { prefixTrackNumber = False - , trackIds = [] - , zipName = "failed-to-decode-json" - } - in - model.hypaethralUserData.tracks - |> Tracks.pick trackIds - |> List.indexedMap Tuple.pair - |> Json.Encode.list - (\( idx, track ) -> - Json.Encode.object - [ ( "filename" - , [ if prefixTrackNumber then - (idx + 1) - |> String.fromInt - |> String.padLeft 2 '0' - |> (\s -> s ++ " - ") - - else - "" - , track.tags.artist - |> Maybe.map (\a -> a ++ " - ") - |> Maybe.withDefault "" - , track.tags.title - ] - |> String.concat - |> Json.Encode.string - ) - - -- - , ( "path" - , Json.Encode.string track.path - ) - , ( "url" - , track - |> Queue.makeTrackUrl - model.currentTime - model.hypaethralUserData.sources - |> Json.Encode.string - ) - ] - ) - |> (\encodedTracks -> - Json.Encode.object - [ ( "name", Json.Encode.string zipName ) - , ( "tracks", encodedTracks ) - ] - ) - |> Ports.downloadTracks - |> return model - - -gotSearchResults : List String -> Manager -gotSearchResults results = - Common.giveUI Alien.SearchTracks (Json.Encode.list Json.Encode.string results) - - -makeArtworkTrackUrls : Json.Value -> Manager -makeArtworkTrackUrls json model = - json - |> Json.decodeValue - (Json.dict Json.string) - |> Result.map - (\dict -> - let - maybeSource = - Maybe.andThen - (\trackSourceId -> - List.find - (.id >> (==) trackSourceId) - model.hypaethralUserData.sources - ) - (Dict.get "trackSourceId" dict) - - trackPath = - Dict.fetch "trackPath" "" dict - - mkTrackUrl = - makeTrackUrl model.currentTime trackPath maybeSource - in - dict - |> Dict.insert "trackGetUrl" (mkTrackUrl Get) - |> Dict.insert "trackHeadUrl" (mkTrackUrl Head) - |> Json.Encode.dict identity Json.Encode.string - |> Ports.provideArtworkTrackUrls - |> return model - ) - |> Result.withDefault - (Return.singleton model) - - -removeByPaths : { sourceId : String, paths : List String } -> Manager -removeByPaths args model = - User.saveTracksAndUpdateSearchIndex - (model.hypaethralUserData.tracks - |> Tracks.removeByPaths args - |> .kept - ) - model - - -removeBySourceId : Json.Value -> Manager -removeBySourceId data model = - case Json.decodeValue Json.string data of - Ok sourceId -> - User.saveTracksAndUpdateSearchIndex - (model.hypaethralUserData.tracks - |> Tracks.removeBySourceId sourceId - |> .kept - ) - model - - Err _ -> - Return.singleton model - - -removeFromCache : Json.Value -> Manager -removeFromCache data = - Return.communicate (Ports.removeTracksFromCache data) - - -replaceTags : ContextForTagsSync -> Manager -replaceTags context model = - model.hypaethralUserData.tracks - |> List.foldr - (\track ( acc, trackIds, tags ) -> - case List.elemIndex track.id trackIds of - Just idx -> - let - newTags = - tags - |> List.getAt idx - |> Maybe.andThen identity - |> Maybe.withDefault track.tags - in - ( { track | tags = newTags } :: acc - , List.removeAt idx trackIds - , List.removeAt idx tags - ) - - Nothing -> - ( track :: acc - , trackIds - , tags - ) - ) - ( [] - , context.trackIds - , context.receivedTags - ) - |> (\( a, _, _ ) -> - User.saveTracksAndUpdateSearchIndex a model - ) - |> andThen - (\m -> - m.hypaethralUserData.tracks - |> Json.Encode.list Tracks.Encoding.encodeTrack - |> (\data -> Common.giveUI Alien.ReloadTracks data m) - ) - - -search : Json.Value -> Manager -search encodedSearchTerm = - encodedSearchTerm - |> Json.decodeValue Json.string - |> Result.map Ports.requestSearch - |> Result.withDefault Cmd.none - |> Return.communicate - - -storeInCache : Json.Value -> Manager -storeInCache data = - Return.communicate (Ports.storeTracksInCache data) - - -syncTrackTags : Json.Value -> Manager -syncTrackTags data model = - let - result = - Json.decodeValue - (Json.list <| - Json.map3 - (\path sourceId trackId -> - { path = path - , sourceId = sourceId - , trackId = trackId - } - ) - (Json.field "path" Json.string) - (Json.field "sourceId" Json.string) - (Json.field "trackId" Json.string) - ) - data - - ( sources, _ ) = - result - |> Result.withDefault [] - |> List.foldl - (\{ sourceId } ( dict, acc ) -> - if List.member sourceId acc then - ( dict, acc ) - - else - case List.find (.id >> (==) sourceId) model.hypaethralUserData.sources of - Just source -> - ( Dict.insert sourceId source dict, sourceId :: acc ) - - Nothing -> - ( dict, sourceId :: acc ) - ) - ( Dict.empty, [] ) - in - case result of - Ok list -> - list - |> List.foldr - (\{ path, sourceId, trackId } ( accPaths, accUrls, accIds ) -> - sources - |> Dict.get sourceId - |> Maybe.map - (tagUrls model.currentTime path) - |> Maybe.map - (\urls -> - ( path :: accPaths, urls :: accUrls, trackId :: accIds ) - ) - |> Maybe.withDefault - ( accPaths, accUrls, accIds ) - ) - ( [], [], [] ) - |> (\( accPaths, accUrls, accIds ) -> - Ports.syncTags - { receivedFilePaths = accPaths - , receivedTags = [] - , trackIds = accIds - , urlsForTags = accUrls - } - ) - |> return model - - Err _ -> - Return.singleton model - - -updateSearchIndex : Json.Value -> Manager -updateSearchIndex data = - Return.communicate (Ports.updateSearchIndex data) - - - --- ⚗️ - - -downloadParamsDecoder : - Decoder - { prefixTrackNumber : Bool - , trackIds : List String - , zipName : String - } -downloadParamsDecoder = - Json.map3 - (\a b c -> - { prefixTrackNumber = a - , trackIds = b - , zipName = c - } - ) - (Json.field "prefixTrackNumber" <| Json.bool) - (Json.field "trackIds" <| Json.list Json.string) - (Json.field "zipName" <| Json.string) - - -makeTrackUrl : Time.Posix -> String -> Maybe Source -> HttpMethod -> String -makeTrackUrl timestamp trackPath maybeSource httpMethod = - case maybeSource of - Just source -> - Sources.Services.makeTrackUrl - source.service - timestamp - source.id - source.data - httpMethod - trackPath - - Nothing -> - "" - - -tagUrls : Time.Posix -> String -> Source -> TagUrls -tagUrls currentTime path source = - let - maker = - Sources.Services.makeTrackUrl source.service currentTime source.id source.data - in - { getUrl = maker Get path - , headUrl = maker Head path - } diff --git a/src/Core/Brain/Types.elm b/src/Core/Brain/Types.elm deleted file mode 100644 index 0fcde6b0d..000000000 --- a/src/Core/Brain/Types.elm +++ /dev/null @@ -1,76 +0,0 @@ -module Brain.Types exposing (..) - -import Brain.Sources.Processing.Types as Processing -import Brain.User.Types as User -import Debouncer.Basic exposing (Debouncer) -import Json.Decode as Json -import List.Zipper exposing (Zipper) -import Management -import Sources.Processing as Processing -import Time -import User.Layer as User exposing (HypaethralBaggage, HypaethralBit) - - - --- 🧠 - - -type alias Flags = - { initialUrl : String } - - - --- 🌳 - - -type alias Model = - { currentTime : Time.Posix - , hypaethralDebouncer : Debouncer HypaethralBit (List HypaethralBit) - , hypaethralRetrieval : Maybe (Zipper ( HypaethralBit, Json.Value, HypaethralBaggage )) - , hypaethralStorage : List { bit : HypaethralBit, saving : Bool } - , hypaethralUserData : User.HypaethralData - , origin : String - , processingStatus : Processing.Status - , userSyncMethod : Maybe User.Method - } - - - --- 📣 - - -type Msg - = Bypass - | Cmd (Cmd Msg) - ----------------------------------------- - -- Tracks - ----------------------------------------- - | DownloadTracks Json.Value - | GotSearchResults (List String) - | MakeArtworkTrackUrls Json.Value - | RemoveTracksBySourceId Json.Value - | RemoveTracksFromCache Json.Value - | ReplaceTrackTags Processing.ContextForTagsSync - | Search Json.Value - | StoreTracksInCache Json.Value - | SyncTrackTags Json.Value - | UpdateSearchIndex Json.Value - ----------------------------------------- - -- 🦉 Nested - ----------------------------------------- - | ProcessingMsg Processing.Msg - | UserMsg User.Msg - ----------------------------------------- - -- 📭 Other - ----------------------------------------- - | RefreshedAccessToken Json.Value - | SetCurrentTime Time.Posix - | ToCache Json.Value - - -type alias Organizer model = - Management.Manager Msg model - - -type alias Manager = - Organizer Model diff --git a/src/Core/Brain/User/Hypaethral.elm b/src/Core/Brain/User/Hypaethral.elm deleted file mode 100644 index af1505b0d..000000000 --- a/src/Core/Brain/User/Hypaethral.elm +++ /dev/null @@ -1,128 +0,0 @@ -module Brain.User.Hypaethral exposing (..) - -import Alien -import Brain.Task.Ports -import Json.Decode -import Json.Encode -import Task exposing (Task) -import TaskPort -import TaskPort.Extra as TaskPort -import User.Layer exposing (..) - - - --- RETRIEVAL - - -retrieveDropbox : String -> HypaethralBit -> Task String (Maybe Json.Decode.Value) -retrieveDropbox accessToken bit = - [ ( "fileName", fileName bit ) - , ( "token", Json.Encode.string accessToken ) - ] - |> TaskPort.call - { function = "fromDropbox" - , valueDecoder = Json.Decode.maybe Json.Decode.value - , argsEncoder = Json.Encode.object - } - |> Task.mapError TaskPort.errorToStringCustom - - -retrieveIpfs : String -> HypaethralBit -> Task String (Maybe Json.Decode.Value) -retrieveIpfs apiOrigin bit = - [ ( "fileName", fileName bit ) - , ( "apiOrigin", Json.Encode.string apiOrigin ) - ] - |> TaskPort.call - { function = "fromIpfs" - , valueDecoder = Json.Decode.maybe Json.Decode.value - , argsEncoder = Json.Encode.object - } - |> Task.mapError TaskPort.errorToStringCustom - - -retrieveRemoteStorage : { token : String, userAddress : String } -> HypaethralBit -> Task String (Maybe Json.Decode.Value) -retrieveRemoteStorage { token, userAddress } bit = - [ ( "fileName", fileName bit ) - , ( "token", Json.Encode.string token ) - , ( "userAddress", Json.Encode.string userAddress ) - ] - |> TaskPort.call - { function = "fromRemoteStorage" - , valueDecoder = Json.Decode.maybe Json.Decode.value - , argsEncoder = Json.Encode.object - } - |> Task.mapError TaskPort.errorToStringCustom - - -retrieveLocal : HypaethralBit -> Task String (Maybe Json.Decode.Value) -retrieveLocal bit = - Json.Decode.value - |> Brain.Task.Ports.fromCacheWithSuffix - Alien.SyncLocal - (hypaethralBitFileName bit) - |> Task.mapError TaskPort.errorToStringCustom - - - --- STORAGE - - -saveDropbox : String -> HypaethralBit -> Json.Decode.Value -> Task String () -saveDropbox accessToken bit data = - [ ( "fileName", fileName bit ) - , ( "data", data ) - , ( "token", Json.Encode.string accessToken ) - ] - |> TaskPort.call - { function = "toDropbox" - , valueDecoder = TaskPort.ignoreValue - , argsEncoder = Json.Encode.object - } - |> Task.mapError TaskPort.errorToStringCustom - - -saveIpfs : String -> HypaethralBit -> Json.Decode.Value -> Task String () -saveIpfs apiOrigin bit data = - [ ( "apiOrigin", Json.Encode.string apiOrigin ) - , ( "fileName", fileName bit ) - , ( "data", data ) - ] - |> TaskPort.call - { function = "toIpfs" - , valueDecoder = TaskPort.ignoreValue - , argsEncoder = Json.Encode.object - } - |> Task.mapError TaskPort.errorToStringCustom - - -saveRemoteStorage : { token : String, userAddress : String } -> HypaethralBit -> Json.Decode.Value -> Task String () -saveRemoteStorage { token, userAddress } bit data = - [ ( "fileName", fileName bit ) - , ( "data", data ) - , ( "token", Json.Encode.string token ) - , ( "userAddress", Json.Encode.string userAddress ) - ] - |> TaskPort.call - { function = "toRemoteStorage" - , valueDecoder = TaskPort.ignoreValue - , argsEncoder = Json.Encode.object - } - |> Task.mapError TaskPort.errorToStringCustom - - -saveLocal : HypaethralBit -> Json.Decode.Value -> Task String () -saveLocal bit data = - data - |> Brain.Task.Ports.toCacheWithSuffix - Alien.SyncLocal - (hypaethralBitFileName bit) - |> Task.mapError TaskPort.errorToStringCustom - - - --- 🛠 - - -fileName : HypaethralBit -> Json.Decode.Value -fileName = - Json.Encode.string << hypaethralBitFileName diff --git a/src/Core/Brain/User/State.elm b/src/Core/Brain/User/State.elm deleted file mode 100644 index 5cc12c14c..000000000 --- a/src/Core/Brain/User/State.elm +++ /dev/null @@ -1,749 +0,0 @@ -module Brain.User.State exposing (..) - -import Alien -import Brain.Common.State as Common -import Brain.Ports as Ports -import Brain.Task.Ports -import Brain.Types as Brain exposing (..) -import Brain.User.Hypaethral as Hypaethral -import Brain.User.Types as User exposing (..) -import Debouncer.Basic as Debouncer -import EverySet -import Json.Decode as Decode -import Json.Encode as Json -import Playlists.Encoding as Playlists -import Return exposing (andThen, return) -import Return.Ext as Return -import Settings -import Sources.Encoding as Sources -import Syncing -import Syncing.Services.Dropbox.Token -import Task exposing (Task) -import TaskPort.Extra as TaskPort -import Time -import Tracks exposing (Track) -import Tracks.Encoding as Tracks -import Url exposing (Url) -import Url.Ext as Url -import User.Layer as User exposing (..) -import User.Layer.Methods.Dropbox as Dropbox - - - --- 🌳 - - -initialCommand : Url -> Cmd Brain.Msg -initialCommand uiUrl = - case Url.action uiUrl of - _ -> - Cmd.batch - [ loadEnclosedData - , loadLocalHypaethralData - { initialUrl = uiUrl - , methodTask = loadSyncMethod - } - ] - - -{-| Loads the "enclosed" data from cache and sends it to the UI. --} -loadEnclosedData : Cmd Brain.Msg -loadEnclosedData = - Decode.value - |> Brain.Task.Ports.fromCache Alien.EnclosedData - |> Task.map (Maybe.withDefault Json.null) - |> Common.attemptPortTask (Common.giveUICmdMsg Alien.LoadEnclosedUserData) - - -{-| Loads the "sync method". --} -loadSyncMethod : Task String (Maybe Method) -loadSyncMethod = - Decode.value - |> Brain.Task.Ports.fromCache Alien.SyncMethod - |> Task.mapError TaskPort.errorToStringCustom - |> Task.map (Maybe.andThen decodeMethod) - - -{-| Loads the "sync method" and "hypaethral" data, -see `Commence` Msg what happens next. --} -loadLocalHypaethralData : { initialUrl : Url, methodTask : Task String (Maybe Method) } -> Cmd Brain.Msg -loadLocalHypaethralData { initialUrl, methodTask } = - methodTask - |> Task.andThen - (\maybeMethod -> - Hypaethral.retrieveLocal - |> User.retrieveHypaethralData - |> Task.map - (\bits -> - bits - |> List.map (\( a, b ) -> ( hypaethralBitKey a, Maybe.withDefault Json.null b )) - |> Json.object - ) - |> Task.map (Tuple.pair maybeMethod) - ) - |> Common.attemptTask - (\( maybeMethod, hypaethralJson ) -> - hypaethralJson - |> User.decodeHypaethralData - |> Result.map - (\hypaethralData -> - Commence - maybeMethod - initialUrl - ( hypaethralJson - , hypaethralData - ) - ) - |> Result.mapError Decode.errorToString - |> Common.reportErrorToUI UserMsg - ) - - - --- 📣 - - -update : User.Msg -> Manager -update msg = - case msg of - Commence a b c -> - commence a b c - - SetSyncMethod a -> - setSyncMethod a - - Sync -> - sync { initialTask = Nothing } - - UnsetSyncMethod -> - unsetSyncMethod - - ----------------------------------------- - -- x. Data - ----------------------------------------- - RetrieveEnclosedData -> - retrieveEnclosedData - - EnclosedDataRetrieved a -> - enclosedDataRetrieved a - - SaveEnclosedData a -> - saveEnclosedData a - - ----------------------------------------- - -- y. Data - ----------------------------------------- - -- The hypaethral user data is received in pieces, - -- pieces which are "cached" here in the web worker. - -- - -- The reasons for this are: - -- 1. Lesser performance penalty on the UI when saving data - -- (ie. this avoids having to encode/decode everything each time) - -- 2. The data can be used in the web worker (brain) as well. - -- (eg. for track-search index) - -- - SaveFavourites a -> - saveFavourites a - - SavePlaylists a -> - savePlaylists a - - SaveProgress a -> - saveProgress a - - SaveSettings a -> - saveSettings a - - SaveSources a -> - saveSources a - - SaveTracks a -> - saveTracks a - - ----------------------------------------- - -- z. Data - ----------------------------------------- - FinishedSyncing -> - finishedSyncing - - GotHypaethralData a -> - gotHypaethralData a - - SaveHypaethralDataBits a -> - saveHypaethralDataBits a - - SaveHypaethralDataSlowly a -> - saveHypaethralDataSlowly a - - ----------------------------------------- - -- z. Secret Key - ----------------------------------------- - RemoveEncryptionKey -> - removeEncryptionKey - - UpdateEncryptionKey a -> - updateEncryptionKey a - - ----------------------------------------- - -- 📭 Other - ----------------------------------------- - RefreshedDropboxTokens a b c -> - refreshedDropboxTokens a b c - - - --- 🔱 - - -commence : Maybe Method -> Url -> ( Json.Value, HypaethralData ) -> Manager -commence maybeMethod initialUrl ( hypaethralJson, hypaethralData ) model = - -- 🚀 - -- Initiated from `initialCommand`. - -- Loaded the used-sync method and the local hypaethral data. - { model | userSyncMethod = maybeMethod } - |> sendHypaethralDataToUI hypaethralJson hypaethralData - |> andThen - (case Url.action initialUrl of - _ -> - sync { initialTask = Nothing } - ) - - -setSyncMethod : Json.Value -> Manager -setSyncMethod json model = - -- 🐤 - -- Set & store method, - -- and retrieve data. - let - decoder = - Decode.map2 - (\a b -> ( a, b )) - (Decode.field "method" <| Decode.map methodFromString Decode.string) - (Decode.field "passphrase" <| Decode.maybe Decode.string) - in - case Decode.decodeValue decoder json of - Ok ( Just method, Just passphrase ) -> - let - initialTask = - passphrase - |> Brain.Task.Ports.fabricateSecretKey - |> Task.mapError TaskPort.errorToStringCustom - in - { model | userSyncMethod = Just method } - |> sync { initialTask = Just initialTask } - |> andThen (saveMethod method) - - Ok ( Just method, Nothing ) -> - { model | userSyncMethod = Just method } - |> sync { initialTask = Nothing } - |> andThen (saveMethod method) - - Ok ( Nothing, _ ) -> - Return.singleton { model | userSyncMethod = Nothing } - - Err _ -> - Return.singleton model - - -sync : { initialTask : Maybe (Task.Task String ()) } -> Manager -sync { initialTask } model = - model - |> syncCommand (Maybe.withDefault (Task.succeed ()) initialTask) - |> return model - |> andThen - (case model.userSyncMethod of - Just method -> - Common.giveUI Alien.StartedSyncing (encodeMethod method) - - Nothing -> - Return.singleton - ) - - -syncCommand : Task.Task String a -> Model -> Cmd Brain.Msg -syncCommand initialTask model = - let - localData = - model.hypaethralUserData - - attemptSync args = - args - |> Syncing.task - initialTask - { localData = localData - , saveLocal = Hypaethral.saveLocal - } - |> Common.attemptTask - (\maybe -> - case maybe of - Just data -> - UserMsg (GotHypaethralData data) - - Nothing -> - UserMsg FinishedSyncing - ) - in - case model.userSyncMethod of - Just (Dropbox { accessToken, expiresAt, refreshToken }) -> - if - Syncing.Services.Dropbox.Token.isExpired - { currentTime = model.currentTime - , expiresAt = expiresAt - } - then - refreshDropboxTokens - model.currentTime - Sync - initialTask - refreshToken - - else - attemptSync - { retrieve = Hypaethral.retrieveDropbox accessToken - , save = Hypaethral.saveDropbox accessToken - } - - Just (Ipfs { apiOrigin }) -> - attemptSync - { retrieve = Hypaethral.retrieveIpfs apiOrigin - , save = Hypaethral.saveIpfs apiOrigin - } - - Just (RemoteStorage args) -> - attemptSync - { retrieve = Hypaethral.retrieveRemoteStorage args - , save = Hypaethral.saveRemoteStorage args - } - - Nothing -> - Cmd.none - - -unsetSyncMethod : Manager -unsetSyncMethod model = - -- 💀 - -- Unset & remove stored method. - [ Common.attemptPortTask (always Brain.Bypass) (Brain.Task.Ports.removeCache Alien.SyncMethod) - , Common.attemptPortTask (always Brain.Bypass) (Brain.Task.Ports.removeCache Alien.SecretKey) - - -- - , case model.userSyncMethod of - Just (Dropbox _) -> - Cmd.none - - Just (Ipfs _) -> - Cmd.none - - Just (RemoteStorage _) -> - Ports.deconstructRemoteStorage () - - Nothing -> - Cmd.none - ] - |> Cmd.batch - |> return { model | userSyncMethod = Nothing } - - - --- 🔱 ░░ DATA - ENCLOSED - - -enclosedDataRetrieved : Json.Value -> Manager -enclosedDataRetrieved json = - Common.giveUI Alien.LoadEnclosedUserData json - - -retrieveEnclosedData : Manager -retrieveEnclosedData = - Decode.value - |> Brain.Task.Ports.fromCache Alien.EnclosedData - |> Common.attemptPortTask - (\maybe -> - case maybe of - Just json -> - Brain.UserMsg (EnclosedDataRetrieved json) - - Nothing -> - Brain.Bypass - ) - |> Return.communicate - - -saveEnclosedData : Json.Value -> Manager -saveEnclosedData json = - json - |> Brain.Task.Ports.toCache Alien.EnclosedData - |> Common.attemptPortTask (always Brain.Bypass) - |> Return.communicate - - - --- 🔱 ░░ DATA - HYPAETHRAL - - -finishedSyncing : Manager -finishedSyncing model = - case model.userSyncMethod of - Just userSyncMethod -> - Common.giveUI Alien.SyncMethod (encodeMethod userSyncMethod) model - - Nothing -> - Return.singleton model - - -gotHypaethralData : HypaethralData -> Manager -gotHypaethralData hypaethralData model = - model - |> sendHypaethralDataToUI (User.encodeHypaethralData hypaethralData) hypaethralData - |> andThen finishedSyncing - - -saveAllHypaethralDataTask : HypaethralData -> Method -> Task String () -saveAllHypaethralDataTask userData method = - let - save = - saveHypaethralDataBitsTask (ModifiedAt :: User.allHypaethralBits) userData - in - case method of - Dropbox { accessToken } -> - save (Hypaethral.saveDropbox accessToken) - - Ipfs { apiOrigin } -> - save (Hypaethral.saveIpfs apiOrigin) - - RemoteStorage a -> - save (Hypaethral.saveRemoteStorage a) - - -saveHypaethralDataBitsTask : List HypaethralBit -> HypaethralData -> (HypaethralBit -> Json.Value -> Task String ()) -> Task String () -saveHypaethralDataBitsTask bits userData saveFn = - [ -------- - -- LOCAL - -------- - List.map - (\bit -> - Hypaethral.saveLocal bit (encodeHypaethralBit bit userData) - ) - bits - , --------- - -- REMOTE - --------- - List.map - (\bit -> - saveFn bit (encodeHypaethralBit bit userData) - ) - bits - ] - |> List.concat - |> List.foldl - (\nextTask -> Task.andThen (\_ -> nextTask)) - (Task.succeed ()) - - -{-| Save different parts of hypaethral data, -one part at a time. --} -saveHypaethralDataBits : List HypaethralBit -> Manager -saveHypaethralDataBits bitsWithoutModifiedAt model = - let - bits = - ModifiedAt :: bitsWithoutModifiedAt - - userData = - model.hypaethralUserData - - updatedUserData = - { userData | modifiedAt = Just model.currentTime } - - updatedModel = - { model | hypaethralUserData = updatedUserData } - - save saveFn = - Time.now - |> Task.andThen - (\currentTime -> - saveHypaethralDataBitsTask - bits - { updatedUserData | modifiedAt = Just currentTime } - saveFn - ) - |> Common.attemptTask (always Brain.Bypass) - |> return updatedModel - in - case model.userSyncMethod of - Just (Dropbox { accessToken, expiresAt, refreshToken }) -> - if - Syncing.Services.Dropbox.Token.isExpired - { currentTime = model.currentTime - , expiresAt = expiresAt - } - then - refreshToken - |> refreshDropboxTokens - model.currentTime - (SaveHypaethralDataBits bits) - (Task.succeed ()) - |> return model - - else - save (Hypaethral.saveDropbox accessToken) - - Just (Ipfs { apiOrigin }) -> - save (Hypaethral.saveIpfs apiOrigin) - - Just (RemoteStorage args) -> - save (Hypaethral.saveRemoteStorage args) - - Nothing -> - -- Only save locally - save (\_ _ -> Task.succeed ()) - - -saveHypaethralDataBitWithDebounce : HypaethralBit -> Manager -saveHypaethralDataBitWithDebounce bit = - bit - |> Debouncer.provideInput - |> saveHypaethralDataSlowly - - -saveHypaethralDataSlowly : Debouncer.Msg HypaethralBit -> Manager -saveHypaethralDataSlowly debouncerMsg model = - let - ( m, c, e ) = - Debouncer.update debouncerMsg model.hypaethralDebouncer - - bits = - e - |> Maybe.withDefault [] - |> EverySet.fromList - |> EverySet.toList - in - c - |> Cmd.map (SaveHypaethralDataSlowly >> UserMsg) - |> return { model | hypaethralDebouncer = m } - |> (if not (List.isEmpty bits) then - andThen (saveHypaethralDataBits bits) - - else - identity - ) - - -sendHypaethralDataToUI : Json.Value -> HypaethralData -> Manager -sendHypaethralDataToUI encodedData decodedData model = - [ encodedData - |> Alien.broadcast Alien.LoadHypaethralUserData - |> Ports.toUI - - -- - , decodedData.tracks - |> Json.list Tracks.encodeTrack - |> Ports.updateSearchIndex - ] - |> Cmd.batch - |> return { model | hypaethralUserData = decodedData } - - - --- 🔱 ░░ DATA - HYPAETHRAL BITS - - -saveFavourites : Json.Value -> Manager -saveFavourites value model = - value - |> Decode.decodeValue (Decode.list Tracks.favouriteDecoder) - |> Result.withDefault model.hypaethralUserData.favourites - |> hypaethralLenses.setFavourites model - |> saveHypaethralDataBitWithDebounce Favourites - - -savePlaylists : Json.Value -> Manager -savePlaylists value model = - value - |> Decode.decodeValue (Decode.list Playlists.decoder) - |> Result.withDefault model.hypaethralUserData.playlists - |> hypaethralLenses.setPlaylists model - |> saveHypaethralDataBitWithDebounce Playlists - - -saveProgress : Json.Value -> Manager -saveProgress value model = - value - |> Decode.decodeValue (Decode.dict Decode.float) - |> Result.withDefault model.hypaethralUserData.progress - |> hypaethralLenses.setProgress model - |> saveHypaethralDataBitWithDebounce Progress - - -saveSettings : Json.Value -> Manager -saveSettings value model = - value - |> Decode.decodeValue (Decode.map Just Settings.decoder) - |> Result.withDefault model.hypaethralUserData.settings - |> hypaethralLenses.setSettings model - |> saveHypaethralDataBitWithDebounce Settings - - -saveSources : Json.Value -> Manager -saveSources value model = - value - |> Decode.decodeValue (Decode.list Sources.decoder) - |> Result.withDefault model.hypaethralUserData.sources - |> hypaethralLenses.setSources model - |> saveHypaethralDataBitWithDebounce Sources - - -saveTracks : Json.Value -> Manager -saveTracks value model = - saveTracksAndUpdateSearchIndex - (value - |> Decode.decodeValue (Decode.list Tracks.trackDecoder) - |> Result.withDefault model.hypaethralUserData.tracks - ) - model - - -saveTracksAndUpdateSearchIndex : List Track -> Manager -saveTracksAndUpdateSearchIndex tracks model = - tracks - -- Store in model - |> hypaethralLenses.setTracks model - -- Update search index - |> Return.communicate - (tracks - |> Json.list Tracks.encodeTrack - |> Ports.updateSearchIndex - ) - -- Save with delay - |> andThen (saveHypaethralDataBitWithDebounce Tracks) - - - --- 🔱 ░░ DATA - HYPAETHRAL LENSES - - -hypaethralLenses = - { setFavourites = makeHypaethralLens (\h f -> { h | favourites = f }) - , setPlaylists = makeHypaethralLens (\h p -> { h | playlists = p }) - , setProgress = makeHypaethralLens (\h p -> { h | progress = p }) - , setSettings = makeHypaethralLens (\h s -> { h | settings = s }) - , setSources = makeHypaethralLens (\h s -> { h | sources = s }) - , setTracks = makeHypaethralLens (\h t -> { h | tracks = t }) - } - - -makeHypaethralLens : (HypaethralData -> a -> HypaethralData) -> Model -> a -> Model -makeHypaethralLens setter model value = - { model | hypaethralUserData = setter model.hypaethralUserData value } - - - --- 🔱 ░░ METHOD - - -saveMethod : Method -> Manager -saveMethod method model = - method - |> encodeMethod - |> Brain.Task.Ports.toCache Alien.SyncMethod - |> Common.attemptPortTask (always Brain.Bypass) - |> return { model | userSyncMethod = Just method } - - - --- 🔱 ░░ SECRET KEY - - -removeEncryptionKey : Manager -removeEncryptionKey model = - Alien.SecretKey - |> Brain.Task.Ports.removeCache - |> Task.mapError TaskPort.errorToStringCustom - |> Task.andThen (\_ -> Time.now) - |> Task.andThen - (\currentTime -> - case model.userSyncMethod of - Just method -> - let - data = - model.hypaethralUserData - in - saveAllHypaethralDataTask { data | modifiedAt = Just currentTime } method - - Nothing -> - Task.succeed () - ) - |> Common.attemptTask (always Brain.Bypass) - |> return model - - -updateEncryptionKey : Json.Value -> Manager -updateEncryptionKey json model = - case Decode.decodeValue Decode.string json of - Ok passphrase -> - passphrase - |> Brain.Task.Ports.fabricateSecretKey - |> Task.mapError TaskPort.errorToStringCustom - |> Task.andThen (\_ -> Time.now) - |> Task.andThen - (\currentTime -> - case model.userSyncMethod of - Just method -> - let - data = - model.hypaethralUserData - in - saveAllHypaethralDataTask { data | modifiedAt = Just currentTime } method - - Nothing -> - Task.succeed () - ) - |> Common.attemptTask (always Brain.Bypass) - |> return model - - Err _ -> - Return.singleton model - - - --- 📭 ░░ OTHER - - -refreshDropboxTokens : Time.Posix -> User.Msg -> Task.Task String a -> String -> Cmd Brain.Msg -refreshDropboxTokens currentTime msg initialTask refreshToken = - initialTask - |> Task.andThen - (\_ -> Dropbox.refreshAccessToken refreshToken) - |> Task.attempt - (\result -> - case result of - Ok tokens -> - msg - |> RefreshedDropboxTokens - { currentTime = Time.posixToMillis currentTime // 1000 - , refreshToken = refreshToken - } - tokens - |> UserMsg - - Err err -> - Common.reportUICmdMsg Alien.ReportError err - ) - - -refreshedDropboxTokens : - { currentTime : Int, refreshToken : String } - -> Dropbox.Tokens - -> User.Msg - -> Manager -refreshedDropboxTokens { currentTime, refreshToken } tokens msg model = - { accessToken = tokens.accessToken - , expiresAt = currentTime + tokens.expiresIn - , refreshToken = refreshToken - } - |> Dropbox - |> (\m -> saveMethod m model) - |> andThen (update msg) diff --git a/src/Core/Brain/User/Types.elm b/src/Core/Brain/User/Types.elm deleted file mode 100644 index a03a76db9..000000000 --- a/src/Core/Brain/User/Types.elm +++ /dev/null @@ -1,52 +0,0 @@ -module Brain.User.Types exposing (..) - -import Debouncer.Basic as Debouncer -import Json.Decode as Json -import Url exposing (Url) -import User.Layer as User exposing (HypaethralBit, HypaethralData) -import User.Layer.Methods.Dropbox as Dropbox - - - --- 📣 - - -type Msg - = Commence (Maybe User.Method) Url ( Json.Value, HypaethralData ) - ----------------------------------------- - -- Method - ----------------------------------------- - | SetSyncMethod Json.Value - | Sync - | UnsetSyncMethod - ----------------------------------------- - -- Enclosed Data - ----------------------------------------- - | RetrieveEnclosedData - | EnclosedDataRetrieved Json.Value - | SaveEnclosedData Json.Value - ----------------------------------------- - -- Hypaethral Data, pt. 1 - ----------------------------------------- - | SaveFavourites Json.Value - | SavePlaylists Json.Value - | SaveProgress Json.Value - | SaveSettings Json.Value - | SaveSources Json.Value - | SaveTracks Json.Value - ----------------------------------------- - -- Hypaethral Data, pt. 2 - ----------------------------------------- - | FinishedSyncing - | GotHypaethralData HypaethralData - | SaveHypaethralDataBits (List HypaethralBit) - | SaveHypaethralDataSlowly (Debouncer.Msg HypaethralBit) - ----------------------------------------- - -- Encryption - ----------------------------------------- - | RemoveEncryptionKey - | UpdateEncryptionKey Json.Value - ----------------------------------------- - -- 📭 Other - ----------------------------------------- - | RefreshedDropboxTokens { currentTime : Int, refreshToken : String } Dropbox.Tokens Msg diff --git a/src/Core/Themes/Sunrise/Alfred/View.elm b/src/Core/Themes/Sunrise/Alfred/View.elm deleted file mode 100644 index 0c2bc804e..000000000 --- a/src/Core/Themes/Sunrise/Alfred/View.elm +++ /dev/null @@ -1,281 +0,0 @@ -module Themes.Sunrise.Alfred.View exposing (view) - -import Alfred exposing (..) -import Chunky exposing (..) -import Color exposing (Color) -import Conditional exposing (ifThenElse) -import Html exposing (Html, text) -import Html.Attributes exposing (attribute, autofocus, id, placeholder, style, type_) -import Html.Events exposing (onInput) -import Html.Ext exposing (onTapStopPropagation) -import Json.Decode -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import UI.Types as UI - - - --- 🗺 - - -view : Maybe (Alfred UI.Msg) -> Maybe Color -> Html UI.Msg -view maybeInstance extractedBackdropColor = - let - bgColor = - Maybe.unwrap "inherit" Color.toCssString extractedBackdropColor - in - case maybeInstance of - Just instance -> - let - hasResults = - List.sum (List.map (.items >> List.length) instance.results) > 0 - in - chunk - [ "inset-0" - , "flex" - , "flex-col" - , "fixed" - , "items-center" - , "px-3" - , "cursor-pointer" - , "z-50" - ] - [ ----------------------------------------- - -- Message - ----------------------------------------- - chunk - [ "italic" - , "leading-normal" - , "mt-12" - , "opacity-75" - , "text-center" - , "text-white" - - -- Dark mode - ------------ - , "dark:text-base07" - ] - [ text instance.message ] - - ----------------------------------------- - -- Search - ----------------------------------------- - , brick - [ Html.Events.custom - "tap" - (Json.Decode.succeed - { message = UI.Bypass - , stopPropagation = True - , preventDefault = True - } - ) - ] - [ "text-sm" - , "max-w-xl" - , "mt-8" - , "w-full" - ] - [ slab - Html.input - [ autofocus True - , id "diffuse__alfred" - , onInput UI.GotAlfredInput - , type_ "text" - - -- - , attribute "spellcheck" "false" - - -- - , case instance.operation of - Query -> - placeholder "Type to search" - - QueryOrMutation -> - placeholder "Type to search or create" - - Mutation -> - placeholder "Type to create" - ] - [ "border" - , "bg-white" - , "block" - , "leading-normal" - , "opacity-95" - , "outline-none" - , "p-4" - , "rounded-t" - , "shadow-md" - , "text-xl" - , "tracking-tad-closer" - , "w-full" - - -- - , if not hasResults then - "rounded-b" - - else - "" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:border-base00" - ] - [] - ] - - ----------------------------------------- - -- Results - ----------------------------------------- - , brick - [ id "alfred__results" ] - [ "bg-white" - , "border-t-0" - , "leading-tight" - , "max-w-xl" - , "mb-32" - , "opacity-95" - , "overflow-x-hidden" - , "overflow-y-auto" - , "rounded-b" - , "shadow-md" - , "smooth-scrolling" - , "text-nearly-sm" - , "w-full" - - -- - , ifThenElse hasResults "border" "" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:border-base00" - ] - (instance.results - |> List.foldl - (\group ( acc, indexBase ) -> - case List.length group.items of - 0 -> - ( acc, indexBase ) - - x -> - ( groupView bgColor instance group indexBase :: acc - , indexBase + x - ) - ) - ( [], 0 ) - |> Tuple.first - |> List.reverse - ) - ] - - Nothing -> - nothing - - -groupView bgColor instance group indexBase = - raw - [ case group.name of - Just name -> - chunk - [ "all-small-caps" - , "antialiased" - , "font-semibold" - , "leading-tight" - , "mb-2" - , "mx-2" - , "mt-5" - , "opacity-60" - , "px-3" - , "text-sm" - , "tracking-wider" - ] - [ Html.text name ] - - Nothing -> - Html.text "" - , raw - (List.indexedMap - (\i -> itemView bgColor instance <| indexBase + i) - group.items - ) - ] - - -itemView bgColor instance idx item = - brick - [ onTapStopPropagation (UI.SelectAlfredItem idx) - - -- - , if idx == instance.focus then - id "alfred__results__focus" - - else - id ("alfred__results__" ++ String.fromInt idx) - - -- - , if idx == instance.focus then - style "background-color" bgColor - - else - style "" "" - ] - (List.concat - [ [ "flex" - , "items-center" - , "m-2" - , "p-3" - , "relative" - , "rounded" - ] - - -- - , if idx == instance.focus then - [ "text-white" - , "dark:opacity-80" - , "dark:text-base07" - ] - - else - [ "text-inherit" ] - - -- - -- , if modBy 2 idx == 0 then - -- [] - -- else - -- [ "bg-gray-100", "dark:bg-base01-15" ] - ] - ) - [ case item.icon of - Just icon -> - slab - Html.span - [] - [ "inline-block" - , "mr-2" - , "w-5" - ] - [ icon Inherit - ] - - Nothing -> - text "" - - -- - , slab - Html.span - [] - [ "flex-1", "inline-block", "pt-px" ] - [ text item.title ] - - -- - , if idx == instance.focus then - chunk - [ "leading-0", "ml-2" ] - [ Icons.keyboard_return 13 Inherit - ] - - else - nothing - ] diff --git a/src/Core/Themes/Sunrise/Console.elm b/src/Core/Themes/Sunrise/Console.elm deleted file mode 100644 index 5629bccab..000000000 --- a/src/Core/Themes/Sunrise/Console.elm +++ /dev/null @@ -1,312 +0,0 @@ -module Themes.Sunrise.Console exposing (view) - -import Chunky exposing (..) -import Conditional exposing (..) -import Html exposing (Html, text) -import Html.Attributes exposing (style, title) -import Html.Events exposing (on, onClick) -import Json.Decode as Decode -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import UI.Audio.Types exposing (AudioLoadingState(..), NowPlaying, nowPlayingIdentifiedTrack) -import UI.Queue.Types as Queue -import UI.Tracks.Types as Tracks -import UI.Types exposing (Msg(..)) - - - --- 🗺 - - -view : - Maybe NowPlaying - -> Bool - -> Bool - -> Html Msg -view nowPlaying repeat shuffle = - chunk - [ "antialiased" - , "mt-1" - , "text-center" - , "w-full" - - -- - , "lg:max-w-insulation" - ] - [ ----------------------------------------- - -- Now Playing - ----------------------------------------- - chunk - [ "text-sm" - , "italic" - , "leading-normal" - , "py-4" - , "text-white" - ] - [ case Maybe.map .loadingState nowPlaying of - Nothing -> - text "Diffuse" - - Just Loading -> - text "Loading track ..." - - Just Loaded -> - case Maybe.map nowPlayingIdentifiedTrack nowPlaying of - Just ( _, { tags } ) -> - slab - Html.span - [ onClick (TracksMsg Tracks.ScrollToNowPlaying) - , title "Scroll to track" - ] - [ "cursor-pointer" ] - [ case tags.artist of - Just artist -> - text (artist ++ " - " ++ tags.title) - - Nothing -> - text tags.title - ] - - Nothing -> - text "Diffuse" - - ----------------------------------------- - -- Errors - ----------------------------------------- - Just DecodeError -> - text "(!) An error occurred while decoding the audio" - - Just NetworkError -> - text "Waiting until your internet connection comes back online ..." - - Just NotSupportedError -> - text "(!) Your browser does not support playing this type of audio" - - -- Just NotSupportedOrMissing -> - -- text "The audio is missing or is in a format not supported by your browser." - ] - - ----------------------------------------- - -- Progress Bar - ----------------------------------------- - , let - maybeDuration = - Maybe.andThen .duration nowPlaying - - maybePosition = - Maybe.map .playbackPosition nowPlaying - - progress = - case ( maybeDuration, maybePosition ) of - ( Just duration, Just position ) -> - if duration <= 0 then - 0 - - else - (position / duration) - |> (*) 100 - |> min 100 - |> max 0 - - _ -> - 0 - in - brick - (case nowPlaying of - Just { item } -> - item.identifiedTrack - |> Tuple.second - |> .id - |> (\id -> - \float -> Seek { progress = float, trackId = id } - ) - |> clickLocationDecoder - |> on "click" - |> List.singleton - - Nothing -> - [] - ) - [ "cursor-pointer" - , "py-1" - ] - [ brick - [ style "background-color" "rgba(255, 255, 255, 0.25)" - , style "height" "3px" - ] - [ "rounded-sm" - , "select-none" - ] - [ brick - [ style "background-color" "rgba(255, 255, 255, 0.325)" - , style "height" "3px" - , style "width" (String.fromFloat progress ++ "%") - ] - [ "progressBarValue" - , "rounded-sm" - ] - [] - ] - ] - - ----------------------------------------- - -- Buttons - ----------------------------------------- - , chunk - [ "flex" - , "justify-between" - , "mb-3" - , "mt-4" - , "select-none" - , "text-white-90" - - -- - , "sm:justify-center" - ] - [ button "Toggle repeat" - (smallLight repeat) - (icon Icons.repeat 18) - (QueueMsg Queue.ToggleRepeat) - - -- - , button - "Play previous track" - lightPlaceHolder - (icon Icons.fast_rewind 20) - (QueueMsg Queue.Rewind) - - -- - , let - isPlaying = - Maybe.unwrap False .isPlaying nowPlaying - in - button - "" - (largeLight isPlaying) - play - TogglePlay - - -- - , button - "Play next track" - lightPlaceHolder - (icon Icons.fast_forward 20) - (QueueMsg Queue.Shift) - - -- - , button - "Toggle shuffle" - (smallLight shuffle) - (icon Icons.shuffle 18) - (QueueMsg Queue.ToggleShuffle) - ] - ] - - -button : String -> Html msg -> Html msg -> msg -> Html msg -button t light content msg = - brick - [ onClick msg - , title t - ] - [ "cursor-pointer" - , "flex" - , "flex-col" - , "items-center" - , "px-1" - - -- - , "sm:mx-8" - ] - [ brick - [ style "height" "4px" ] - [] - [ light ] - , brick - [ style "height" "25px" ] - [ "flex" - , "items-center" - , "my-2" - ] - [ content ] - ] - - -smallLight : Bool -> Html msg -smallLight isOn = - brick - [ style "height" "4px" - , style "width" "4px" - - -- - , style "background-color" <| - ifThenElse - isOn - "rgb(157, 174, 255)" - "rgba(255, 255, 255, 0.25)" - ] - [ "rounded-full" ] - [] - - -largeLight : Bool -> Html msg -largeLight isOn = - brick - [ style "height" "4px" - , style "left" "-2px" - , style "width" "17px" - - -- - , style "background-color" <| - ifThenElse - isOn - "rgb(198, 254, 153)" - "rgba(255, 255, 255, 0.25)" - ] - [ "relative", "rounded-full" ] - [] - - -lightPlaceHolder : Html msg -lightPlaceHolder = - Html.div - [ style "height" "4px" ] - [] - - -play : Html msg -play = - brick - [ style "font-size" "11.25px" - , style "letter-spacing" "3.75px" - ] - [ "font-bold" - , "font-display" - , "relative" - , "whitespace-nowrap" - ] - [ text "PLAY" ] - - - --- ⚗️ - - -icon : (Int -> Coloring -> Html msg) -> Int -> Html msg -icon iconFunction int = - iconFunction int Inherit - - - --- EVENTS - - -clickLocationDecoder : (Float -> msg) -> Decode.Decoder msg -clickLocationDecoder message = - Decode.map message - (Decode.map2 - (\a b -> a / b) - (Decode.at [ "offsetX" ] Decode.float) - (Decode.at [ "currentTarget", "clientWidth" ] Decode.float) - ) diff --git a/src/Core/Themes/Sunrise/ContextMenu.elm b/src/Core/Themes/Sunrise/ContextMenu.elm deleted file mode 100644 index 777675e80..000000000 --- a/src/Core/Themes/Sunrise/ContextMenu.elm +++ /dev/null @@ -1,131 +0,0 @@ -module Themes.Sunrise.ContextMenu exposing (view) - -import Chunky exposing (..) -import Conditional exposing (..) -import ContextMenu exposing (..) -import Html exposing (Html, text) -import Html.Attributes exposing (style) -import Html.Events exposing (custom) -import Json.Decode -import Material.Icons.Types exposing (Coloring(..)) -import UI.Types as UI exposing (Msg) - - - --- 🗺 - - -view : Float -> Maybe (ContextMenu Msg) -> Html Msg -view viewportWidth m = - case m of - Just (ContextMenu items coordinates) -> - let - ( height, width ) = - ( 250, 170 ) - - left = - coordinates.x - |> max (width / 2 + 12) - |> min (viewportWidth - width / 2 - 12) - - top = - coordinates.y - |> max (height / 2 + 12) - in - brick - [ style "left" (String.fromFloat left ++ "px") - , style "top" (String.fromFloat top ++ "px") - - -- - , style "min-width" "170px" - ] - [ "absolute" - , "bg-white" - , "leading-loose" - , "opacity-95" - , "overflow-hidden" - , "-translate-x-1/2" - , "-translate-y-1/2" - , "rounded" - , "shadow-md" - , "select-none" - , "text-almost-sm" - , "transform" - , "z-50" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:border" - , "dark:border-base00" - ] - (List.map - (\item -> - case item of - Item i -> - itemView i - - Divider -> - -- NOTE: Not needed at the moment - nothing - ) - items - ) - - Nothing -> - nothing - - -itemView : ContextMenu.ItemProperties Msg -> Html Msg -itemView { icon, label, msg, active } = - brick - [ custom - "tap" - (Json.Decode.succeed - { message = UI.MsgViaContextMenu msg - , stopPropagation = True - , preventDefault = True - } - ) - ] - [ "border-b" - , "cursor-pointer" - , "pl-4" - , "pr-8" - , "py-3" - , "truncate" - - -- - , "last:border-transparent" - - -- - , ifThenElse active "antialiased" "subpixel-antialiased" - , ifThenElse active "border-transparent" "border-gray-200" - , ifThenElse active "bg-base00" "bg-transparent" - , ifThenElse active "text-white" "text-inherit" - , ifThenElse active "font-semibold" "font-normal" - - -- Dark mode - ------------ - , "dark:border-base00" - - -- - , ifThenElse active "dark:bg-base07" "dark:bg-transparent" - , ifThenElse active "dark:text-darkest-hour" "dark:text-inherit" - ] - [ inline - [ "align-middle" - , "inline-block" - , "leading-0" - ] - [ icon 14 Inherit ] - , inline - [ "align-middle" - , "inline-block" - , "leading-0" - , "ml-2" - , "pl-1" - , "relative" - ] - [ text label ] - ] diff --git a/src/Core/Themes/Sunrise/Kit.elm b/src/Core/Themes/Sunrise/Kit.elm deleted file mode 100644 index bf7e19961..000000000 --- a/src/Core/Themes/Sunrise/Kit.elm +++ /dev/null @@ -1,704 +0,0 @@ -module Themes.Sunrise.Kit exposing (..) - -import Chunky exposing (..) -import Color -import Conditional exposing (ifThenElse) -import Html exposing (Html) -import Html.Attributes as A exposing (href, style) -import Html.Events exposing (onClick, onInput) -import Icons exposing (Icon) -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Svg - - - --- COLORS - - -colorKit = - { base00 = rgb 45 45 45 - , base01 = rgb 63 63 63 - , base02 = rgb 79 79 79 - , base03 = rgb 119 119 119 - , base04 = rgb 140 140 140 - , base05 = rgb 163 163 163 - , base06 = rgb 186 186 186 - , base07 = rgb 232 232 232 - , base08 = rgb 239 97 85 - , base09 = rgb 249 155 21 - , base0A = rgb 254 196 24 - , base0B = rgb 72 182 133 - , base0C = rgb 91 196 191 - , base0D = rgb 6 182 239 - , base0E = rgb 129 91 164 - , base0F = rgb 233 107 168 - } - - -colors = - { -- States - success = colorKit.base0B - , error = colorKit.base08 - , warning = colorKit.base0A - - -- Gray - , gray_100 = Color.hsl 0 0 0.988 - , gray_200 = Color.hsl 0 0 0.973 - , gray_300 = Color.hsl 0 0 0.933 - , gray_400 = Color.hsl 0 0 0.882 - , gray_500 = Color.hsl 0 0 0.863 - , gray_600 = Color.hsl 0 0 0.776 - - -- Other - , background = rgb 2 7 14 - , selection = colorKit.base04 - , text = colorKit.base01 - } - - -rgb = - Color.rgb255 - - - --- 🍱 ░░ BUTTON - - -type ButtonColor - = Accent - | Blank - | Gray - - -type ButtonType - = Filled - | IconOnly - | Normal - - -button : ButtonType -> msg -> Html msg -> Html msg -button = - buttonWithColor Gray - - -buttonLink : String -> ButtonType -> Html msg -> Html msg -buttonLink theHref buttonType = - buttonWithOptions Html.a [ href theHref ] Accent buttonType Nothing - - -buttonLinkWithColor : ButtonColor -> String -> ButtonType -> Html msg -> Html msg -buttonLinkWithColor color theHref buttonType = - buttonWithOptions Html.a [ href theHref ] color buttonType Nothing - - -buttonWithColor : ButtonColor -> ButtonType -> msg -> Html msg -> Html msg -buttonWithColor color buttonType msg = - buttonWithOptions Html.button [] color buttonType (Just msg) - - -buttonWithOptions : - (List (Html.Attribute msg) -> List (Html msg) -> Html msg) - -> List (Html.Attribute msg) - -> ButtonColor - -> ButtonType - -> Maybe msg - -> Html msg - -> Html msg -buttonWithOptions tag attributes buttonColor buttonType maybeMsg child = - let - defaultClasses = - [ "antialiased" - , "border-2" - , "cursor-pointer" - , "font-semibold" - , "inline-block" - , "leading-relaxed" - , "no-underline" - , "py-2" - , "px-4" - , "rounded" - , "text-center" - , "text-sm" - - -- - , "fixate:bg-white" - , "fixate:border-black" - , "fixate:text-black" - ] - - specificClasses = - case buttonType of - Filled -> - case buttonColor of - Accent -> - [ "bg-accent-btn" - , "border-transparent" - , "text-white-90" - ] - - Blank -> - [ "bg-white" - , "border-transparent" - , "text-accent-light" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:text-accent-dark" - ] - - Gray -> - [ "bg-base04" - , "border-transparent" - , "text-white" - - -- Dark mode - ------------ - , "dark:bg-base05" - ] - - _ -> - case buttonColor of - Accent -> - [ "bg-transparent" - , "border-accent-btn" - , "text-accent-btn" - ] - - Blank -> - [ "bg-transparent" - , "border-white" - , "text-white" - ] - - Gray -> - [ "bg-transparent" - , "border-base04" - , "text-base04" - - -- Dark mode - ------------ - , "dark:border-base05" - , "dark:text-base05" - ] - in - slab - tag - (case maybeMsg of - Just msg -> - attributes ++ [ onClick msg ] - - Nothing -> - attributes - ) - (List.append - defaultClasses - specificClasses - ) - [ case buttonType of - IconOnly -> - inline - [ "align-middle" - , "inline-block" - , "leading-none" - , "pointer-events-none" - , "text-0" - ] - [ child ] - - _ -> - inline - [ "align-middle" - , "inline-block" - , "leading-none" - , "pointer-events-none" - ] - [ child ] - ] - - - --- 🍱 ░░ OTHER - - -askForInput : { question : String, info : List (Html msg) } -> Html msg -askForInput { question, info } = - Html.span - [] - [ chunk - [ "font-semibold", "pt-1" ] - [ Html.text question ] - , case info of - [] -> - Html.text "" - - _ -> - chunk - [ "italic", "mt-2", "text-sm" ] - info - ] - - -canister : List (Html msg) -> Html msg -canister children = - chunk - [ "mx-1", "px-4", "pb-4" ] - children - - -canisterForm : List (Html msg) -> Html msg -canisterForm children = - chunk - [ "mx-1", "px-4", "pb-4", "w-full" ] - children - - -centeredContent : List (Html msg) -> Html msg -centeredContent children = - chunk - [ "flex" - , "flex-grow" - , "items-stretch" - , "overflow-hidden" - , "relative" - ] - [ Html.map never logoBackdrop - , chunk - [ "flex" - , "flex-col" - , "flex-grow" - , "items-center" - , "justify-center" - , "max-w-full" - , "relative" - , "z-10" - ] - children - ] - - -checkbox : { checked : Bool, toggleMsg : msg } -> Html msg -checkbox opts = - brick - [ onClick opts.toggleMsg - , style "left" "-3px" - ] - [ "inline-block", "cursor-pointer", "relative" ] - [ if opts.checked then - Icons.check_box 22 Inherit - - else - Icons.check_box_outline_blank 22 Inherit - ] - - -focusScreen : { icon : Icon msg, iconHref : Maybe String, text : List (Html msg), textHref : Maybe String } -> List (Html msg) -> Html msg -focusScreen { icon, iconHref, text, textHref } nodes = - [ slab - (case iconHref of - Just _ -> - Html.a - - Nothing -> - Html.div - ) - (case iconHref of - Just h -> - [ href h ] - - Nothing -> - [] - ) - [ "block" - , "opacity-30" - , "text-inherit" - ] - [ icon 64 Inherit ] - , slab - (case iconHref of - Just _ -> - Html.a - - Nothing -> - Html.div - ) - (case textHref of - Just h -> - [ href h ] - - Nothing -> - [] - ) - [ "block" - , "leading-normal" - , "mt-2" - , "opacity-40" - , "text-center" - , "text-inherit" - ] - text - , chunk - [ "max-w-full" - , "mt-4" - ] - nodes - ] - |> chunk - [ "flex" - , "flex-col" - , "items-center" - , "max-h-full" - , "overflow-y-auto" - , "px-4" - , "py-8" - , "w-full" - ] - |> List.singleton - |> centeredContent - - -h1 : String -> Html msg -h1 text = - slab - Html.h1 - [ style "font-size" "13.5px" ] - [ "all-small-caps" - , "antialiased" - , "bg-base06" - , "inline-block" - , "font-semibold" - , "leading-tight" - , "m-0" - , "-top-px" - , "overflow-hidden" - , "pointer-events-none" - , "px-2" - , "py-1" - , "relative" - , "rounded-b" - , "uppercase" - , "text-sm" - , "text-white" - - -- Dark mode - ------------ - , "dark:bg-base01" - , "dark:text-base05" - ] - [ Html.text text ] - - -h2 : String -> Html msg -h2 text = - slab - Html.h2 - [] - [ "font-bold" - , "font-display" - , "leading-tight" - , "mb-8" - , "mt-4" - , "mx-auto" - , "text-2xl" - , "text-center" - ] - [ Html.text text ] - - -h3 : String -> Html msg -h3 text = - slab - Html.h2 - [] - [ "antialiased" - , "font-bold" - , "font-display" - , "leading-tight" - , "mb-8" - , "mt-4" - , "text-xl" - ] - [ Html.text text ] - - -inlineIcon : (Int -> Coloring -> Svg.Svg msg) -> Html msg -inlineIcon icon = - inline - [ "align-sub" - , "inline-block" - , "leading-0" - , "mr-1" - , "text-0" - ] - [ icon 14 Inherit ] - - -intro : Html msg -> Html msg -intro child = - slab - Html.p - [ style "line-height" "1.75" ] - [ "mb-6" - , "mt-3" - , "text-base05" - , "text-sm" - - -- Dark mode - ------------ - , "dark:text-base03" - ] - [ child ] - - -label : List (Html.Attribute msg) -> String -> Html msg -label attributes t = - slab - Html.label - (style "font-size" "11.25px" :: attributes) - [ "antialiased" - , "block" - , "font-bold" - , "leading-normal" - , "opacity-90" - , "uppercase" - ] - [ Html.text t ] - - -link : { label : String, url : String } -> Html msg -link params = - slab - Html.a - [ A.href params.url - , A.target "_blank" - ] - [ "border-b-2" - , "border-base04" - , "inline-block" - , "leading-none" - , "no-underline" - , "text-inherit" - ] - [ Html.text params.label ] - - -logoBackdrop : Html Never -logoBackdrop = - chunk - [ "logo-backdrop" - - -- - , "absolute" - , "bg-cover" - , "bg-no-repeat" - , "h-0" - , "left-full" - , "opacity-025" - , "pt-full" - , "top-0" - , "z-0" - - -- Dark mode - ------------ - , "dark:opacity-40" - ] - [] - - -receptacle : { scrolling : Bool } -> List (Html msg) -> Html msg -receptacle { scrolling } = - chunk - [ "absolute" - , "bg-white" - , "flex" - , "flex-col" - , "inset-0" - , "overflow-x-hidden" - , "scrolling-touch" - , "z-50" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - - -- - , ifThenElse scrolling "overflow-y-auto" "overflow-y-hidden" - ] - - -select : (String -> msg) -> List (Html msg) -> Html msg -select inputHandler options = - chunk - [ "max-w-md" - , "mx-auto" - , "relative" - , "text-base05" - , "w-full" - - -- - , "focus-within:text-black" - - -- Dark mode - ------------ - , "dark:text-gray-600" - , "dark:focus-within:text-base07" - ] - [ slab - Html.select - [ onInput inputHandler ] - [ "appearance-none" - , "border-b" - , "border-l-0" - , "border-r-0" - , "border-t-0" - , "border-gray-400" - , "bg-transparent" - , "block" - , "leading-normal" - , "m-0" - , "outline-none" - , "py-2" - , "px-0" - , "rounded-none" - , "text-base01" - , "text-lg" - , "w-full" - - -- - , "focus:border-black" - - -- Dark mode - ------------ - , "dark:border-base02" - , "dark:text-gray-600" - - -- - , "dark:focus:border-base07" - ] - options - , chunk - [ "absolute" - , "-translate-y-1/2" - , "mt-px" - , "right-0" - , "text-0" - , "top-1/2" - , "transform" - ] - [ Icons.keyboard_arrow_down 20 Inherit ] - ] - - -textArea : List (Html.Attribute msg) -> Html msg -textArea attributes = - slab - Html.textarea - attributes - [ "bg-white" - , "block" - , "leading-normal" - , "mb-4" - , "p-4" - , "resize-none" - , "rounded" - , "text-base01" - , "text-sm" - , "w-full" - - -- - , "placeholder:text-base01" - , "placeholder:text-opacity-40" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:text-gray-600" - - -- - , "dark:placeholder:text-gray-600" - , "dark:placeholder:text-opacity-30" - ] - [] - - -textButton : { label : String, onClick : msg } -> Html msg -textButton params = - slab - Html.button - [ onClick params.onClick ] - [ "appearance-none" - , "bg-transparent" - , "border-base04" - , "border-b-2" - , "text-inherit" - , "leading-tight" - , "m-0" - , "p-0" - , "cursor-pointer" - ] - [ Html.text params.label ] - - -textField : List (Html.Attribute msg) -> Html msg -textField attributes = - slab - Html.input - attributes - [ "appearance-none" - , "border-b" - , "border-l-0" - , "border-r-0" - , "border-t-0" - , "border-gray-400" - , "bg-transparent" - , "block" - , "leading-normal" - , "mt-1" - , "py-2" - , "rounded-none" - , "text-base01" - , "text-sm" - , "w-full" - - -- - , "focus:border-black" - , "focus:outline-none" - , "placeholder:text-base01" - , "placeholder:text-opacity-40" - - -- Dark mode - ------------ - , "dark:border-base02" - , "dark:text-gray-600" - - -- - , "dark:focus:border-base07" - , "dark:placeholder:text-gray-600" - , "dark:placeholder:text-opacity-30" - ] - [] - - -textFieldAlt : List (Html.Attribute msg) -> Html msg -textFieldAlt attributes = - slab - Html.input - attributes - [ "bg-white" - , "block" - , "leading-normal" - , "mb-3" - , "p-3" - , "resize-none" - , "rounded" - , "text-base01" - , "text-sm" - , "w-full" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:text-gray-600" - ] - [] diff --git a/src/Core/Themes/Sunrise/List.elm b/src/Core/Themes/Sunrise/List.elm deleted file mode 100644 index b0da73a07..000000000 --- a/src/Core/Themes/Sunrise/List.elm +++ /dev/null @@ -1,174 +0,0 @@ -module Themes.Sunrise.List exposing (Action, Item, Variant(..), view) - -import Chunky exposing (..) -import Conditional exposing (..) -import Html exposing (Html) -import Html.Attributes exposing (title) -import Html.Events exposing (onClick) -import Html.Events.Extra.Mouse as Mouse exposing (onWithOptions) -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import UI.DnD as DnD -import VirtualDom - - - --- 🌳 - - -type alias Action msg = - { icon : Int -> Coloring -> VirtualDom.Node msg - , msg : Maybe (Mouse.Event -> msg) - , title : String - } - - -type alias Item msg = - { label : Html msg - , actions : List (Action msg) - , msg : Maybe msg - , isSelected : Bool - } - - -type Variant context msg - = Normal - | Draggable (DnD.Environment context msg) - - - --- ⛩ - - -view : Variant Int msg -> List (Item msg) -> Html msg -view variant items = - chunk - [ "antialiased", "font-semibold", "leading-snug", "text-nearly-sm" ] - (List.indexedMap (item variant) items) - - - ------------------------------------------ --- ㊙️ ------------------------------------------ - - -item : Variant Int msg -> Int -> Item msg -> Html msg -item variant idx { label, actions, msg, isSelected } = - let - dragTarget = - case variant of - Normal -> - False - - Draggable env -> - DnD.isDraggingOver idx env.model - in - chunky - [ "border-t" - - -- - , if dragTarget then - "border-base03" - - else - "border-transparent" - - -- Dark mode - ------------ - , if dragTarget then - "dark:border-gray-300" - - else - "dark:border-transparent" - ] - <| - chunk - [ "border-b" - , "border-gray-200" - , "flex" - , "items-center" - - -- - , ifThenElse (Maybe.isJust msg) "cursor-pointer" "cursor-default" - , ifThenElse isSelected "text-base03" "text-inherit" - - -- Dark mode - ------------ - , "dark:border-base00" - , ifThenElse isSelected "dark:text-gray-300" "dark:text-inherit" - ] - [ -- Label - -------- - brick - (case variant of - Normal -> - case msg of - Just m -> - [ onClick m ] - - Nothing -> - [] - - Draggable env -> - List.append - (case ( isSelected, msg ) of - ( True, _ ) -> - [ DnD.listenToStart env idx ] - - ( False, Just m ) -> - [ onClick m ] - - ( False, Nothing ) -> - [] - ) - (DnD.listenToEnterLeave env idx) - ) - [ "flex-grow" - , "py-4" - , "overflow-hidden" - ] - [ label ] - - -- Actions - ---------- - , chunk - [ "flex" - , "items-center" - - -- - , case variant of - Normal -> - "pointer-events-auto" - - Draggable env -> - if DnD.isDragging env.model then - "pointer-events-none" - - else - "pointer-events-auto" - ] - (List.map actionView actions) - ] - - -actionView : Action msg -> Html msg -actionView action = - brick - (case action.msg of - Just msg -> - [ title action.title - - -- - , onWithOptions "click" { stopPropagation = True, preventDefault = True } msg - ] - - Nothing -> - [ title action.title ] - ) - [ "leading-0" - , "ml-1" - , "pl-1" - , ifThenElse (Maybe.isJust action.msg) "cursor-pointer" "cursor-default" - ] - [ action.icon 16 Inherit ] diff --git a/src/Core/Themes/Sunrise/Navigation.elm b/src/Core/Themes/Sunrise/Navigation.elm deleted file mode 100644 index 001a1a974..000000000 --- a/src/Core/Themes/Sunrise/Navigation.elm +++ /dev/null @@ -1,246 +0,0 @@ -module Themes.Sunrise.Navigation exposing (global, local, localWithTabindex) - -import Alfred exposing (Alfred) -import Chunky exposing (..) -import Common -import Conditional exposing (..) -import Html exposing (Html, text) -import Html.Attributes exposing (href, style, tabindex, target, title) -import Html.Events exposing (onClick) -import Html.Events.Extra.Mouse as Mouse -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import UI.Navigation exposing (..) -import UI.Page as Page exposing (Page) - - - --- GLOBAL - - -global : List ( Page, String ) -> Maybe (Alfred reply) -> Page -> Html msg -global items alfred activePage = - brick - [ style "font-size" "11.25px" ] - [ "antialiased" - , "font-semibold" - , "mb-16" - , "mt-8" - , "text-xs" - , "tracking-widest" - , "uppercase" - - -- - , ifThenElse (Maybe.isJust alfred) "opacity-0" "opacity-100" - ] - (List.indexedMap - (globalItem activePage <| List.length items) - items - ) - - -globalItem : Page -> Int -> Int -> ( Page, String ) -> Html msg -globalItem activePage totalItems idx ( page, label ) = - let - isActivePage = - Page.sameBase page activePage - - isLastItem = - idx + 1 == totalItems - in - chunk - [ "inline-block" - , ifThenElse isLastItem "mr-0" "mr-1" - ] - [ slab - Html.a - [ href (Page.toString page) ] - [ "inline-block" - , "leading-normal" - , "no-underline" - , "cursor-pointer" - , "pt-2" - - -- - , ifThenElse isActivePage "border-b" "border-b-0" - , ifThenElse isActivePage "border-base01-15" "border-transparent" - , ifThenElse isActivePage "text-base01" "text-base01-55" - , ifThenElse isLastItem "mr-0" "mr-8" - - -- - , "focus:border-black-50" - , "focus:outline-none" - , "focus:text-black" - ] - [ text label ] - ] - - - --- LOCAL - - -local : List ( Icon msg, Label, Action msg ) -> Html msg -local = - localWithTabindex 0 - - -localWithTabindex : Int -> List ( Icon msg, Label, Action msg ) -> Html msg -localWithTabindex tabindex_ items = - brick - [ style "font-size" "12.5px" ] - [ "antialiased" - , "border-b" - , "border-gray-300" - - -- Dark mode - ------------ - , "dark:border-base01" - ] - [ chunk - [ "flex" ] - (items - |> List.reverse - |> List.map (localItem tabindex_ { amount = List.length items }) - |> List.reverse - ) - ] - - -localItem : Int -> { amount : Int } -> ( Icon msg, Label, Action msg ) -> Html msg -localItem tabindex_ { amount } ( Icon icon, Label labelText labelType, action ) = - slab - (case action of - NavigateToPage _ -> - Html.a - - OpenLinkInNewPage _ -> - Html.a - - PerformMsg _ -> - Html.button - - PerformMsgWithMouseEvent _ -> - Html.button - ) - [ case action of - NavigateToPage page -> - href (Page.toString page) - - OpenLinkInNewPage link -> - href link - - PerformMsg msg -> - onClick msg - - PerformMsgWithMouseEvent msg -> - Mouse.onClick msg - - -- - , case labelType of - Hidden -> - title labelText - - Shown -> - title "" - - -- - , case action of - OpenLinkInNewPage _ -> - target "_blank" - - _ -> - target "_self" - - -- - , tabindex tabindex_ - ] - [ "bg-transparent" - , "border-gray-300" - , "border-r" - , "cursor-pointer" - , "flex-basis-0" - , "font-semibold" - , "leading-none" - , "no-underline" - , "px-4" - , "py-3" - , "text-base02" - - -- - , ifThenElse - (labelText == Common.backToIndex && labelType == Hidden && amount > 1) - "flex-shrink-0" - "flex-grow" - - -- - , ifThenElse - (labelText == Common.backToIndex && labelType == Hidden && amount > 1) - "overflow-visible" - "overflow-hidden" - - -- - , "fixate:text-black" - , "last:border-r-0" - - -- Responsive - ------------- - , "sm:overflow-visible" - - -- - , ifThenElse - (labelType == Hidden) - "sm:flex-shrink-0" - "sm:flex-grow" - - -- - , ifThenElse - (labelType == Hidden) - "sm:flex-grow-0" - "sm:flex-grow" - - -- Dark mode - ------------ - , "dark:border-base01" - , "dark:text-base06" - - -- - , "dark:fixate:text-base07" - ] - [ chunk - [ "border-b" - , "border-t" - , "border-transparent" - , "flex" - , "items-center" - , "justify-center" - , "mt-px" - , "pt-px" - ] - [ inline - [ "flex-shrink-0" ] - [ icon 16 Inherit ] - - -- - , case labelType of - Hidden -> - nothing - - Shown -> - slab - Html.span - [] - [ "hidden" - , "leading-tight" - , "ml-1" - , "transform" - , "translate-y-px" - , "truncate" - - -- Responsive - ------------- - , "sm:inline-block" - ] - [ text labelText ] - ] - ] diff --git a/src/Core/Themes/Sunrise/Notifications.elm b/src/Core/Themes/Sunrise/Notifications.elm deleted file mode 100644 index b28bf72e2..000000000 --- a/src/Core/Themes/Sunrise/Notifications.elm +++ /dev/null @@ -1,133 +0,0 @@ -module Themes.Sunrise.Notifications exposing (view) - -import Chunky exposing (..) -import Color exposing (Color) -import Color.Manipulate -import Conditional exposing (ifThenElse) -import Html exposing (Html, text) -import Html.Attributes exposing (class, rel, style) -import Html.Ext exposing (onDoubleTap, onTap) -import Html.Lazy -import Maybe.Extra as Maybe -import Notifications exposing (..) -import UI.Notifications exposing (Model) -import UI.Types exposing (Msg(..)) - - - --- 🗺 - - -view : Maybe Color -> Model -> Html Msg -view extractedBackdropColor collection = - let - manipulatedColor = - Maybe.map - (Color.Manipulate.darken 0.125) - extractedBackdropColor - in - collection - |> List.reverse - |> List.map (Html.Lazy.lazy2 notificationView manipulatedColor) - |> chunk - [ "notifications" - - -- - , "absolute" - , "break-all" - , "bottom-0" - , "flex" - , "flex-col" - , "items-end" - , "leading-snug" - , "mb-4" - , "mr-3" - , "right-0" - , "text-sm" - , "z-50" - ] - - -notificationView : Maybe Color -> Notification Msg -> Html Msg -notificationView extractedBackdropColor notification = - let - kind = - Notifications.kind notification - - options = - Notifications.options notification - - id = - Notifications.id notification - - dismissMsg = - DismissNotification { id = id } - in - brick - [ if options.sticky then - onDoubleTap dismissMsg - - else - onTap dismissMsg - - -- - , rel (String.fromInt id) - - -- - , case kind of - Casual -> - Maybe.unwrap - (class "bg-white-20") - (style "background-color" << Color.toCssString) - extractedBackdropColor - - Error -> - class "bg-base08" - - Success -> - class "bg-base0b" - ] - [ "duration-200" - , "max-w-xs" - , "mt-2" - , "p-4" - , "rounded" - , "text-white-90" - - -- - , ifThenElse options.sticky "cursor-pointer" "cursor-default" - , ifThenElse options.sticky "select-none" "select-auto" - - -- - , if options.wasDismissed then - "transition" - - else - "transition-colors" - - -- - , if options.wasDismissed then - "opacity-0" - - else - "opacity-100" - ] - [ chunk - [ "mt-px", "pt-px" ] - [ contents notification ] - - -- - , if options.sticky && kind /= Casual then - chunk - [ "cursor-pointer" - , "italic" - , "mt-2" - , "opacity-60" - , "select-none" - , "text-xs" - ] - [ text "Double click to dismiss" ] - - else - nothing - ] diff --git a/src/Core/Themes/Sunrise/Playlists/View.elm b/src/Core/Themes/Sunrise/Playlists/View.elm deleted file mode 100644 index bef64870c..000000000 --- a/src/Core/Themes/Sunrise/Playlists/View.elm +++ /dev/null @@ -1,473 +0,0 @@ -module Themes.Sunrise.Playlists.View exposing (view) - -import Chunky exposing (..) -import Color exposing (Color) -import Common -import Html exposing (Html, text) -import Html.Attributes exposing (href, placeholder, style, value) -import Html.Events exposing (onInput, onSubmit) -import List.Extra as List -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import Playlists exposing (..) -import Themes.Sunrise.Kit as Kit exposing (ButtonType(..)) -import Themes.Sunrise.List -import Themes.Sunrise.Navigation as Navigation -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Playlists.Page exposing (..) -import UI.Types exposing (..) -import Url - - - --- 🗺 - - -view : Page -> List Playlist -> Maybe Playlist -> Maybe { oldName : String, newName : String } -> Maybe Color -> Bool -> Html Msg -view page playlists selectedPlaylist editContext bgColor authMethodSupportsPublicData = - Kit.receptacle - { scrolling = True } - (case page of - Edit encodedName -> - let - filtered = - List.filter - (.autoGenerated >> Maybe.isNothing) - playlists - in - encodedName - |> Url.percentDecode - |> Maybe.andThen (\n -> List.find (.name >> (==) n) filtered) - |> Maybe.map (edit editContext) - |> Maybe.withDefault [ nothing ] - - Index -> - index playlists selectedPlaylist bgColor authMethodSupportsPublicData - - NewCollection -> - newCollection - - NewPlaylist -> - newPlaylist - ) - - - --- INDEX - - -index : List Playlist -> Maybe Playlist -> Maybe Color -> Bool -> List (Html Msg) -index playlists selectedPlaylist bgColor authMethodSupportsPublicData = - let - isSelected playlist = - Maybe.map (\s -> ( s.autoGenerated, s.name )) selectedPlaylist == Just ( playlist.autoGenerated, playlist.name ) - - ( customCollections, customPlaylists ) = - playlists - |> List.filterNot (.autoGenerated >> Maybe.isJust) - |> List.sortBy lowercaseName - |> List.partition .collection - - customPlaylistListItem playlist = - if isSelected playlist then - selectedPlaylistListItem playlist bgColor - - else - { label = text playlist.name - , actions = - List.append - (if authMethodSupportsPublicData then - [ { icon = - if playlist.public then - Icons.public - - else - Icons.public_off - , msg = - Just (\_ -> TogglePlaylistVisibility playlist) - , title = - if playlist.public then - "Make private" - - else - "Make public" - } - ] - - else - [] - ) - [ { icon = Icons.more_vert - , msg = Just (ShowPlaylistListMenu playlist) - , title = "Menu" - } - ] - , msg = Just (ActivatePlaylist playlist) - , isSelected = False - } - - directoryCollections = - playlists - |> List.filter (.autoGenerated >> Maybe.isJust) - |> List.sortBy lowercaseName - |> List.uniqueBy .name - - directoryPlaylistListItem playlist = - if isSelected playlist then - selectedPlaylistListItem playlist bgColor - - else - { label = text playlist.name - , actions = - [ { icon = Icons.more_vert - , msg = Just (ShowPlaylistListMenu playlist) - , title = "Menu" - } - ] - , msg = Just (ActivatePlaylist playlist) - , isSelected = False - } - in - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label Common.backToIndex Hidden - , NavigateToPage Page.Index - ) - , ( Icon Icons.add - , Label "Create a new collection" Shown - , NavigateToPage (Page.Playlists NewCollection) - ) - , ( Icon Icons.add - , Label "Create a new playlist" Shown - , NavigateToPage (Page.Playlists NewPlaylist) - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , if List.isEmpty playlists then - chunk - [ "relative" ] - [ chunk - [ "absolute", "left-0", "top-0" ] - [ Kit.canister [ Kit.h1 "Playlists" ] ] - ] - - else - Kit.canister - [ Kit.h1 "Collections & Playlists" - - -- Intro - -------- - , intro - - -- Custom Collections - --------------------- - , if List.isEmpty customCollections then - nothing - - else - raw - [ category "Your Collections" - , Themes.Sunrise.List.view - Themes.Sunrise.List.Normal - (List.map customPlaylistListItem customCollections) - ] - - -- Custom Playlists - ------------------- - , if List.isEmpty customPlaylists then - nothing - - else - raw - [ category "Your Playlists" - , Themes.Sunrise.List.view - Themes.Sunrise.List.Normal - (List.map customPlaylistListItem customPlaylists) - ] - - -- Directory Collections - ------------------------ - , if List.isEmpty directoryCollections then - nothing - - else - raw - [ category "Autogenerated Directory Collections" - , Themes.Sunrise.List.view - Themes.Sunrise.List.Normal - (List.map directoryPlaylistListItem directoryCollections) - ] - ] - - -- - , if List.isEmpty playlists then - Kit.centeredContent - [ slab - Html.a - [ href (Page.toString <| Page.Playlists NewPlaylist) ] - [ "block" - , "opacity-30" - , "text-inherit" - ] - [ Icons.waves 64 Inherit ] - , slab - Html.a - [ href (Page.toString <| Page.Playlists NewPlaylist) ] - [ "block" - , "leading-normal" - , "mt-2" - , "opacity-40" - , "text-center" - , "text-inherit" - ] - [ text "No playlists found, create one" - , lineBreak - , text "or enable directory collections." - ] - ] - - else - nothing - ] - - -intro : Html Msg -intro = - [ text "Collections and playlists are not tied to the sources of its tracks, " - , text "same goes for favourites. " - , lineBreak - , text "There's also directory collections, which are derived from root directories." - ] - |> raw - |> Kit.intro - - -category : String -> Html Msg -category cat = - chunk - [ "antialiased" - , "font-display" - , "mb-3" - , "mt-10" - , "text-base05" - , "text-xxs" - , "truncate" - , "uppercase" - - -- Dark mode - ------------ - , "dark:text-base04" - ] - [ Kit.inlineIcon Icons.folder - , inline [ "font-bold", "ml-2" ] [ text cat ] - ] - - -selectedPlaylistListItem : Playlist -> Maybe Color -> Themes.Sunrise.List.Item Msg -selectedPlaylistListItem playlist bgColor = - let - selectionColor = - Maybe.withDefault Kit.colors.selection bgColor - in - { label = - brick - [ selectionColor - |> Color.toCssString - |> style "color" - ] - [] - [ text playlist.name ] - , actions = - [ { icon = \size _ -> Icons.check size (Color selectionColor) - , msg = Just (always DeactivatePlaylist) - , title = "Selected playlist" - } - , { icon = Icons.more_vert - , msg = Just (ShowPlaylistListMenu playlist) - , title = "Menu" - } - ] - , msg = Just DeactivatePlaylist - , isSelected = False - } - - - --- NEW - - -newCollection : List (Html Msg) -newCollection = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Back to list" Hidden - , NavigateToPage (Page.Playlists Index) - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , [ Kit.h2 "Name your collection" - - -- - , [ onInput SetPlaylistCreationContext - , placeholder "The Classics" - ] - |> Kit.textField - |> chunky [ "max-w-md", "mx-auto" ] - - -- Button - --------- - , chunk - [ "mt-10" ] - [ Kit.button - Normal - Bypass - (text "Create collection") - ] - ] - |> Kit.canisterForm - |> List.singleton - |> Kit.centeredContent - |> List.singleton - |> slab - Html.form - [ onSubmit CreateCollection ] - [ "flex" - , "flex-grow" - , "text-center" - ] - ] - - -newPlaylist : List (Html Msg) -newPlaylist = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Back to list" Hidden - , NavigateToPage (Page.Playlists Index) - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , [ Kit.h2 "Name your playlist" - - -- - , [ onInput SetPlaylistCreationContext - , placeholder "Sunset" - ] - |> Kit.textField - |> chunky [ "max-w-md", "mx-auto" ] - - -- Button - --------- - , chunk - [ "mt-10" ] - [ Kit.button - Normal - Bypass - (text "Create playlist") - ] - ] - |> Kit.canisterForm - |> List.singleton - |> Kit.centeredContent - |> List.singleton - |> slab - Html.form - [ onSubmit CreatePlaylist ] - [ "flex" - , "flex-grow" - , "text-center" - ] - ] - - - --- EDIT - - -edit : Maybe { oldName : String, newName : String } -> Playlist -> List (Html Msg) -edit editContext playlist = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Back to list" Hidden - , NavigateToPage (Page.Playlists Index) - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , [ Kit.h2 "Name your playlist" - - -- - , [ onInput (SetPlaylistModificationContext playlist.name) - , placeholder "The Classics" - - -- - , case editContext of - Just { oldName, newName } -> - if playlist.name == oldName then - value newName - - else - value playlist.name - - Nothing -> - value playlist.name - ] - |> Kit.textField - |> chunky [ "max-w-md", "mx-auto" ] - - -- Button - --------- - , chunk - [ "mt-10" ] - [ Kit.button - Normal - Bypass - (text "Save") - ] - ] - |> Kit.canisterForm - |> List.singleton - |> Kit.centeredContent - |> List.singleton - |> slab - Html.form - [ onSubmit ModifyPlaylist ] - [ "flex" - , "flex-grow" - , "text-center" - ] - ] - - - --- 🛠 - - -lowercaseName : Playlist -> String -lowercaseName = - .name >> String.toLower diff --git a/src/Core/Themes/Sunrise/Queue/View.elm b/src/Core/Themes/Sunrise/Queue/View.elm deleted file mode 100644 index b6ac02ad8..000000000 --- a/src/Core/Themes/Sunrise/Queue/View.elm +++ /dev/null @@ -1,309 +0,0 @@ -module Themes.Sunrise.Queue.View exposing (view) - -import Chunky exposing (..) -import Common -import Conditional exposing (..) -import Html exposing (Html, text) -import Html.Attributes exposing (href) -import Html.Lazy as Lazy -import Icons -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Queue exposing (..) -import Themes.Sunrise.Kit as Kit -import Themes.Sunrise.List -import Themes.Sunrise.Navigation as Navigation -import UI.DnD as DnD -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Queue.Page as Queue exposing (Page(..)) -import UI.Queue.Types exposing (..) -import UI.Sources.Page -import UI.Types as UI exposing (..) - - - --- 🗺 - - -view : Queue.Page -> UI.Model -> Html UI.Msg -view page model = - case page of - History -> - Lazy.lazy2 - historyView - model.playedPreviously - model.dnd - - Index -> - Lazy.lazy3 - futureView - model.playingNext - model.selectedQueueItem - model.dnd - - - --- 🗺 ░░ FUTURE - - -futureView : List Queue.Item -> Maybe Queue.Item -> DnD.Model Int -> Html UI.Msg -futureView playingNext selectedQueueItem dnd = - Kit.receptacle - { scrolling = not (DnD.isDragging dnd) } - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label Common.backToIndex Hidden - , NavigateToPage Page.Index - ) - , ( Icon Icons.history - , Label "History" Shown - , NavigateToPage (Page.Queue History) - ) - , ( Icon Icons.clear - , Label "Clear" Shown - , PerformMsg (QueueMsg Clear) - ) - , ( Icon Icons.more_horiz - , Label "Menu" Hidden - , PerformMsgWithMouseEvent (QueueMsg << ShowFutureNavigationMenu) - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , if List.isEmpty playingNext then - chunk - [ "relative" ] - [ chunk - [ "absolute", "left-0", "top-0" ] - [ Kit.canister [ Kit.h1 "Up next" ] ] - ] - - else - Kit.canister - [ Kit.h1 "Up next" - , playingNext - |> List.indexedMap (futureItem selectedQueueItem) - |> Themes.Sunrise.List.view - (Themes.Sunrise.List.Draggable - { model = dnd - , toMsg = UI.DnD - } - ) - |> chunky [ "mt-3" ] - ] - - -- - , if List.isEmpty playingNext then - Kit.centeredContent - [ slab - Html.a - [ href (Page.toString <| Page.Sources UI.Sources.Page.New) ] - [ "text-inherit", "block", "opacity-30" ] - [ Icons.music_note 64 Inherit ] - , slab - Html.a - [ href (Page.toString <| Page.Sources UI.Sources.Page.New) ] - [ "text-inherit", "block", "leading-normal", "mt-2", "opacity-40", "text-center" ] - [ text "Nothing here yet," - , lineBreak - , text "add some music first." - ] - ] - - else - nothing - ] - - -futureItem : Maybe Item -> Int -> Queue.Item -> Themes.Sunrise.List.Item UI.Msg -futureItem selectedQueueItem idx item = - let - ( identifiers, track ) = - item.identifiedTrack - - isSelected = - selectedQueueItem - |> Maybe.map (.identifiedTrack >> Tuple.first >> .indexInList) - |> (==) (Just identifiers.indexInList) - - iconFn = - if item.manualEntry then - identity - - else - Icons.wrapped subtleFutureIconClasses - in - { label = - inline - [ "block" - , "truncate" - - -- - , if item.manualEntry || isSelected then - "text-inherit" - - else - "text-base05" - - -- Dark mode - ------------ - , if item.manualEntry || isSelected then - "dark:text-inherit" - - else - "dark:text-base04" - ] - [ inline - [ "inline-block" - , "mr-2" - , "opacity-60" - , "text-xs" - ] - [ text (String.fromInt <| idx + 1), text "." ] - , case track.tags.artist of - Just artist -> - text (artist ++ " - " ++ track.tags.title) - - Nothing -> - text track.tags.title - ] - , actions = - [ -- Remove - --------- - { icon = - if item.manualEntry then - iconFn Icons.remove_circle_outline - - else - iconFn Icons.not_interested - , msg = - { index = idx, item = item } - |> RemoveItem - |> QueueMsg - |> always - |> Just - , title = - ifThenElse item.manualEntry "Remove" "Ignore" - } - - -- Menu - ------- - , { icon = - iconFn Icons.more_vert - , msg = - Just (QueueMsg << ShowFutureMenu item { index = idx }) - , title = - "Menu" - } - ] - , msg = Just (QueueMsg <| Select item) - , isSelected = isSelected - } - - -subtleFutureIconClasses : List String -subtleFutureIconClasses = - [ "text-gray-500" - - -- Dark mode - ------------ - , "dark:text-base02" - ] - - - --- 🗺 ░░ HISTORY - - -historyView : List Queue.Item -> DnD.Model Int -> Html UI.Msg -historyView playedPreviously dnd = - Kit.receptacle - { scrolling = not (DnD.isDragging dnd) } - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label Common.backToIndex Hidden - , NavigateToPage Page.Index - ) - , ( Icon Icons.update - , Label "Up next" Shown - , NavigateToPage (Page.Queue Index) - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , if List.isEmpty playedPreviously then - chunk - [ "relative" ] - [ chunk - [ "absolute", "left-0", "top-0" ] - [ Kit.canister [ Kit.h1 "History" ] ] - ] - - else - Kit.canister - [ Kit.h1 "History" - , playedPreviously - |> List.reverse - |> List.indexedMap historyItem - |> Themes.Sunrise.List.view Themes.Sunrise.List.Normal - |> chunky [ "mt-3" ] - ] - - -- - , if List.isEmpty playedPreviously then - Kit.centeredContent - [ chunk - [ "opacity-30" ] - [ Icons.music_note 64 Inherit ] - , chunk - [ "leading-normal", "mt-2", "opacity-40", "text-center" ] - [ text "Nothing here yet," - , lineBreak - , text "play some music first." - ] - ] - - else - nothing - ] - - -historyItem : Int -> Queue.Item -> Themes.Sunrise.List.Item UI.Msg -historyItem idx ({ identifiedTrack } as item) = - let - ( _, track ) = - identifiedTrack - in - { label = - inline - [ "block", "truncate" ] - [ inline - [ "inline-block", "text-xs", "mr-2" ] - [ text (String.fromInt <| idx + 1), text "." ] - , case track.tags.artist of - Just artist -> - text (artist ++ " - " ++ track.tags.title) - - Nothing -> - text track.tags.title - ] - , actions = - [ { icon = Icons.more_vert - , msg = Just (QueueMsg << ShowHistoryMenu item) - , title = "Menu" - } - ] - , msg = Nothing - , isSelected = False - } diff --git a/src/Core/Themes/Sunrise/Settings.elm b/src/Core/Themes/Sunrise/Settings.elm deleted file mode 100644 index 6eaa3bc11..000000000 --- a/src/Core/Themes/Sunrise/Settings.elm +++ /dev/null @@ -1,450 +0,0 @@ -module Themes.Sunrise.Settings exposing (Dependencies, view) - -import Chunky exposing (..) -import Color exposing (Color) -import Common exposing (ServiceWorkerStatus(..)) -import Conditional exposing (ifThenElse) -import DateFormat as Date -import Html exposing (Html, text) -import Html.Attributes as A exposing (..) -import Html.Events as E exposing (onClick) -import Html.Lazy -import LastFm -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import Themes.Sunrise.Kit as Kit -import Themes.Sunrise.Navigation as Navigation -import Themes.Sunrise.Settings.Data -import Themes.Sunrise.Settings.Sync -import Time -import UI.Backdrop as Backdrop exposing (backgroundPositioning) -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Settings.Page as Settings exposing (..) -import UI.Sources.Types as Sources -import UI.Tracks.Types as Tracks -import UI.Types exposing (Msg(..)) -import User.Layer - - - --- 🗺 - - -type alias Dependencies = - { syncMethod : Maybe User.Layer.Method - , buildTimestamp : Int - , chosenBackgroundImage : Maybe String - , coverSelectionReducesPool : Bool - , currentTimeZone : Time.Zone - , extractedBackdropColor : Maybe Color - , hideDuplicateTracks : Bool - , lastFm : LastFm.Model - , processAutomatically : Bool - , rememberProgress : Bool - , serviceWorkerStatus : ServiceWorkerStatus - , version : String - } - - -view : Settings.Page -> Dependencies -> Html Msg -view page deps = - case page of - Data -> - Themes.Sunrise.Settings.Data.view deps.syncMethod - - Index -> - Kit.receptacle { scrolling = True } (index deps) - - Sync -> - Themes.Sunrise.Settings.Sync.view deps.syncMethod - - - --- INDEX - - -index : Dependencies -> List (Html Msg) -index deps = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.account_circle - , Label "Data & Sync" Shown - , NavigateToPage (Page.Settings Sync) - ) - , ( Icon Icons.brush - , Label "Change theme" Shown - , PerformMsg AssistWithChangingTheme - ) - , ( Icon Icons.help_outline - , Label "Help" Shown - , OpenLinkInNewPage "about/" - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , deps - |> content - |> chunk [ "pb-4" ] - |> List.singleton - |> Kit.canister - ] - - -content : Dependencies -> List (Html Msg) -content deps = - [ ----------------------------------------- - -- Title - ----------------------------------------- - Kit.h1 "Settings" - - ----------------------------------------- - -- Version - ----------------------------------------- - , chunk - [ "mt-6" ] - [ chunk - [ "flex" - , "flex-col" - , "items-center" - , "justify-center" - , "text-base05" - , "text-xs" - - -- Dark mode - ------------ - , "dark:text-base03" - ] - [ slab - Html.img - [ A.src "images/diffuse-dark.svg" - , A.width 160 - ] - [ "dark:hidden" ] - [] - - -- - , slab - Html.img - [ A.src "images/diffuse-light.svg" - , A.width 160 - ] - [ "hidden dark:block" ] - [] - - -- - , chunk - [ "italic", "mt-3", "text-center" ] - [ text "Version " - , text deps.version - , lineBreak - , text "Built on " - , deps.buildTimestamp - |> (*) 1000 - |> Time.millisToPosix - |> Date.format - [ Date.monthNameAbbreviated - , Date.text " " - , Date.dayOfMonthSuffix - , Date.text " " - , Date.yearNumber - , Date.text ", " - , Date.hourMilitaryFixed - , Date.text ":" - , Date.minuteFixed - , Date.text ":" - , Date.secondFixed - ] - deps.currentTimeZone - |> text - - -- - , chunk - [ "not-italic", "mt-3" ] - [ case deps.serviceWorkerStatus of - InstallingInitial -> - inline - [ "inline-flex", "items-center" ] - [ text "Setting up service worker" - , inline [ "ml-1" ] [ Icons.downloading 12 Inherit ] - ] - - InstallingNew -> - inline - [ "inline-flex", "items-center" ] - [ text "Installing new version" - , inline [ "ml-1" ] [ Icons.downloading 12 Inherit ] - ] - - WaitingForActivation -> - inline - [] - [ text "Update available" - , brick - [ Maybe.unwrap - (class "bg-white-20") - (style "background-color" << Color.toCssString) - deps.extractedBackdropColor - - -- - , E.onClick ReloadApp - ] - [ "bg-base06" - , "cursor-pointer" - , "inline-block" - , "leading-none" - , "ml-1" - , "p-1" - , "rounded" - , "text-white" - - -- Dark mode - ------------ - , "dark:bg-base01" - ] - [ text "Reload app" ] - ] - - Activated -> - nothing - ] - ] - ] - ] - - ----------------------------------------- - -- Background - ----------------------------------------- - , chunk - [ "mt-6" ] - [ label "Background Image" - , Html.Lazy.lazy backgroundImage deps.chosenBackgroundImage - ] - - ----------------------------------------- - -- Row 1 - ----------------------------------------- - , chunk - [ "flex", "flex-wrap", "pt-2" ] - [ chunk - [ "w-full", "md:w-1/2" ] - [ label "Downloaded tracks" - , Kit.buttonWithColor - Kit.Gray - Kit.Normal - (TracksMsg Tracks.ClearCache) - (text "Clear cache") - ] - - -- Last.fm - ---------- - , chunk - [ "w-1/2" ] - [ label "Last.fm scrobbling" - - -- - , case ( deps.lastFm.authenticating, deps.lastFm.sessionKey ) of - ( _, Just _ ) -> - Kit.checkbox - { checked = True - , toggleMsg = DisconnectLastFm - } - - ( True, Nothing ) -> - Kit.buttonWithColor - Kit.Gray - Kit.Normal - Bypass - (text "Connecting") - - ( False, Nothing ) -> - Kit.buttonWithColor - Kit.Gray - Kit.Normal - ConnectLastFm - (text "Connect") - ] - ] - - ----------------------------------------- - -- Row 2 - ----------------------------------------- - , chunk - [ "flex", "flex-wrap" ] - [ chunk - [ "w-full", "md:w-1/2" ] - [ label "Hide Duplicates" - , Kit.checkbox - { checked = deps.hideDuplicateTracks - , toggleMsg = TracksMsg Tracks.ToggleHideDuplicates - } - ] - , chunk - [ "w-full", "md:w-1/2" ] - [ label "Process sources automatically" - , Kit.checkbox - { checked = deps.processAutomatically - , toggleMsg = SourcesMsg Sources.ToggleProcessAutomatically - } - ] - ] - - ----------------------------------------- - -- Row 3 - ----------------------------------------- - , chunk - [ "flex", "flex-wrap" ] - [ chunk - [ "w-full", "md:w-1/2" ] - [ label "Remember position on long tracks" - , Kit.checkbox - { checked = deps.rememberProgress - , toggleMsg = ToggleRememberProgress - } - ] - , chunk - [ "w-full", "md:w-1/2" ] - [ label "Cover selection reduces track pool" - , Kit.checkbox - { checked = deps.coverSelectionReducesPool - , toggleMsg = TracksMsg Tracks.ToggleCoverSelectionReducesPool - } - ] - ] - ] - - -label : String -> Html msg -label l = - chunk - [ "mb-3", "mt-6", "pb-px" ] - [ Kit.label [] l ] - - - --- BACKGROUND IMAGE - - -backgroundImage : Maybe String -> Html Msg -backgroundImage chosenBackground = - chunk - [ "flex", "flex-wrap" ] - (List.map - (\( filename, _ ) -> - let - isActive = - chosenBackground == Just filename - in - brick - [ onClick (ChooseBackdrop filename) ] - [ "cursor-pointer" - , "h-0" - , "overflow-hidden" - , "pt-1/8" - , "relative" - , "w-1/5" - - -- - , "md:pt-1/16" - , "md:w-1/10" - ] - [ if isActive then - chunk - [ "absolute" - , "bg-base04" - , "inset-0" - , "mb-1" - , "mr-1" - , "rounded-sm" - , "z-10" - - -- - , "sm:mb-2" - , "sm:mr-2" - - -- - , "md:mb-1" - , "md:mr-1" - ] - [] - - else - nothing - - -- - , brick - [ backgroundPositioning filename - - -- - , ")" - |> String.append filename - |> String.append "url(images/Background/Thumbnails/" - |> style "background-image" - ] - [ "absolute" - , "bg-cover" - , "inset-0" - , "mb-1" - , "mr-1" - , "rounded-sm" - , "z-20" - - -- - , "sm:mb-2" - , "sm:mr-2" - - -- - , "md:mb-1" - , "md:mr-1" - - -- - , ifThenElse isActive "opacity-20" "opacity-100" - ] - [] - - -- - , if isActive then - chunk - [ "absolute" - , "inset-0" - , "flex" - , "font-semibold" - , "items-center" - , "justify-center" - , "leading-snug" - , "mb-1" - , "mr-1" - , "px-2" - , "text-center" - , "text-white" - , "text-xs" - , "z-30" - - -- - , "sm:mb-2" - , "sm:mr-2" - - -- - , "md:mb-1" - , "md:mr-1" - - -- Dark mode - ------------ - , "dark:text-base07" - ] - [ chunk - [ "mt-px" ] - [ Icons.check 16 Inherit ] - ] - - else - nothing - ] - ) - Backdrop.options - ) diff --git a/src/Core/Themes/Sunrise/Settings/Common.elm b/src/Core/Themes/Sunrise/Settings/Common.elm deleted file mode 100644 index 9c763cb26..000000000 --- a/src/Core/Themes/Sunrise/Settings/Common.elm +++ /dev/null @@ -1,20 +0,0 @@ -module Themes.Sunrise.Settings.Common exposing (..) - -import Material.Icons.Round as Icons -import UI.Navigation exposing (..) -import UI.Syncing.Types as Syncing -import UI.Types - - - --- SYNCING - - -changePassphrase method = - ( Icon Icons.lock - , Label "Change Passphrase" Shown - , method - |> Syncing.ShowUpdateEncryptionKeyScreen - |> UI.Types.SyncingMsg - |> PerformMsg - ) diff --git a/src/Core/Themes/Sunrise/Settings/Data.elm b/src/Core/Themes/Sunrise/Settings/Data.elm deleted file mode 100644 index 743d11427..000000000 --- a/src/Core/Themes/Sunrise/Settings/Data.elm +++ /dev/null @@ -1,89 +0,0 @@ -module Themes.Sunrise.Settings.Data exposing (view) - -import Chunky exposing (..) -import Html exposing (Html, text) -import Material.Icons.Round as Icons -import Themes.Sunrise.Kit as Kit exposing (ButtonColor(..), ButtonType(..)) -import Themes.Sunrise.Navigation as Navigation -import Themes.Sunrise.Settings.Common exposing (changePassphrase) -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Settings.Page exposing (Page(..)) -import UI.Types exposing (Msg(..)) -import User.Layer exposing (Method(..)) - - - --- 🗺 - - -view : Maybe Method -> Html Msg -view activeMethod = - Kit.receptacle - { scrolling = True } - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - (case activeMethod of - Just (Dropbox d) -> - [ changePassphrase (Dropbox d) ] - - Just (Ipfs i) -> - [ changePassphrase (Ipfs i) ] - - Just (RemoteStorage r) -> - [ changePassphrase (RemoteStorage r) ] - - Nothing -> - [] - ) - |> List.append - [ ( Icon Icons.arrow_back - , Label "Settings" Hidden - , NavigateToPage (Page.Settings Index) - ) - , ( Icon Icons.account_circle - , Label "Storage Service" Shown - , NavigateToPage (Page.Settings Sync) - ) - ] - |> Navigation.local - - ----------------------------------------- - -- Content - ----------------------------------------- - , chunk - [ "relative" ] - [ chunk - [ "absolute", "left-0", "top-0" ] - [ Kit.canister [ Kit.h1 "Data Backup" ] ] - ] - - -- - , Kit.focusScreen - { icon = Icons.archive - , iconHref = Nothing - , text = - "You can download a snapshot of your user data, or recover an account by uploading a snapshot." - |> text - |> List.singleton - |> chunk [ "max-w-sm" ] - |> List.singleton - , textHref = Nothing - } - [ chunk - [ "flex", "space-x-2.5" ] - [ Kit.button - Normal - RequestImport - (text "Import snapshot") - - -- - , Kit.buttonWithColor - Accent - Filled - Export - (text "Download snapshot") - ] - ] - ] diff --git a/src/Core/Themes/Sunrise/Settings/Sync.elm b/src/Core/Themes/Sunrise/Settings/Sync.elm deleted file mode 100644 index 28969f2b1..000000000 --- a/src/Core/Themes/Sunrise/Settings/Sync.elm +++ /dev/null @@ -1,190 +0,0 @@ -module Themes.Sunrise.Settings.Sync exposing (view) - -import Chunky exposing (..) -import Html exposing (Html, text) -import Material.Icons.Round as Icons -import Maybe.Extra as Maybe -import Themes.Sunrise.Kit as Kit -import Themes.Sunrise.List -import Themes.Sunrise.Navigation as Navigation -import Themes.Sunrise.Settings.Common exposing (changePassphrase) -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Settings.Page exposing (Page(..)) -import UI.Svg.Elements -import UI.Syncing.Common exposing (startDropbox, startIpfs, startRemoteStorage) -import UI.Syncing.Types as Syncing -import UI.Types exposing (Msg(..)) -import User.Layer exposing (Method(..), dropboxMethod, ipfsMethod, methodName, remoteStorageMethod) - - - --- 🗺 - - -view : Maybe Method -> Html Msg -view activeMethod = - Kit.receptacle - { scrolling = True } - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - (case activeMethod of - Just (Dropbox d) -> - [ changePassphrase (Dropbox d) ] - - Just (Ipfs i) -> - [ changePassphrase (Ipfs i) ] - - Just (RemoteStorage r) -> - [ changePassphrase (RemoteStorage r) ] - - Nothing -> - [] - ) - |> List.append - [ ( Icon Icons.arrow_back - , Label "Settings" Hidden - , NavigateToPage (Page.Settings Index) - ) - , ( Icon Icons.archive - , Label "Data Backup" Shown - , NavigateToPage (Page.Settings Data) - ) - ] - |> Navigation.local - - ----------------------------------------- - -- Content - ----------------------------------------- - , chunk - [ "relative" ] - [ chunk - [ "absolute", "left-0", "top-0" ] - [ Kit.canister [ Kit.h1 "Storage Service" ] ] - ] - - -- - , Kit.focusScreen - { icon = Icons.account_circle - , iconHref = Nothing - , text = - "By default, your playlists, favourites and other data are stored locally on your device. To ensure it is backed up, and to keep it in sync across all your connected devices, choose one of the following storage services. You can switch services at any point." - |> text - |> List.singleton - |> chunk [ "max-w-md" ] - |> List.singleton - , textHref = Nothing - } - [ [ dropboxMethod - , remoteStorageMethod - , ipfsMethod - ] - |> List.map (methodView activeMethod) - |> Themes.Sunrise.List.view Themes.Sunrise.List.Normal - |> List.singleton - |> chunk [ "max-w-full", "w-96" ] - ] - ] - - -methodInfoAction : Bool -> Maybe Method -> Method -> Themes.Sunrise.List.Action Msg -methodInfoAction isSelected activeMethod method = - { icon = - \a b -> - inline - [ opacity isSelected activeMethod ] - [ Icons.help a b ] - , msg = - case method of - Dropbox _ -> - Just (\_ -> OpenUrlOnNewPage "https://dropbox.com") - - RemoteStorage _ -> - Just (\_ -> OpenUrlOnNewPage "https://remotestorage.io") - - Ipfs _ -> - Just (\_ -> OpenUrlOnNewPage "https://ipfs.io") - , title = - "Learn more about " ++ methodName method - } - - -methodView activeMethod method = - let - isSelected = - case ( activeMethod, method ) of - ( Just (Dropbox _), Dropbox _ ) -> - True - - ( Just (RemoteStorage _), RemoteStorage _ ) -> - True - - ( Just (Ipfs _), Ipfs _ ) -> - True - - _ -> - False - - label = - methodLabel activeMethod method isSelected - in - { label = - case method of - Dropbox _ -> - label UI.Svg.Elements.dropboxLogo - - RemoteStorage _ -> - label UI.Svg.Elements.remoteStorageLogo - - Ipfs _ -> - label UI.Svg.Elements.ipfsLogo - , actions = - [ if isSelected then - { icon = Icons.cancel - , msg = Just (\_ -> SyncingMsg Syncing.StopSync) - , title = "Stop syncing with " ++ methodName method - } - - else - methodInfoAction isSelected activeMethod method - ] - , msg = - if isSelected then - Just (SyncingMsg Syncing.StopSync) - - else if Maybe.isJust activeMethod then - Nothing - - else - case method of - Dropbox _ -> - Just startDropbox - - RemoteStorage _ -> - Just startRemoteStorage - - Ipfs _ -> - Just startIpfs - , isSelected = - isSelected - } - - -methodLabel activeMethod method isSelected icon = - inline - [ "flex", "items-center", opacity isSelected activeMethod ] - [ inline [ "inline-block", "mr-3" ] [ Html.map never (icon 15) ] - , Html.text (methodName method) - ] - - -opacity isSelected activeMethod = - if isSelected then - "opacity-100" - - else if Maybe.isJust activeMethod then - "opacity-50" - - else - "opacity-100" diff --git a/src/Core/Themes/Sunrise/Sources/Form.elm b/src/Core/Themes/Sunrise/Sources/Form.elm deleted file mode 100644 index 2cf19ac45..000000000 --- a/src/Core/Themes/Sunrise/Sources/Form.elm +++ /dev/null @@ -1,536 +0,0 @@ -module Themes.Sunrise.Sources.Form exposing (..) - -import Chunky exposing (..) -import Common exposing (boolFromString, boolToString) -import Conditional exposing (..) -import Dict.Ext as Dict -import Html exposing (Html, text) -import Html.Attributes as A exposing (attribute, for, name, placeholder, required, selected, type_, value) -import Html.Events exposing (onInput, onSubmit) -import List.Extra as List -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Sources exposing (..) -import Sources.Services as Services -import Themes.Sunrise.Kit as Kit exposing (ButtonType(..), select) -import Themes.Sunrise.Navigation as Navigation -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Sources.Page as Sources -import UI.Sources.Types exposing (..) - - - --- NEW - - -type alias Arguments = - { onboarding : Bool } - - -new : Arguments -> Form -> List (Html Msg) -new args model = - case model.step of - Where -> - newWhere args model - - How -> - newHow model - - By -> - newBy model - - -newWhere : Arguments -> Form -> List (Html Msg) -newWhere { onboarding } { context } = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Back to list" Hidden - -- - , if onboarding then - NavigateToPage Page.Index - - else - NavigateToPage (Page.Sources Sources.Index) - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , (\h -> - form TakeStep - [ Kit.canisterForm h ] - ) - [ Kit.h2 "Where is your music stored?" - - -- Dropdown - ----------- - , let - contextServiceKey = - Services.typeToKey context.service - in - Services.labels - |> List.map - (\( k, l ) -> - Html.option - [ value k, selected (contextServiceKey == k) ] - [ text l ] - ) - |> select SelectService - - -- Button - --------- - , chunk - [ "mt-10" ] - [ Kit.button - IconOnly - Bypass - (Icons.arrow_forward 17 Inherit) - ] - ] - ] - - -newHow : Form -> List (Html Msg) -newHow { context } = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Take a step back" Shown - , PerformMsg TakeStepBackwards - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , (\h -> - form TakeStep - [ chunk - [ "text-left", "w-full" ] - [ Kit.canister h ] - ] - ) - [ Kit.h3 "Where exactly?" - - -- Note - ------- - , note context.service - - -- Fields - --------- - , let - properties = - Services.properties context.service - - dividingPoint = - toFloat (List.length properties) / 2 - - ( listA, listB ) = - List.splitAt (ceiling dividingPoint) properties - in - chunk - [ "flex", "pt-3" ] - [ chunk - [ "flex-grow", "pr-4" ] - (List.map (renderProperty context) listA) - , chunk - [ "flex-grow", "pl-4" ] - (List.map (renderProperty context) listB) - ] - - -- Button - --------- - , chunk - [ "mt-3", "text-center" ] - [ Kit.button - IconOnly - Bypass - (Icons.arrow_forward 17 Inherit) - ] - ] - ] - - -howNote : List (Html Msg) -> Html Msg -howNote = - chunk - [ "text-sm" - , "italic" - , "leading-normal" - , "max-w-lg" - , "mb-8" - ] - - -newBy : Form -> List (Html Msg) -newBy { context } = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Take a step back" Shown - , PerformMsg TakeStepBackwards - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , (\h -> - form AddSourceUsingForm - [ Kit.canisterForm h ] - ) - [ Kit.h2 "One last thing" - , Kit.label [] "What are we going to call this source?" - - -- Input - -------- - , let - nameValue = - Dict.fetch "name" "" context.data - in - chunk - [ "flex" - , "max-w-md" - , "mt-8" - , "mx-auto" - , "justify-center" - , "w-full" - ] - [ Kit.textField - [ name "name" - , onInput (SetFormData "name") - , value nameValue - ] - ] - - -- Note - ------- - , chunk - [ "mt-16" ] - (case context.service of - AmazonS3 -> - corsWarning "CORS__S3" - - AzureBlob -> - corsWarning "CORS__Azure" - - AzureFile -> - corsWarning "CORS__Azure" - - Dropbox -> - [] - - Google -> - [] - - Ipfs -> - corsWarning "CORS__IPFS" - - WebDav -> - corsWarning "CORS__WebDAV" - ) - - -- Button - --------- - , Kit.button - Normal - Bypass - (text "Add source") - ] - ] - - -corsWarning : String -> List (Html Msg) -corsWarning id = - [ chunk - [ "text-sm", "flex", "items-center", "justify-center", "leading-snug", "opacity-50" ] - [ Kit.inlineIcon Icons.warning - , inline - [ "font-semibold" ] - [ text "Make sure CORS is enabled" ] - ] - , chunk - [ "text-sm", "leading-snug", "mb-8", "mt-1", "opacity-50" ] - [ text "You can find the instructions over " - , Kit.link { label = "here", url = "about/cors/#" ++ id } - ] - ] - - - --- EDIT - - -edit : Form -> List (Html Msg) -edit { context } = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Go Back" Shown - , PerformMsg ReturnToIndex - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , (\h -> - form EditSourceUsingForm - [ chunk - [ "text-left", "w-full" ] - [ Kit.canister h ] - ] - ) - [ Kit.h3 "Edit source" - - -- Note - ------- - , note context.service - - -- Fields - --------- - , let - properties = - Services.properties context.service - - dividingPoint = - toFloat (List.length properties) / 2 - - ( listA, listB ) = - List.splitAt (ceiling dividingPoint) properties - in - chunk - [ "flex", "pt-3" ] - [ chunk - [ "flex-grow", "pr-4" ] - (List.map (renderProperty context) listA) - , chunk - [ "flex-grow", "pl-4" ] - (List.map (renderProperty context) listB) - ] - - -- Button - --------- - , chunk - [ "mt-3", "text-center" ] - [ Kit.button - Normal - Bypass - (text "Save") - ] - ] - ] - - - --- PROPERTIES - - -renderProperty : Source -> Property -> Html Msg -renderProperty context property = - chunk - [ "mb-8" ] - [ Kit.label - [ for property.key ] - property.label - - -- - , if - (property.placeholder == boolToString True) - || (property.placeholder == boolToString False) - then - let - bool = - context.data - |> Dict.fetch property.key property.placeholder - |> boolFromString - in - chunk - [ "mt-2", "pt-1" ] - [ Kit.checkbox - { checked = bool - , toggleMsg = - bool - |> not - |> boolToString - |> SetFormData property.key - } - ] - - else - Kit.textField - [ name property.key - , onInput (SetFormData property.key) - , placeholder property.placeholder - , required (property.label |> String.toLower |> String.contains "optional" |> not) - , type_ (ifThenElse property.password "password" "text") - , value (Dict.fetch property.key "" context.data) - - -- - , attribute "spellcheck" "false" - ] - ] - - - --- ⚗️ - - -form : Msg -> List (Html Msg) -> Html Msg -form msg html = - slab - Html.form - [ onSubmit msg ] - [ "flex" - , "flex-grow" - , "flex-shrink-0" - , "text-center" - ] - [ Kit.centeredContent html ] - - -note : Service -> Html Msg -note service = - case service of - AmazonS3 -> - nothing - - AzureBlob -> - nothing - - AzureFile -> - nothing - - Dropbox -> - howNote - [ inline - [ "font-semibold" ] - [ text "If you don't know what any of this is, " - , text "continue to the next screen." - ] - , text " Changing the app key allows you to use your own Dropbox app." - , text " Also, make sure you verified your email address on Dropbox," - , text " or this might not work." - ] - - Google -> - howNote - [ inline - [ "font-semibold" ] - [ text "If you don't know what any of this is, " - , text "continue to the next screen." - ] - , text " Changing the client stuff allows you to use your own Google OAuth client." - , text " Disclaimer: " - , text "The Google Drive API is fairly slow and limited, " - , text "consider using a different service." - ] - - Ipfs -> - howNote - [ inline - [ "font-semibold" ] - [ text "Diffuse will use the ipfs.io gateway by default" ] - , text "." - , lineBreak - , inline - [] - [ text "There are also " - , Html.a - [ A.href "https://ipfs.github.io/public-gateway-checker/" - , A.class "underline" - , A.target "_blank" - ] - [ text "other public gateways" ] - , text " you can choose from." - ] - , lineBreak - , text "If you would like to use another gateway, please provide it below." - ] - - WebDav -> - howNote - [ inline - [ "font-semibold" ] - [ Kit.inlineIcon Icons.warning - , text "This app requires a proper implementation of " - , Kit.link - { label = "CORS" - , url = "https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" - } - , text " on the server side." - ] - , lineBreak - , text " WebDAV servers usually don't implement" - , text " CORS properly, if at all." - , lineBreak - , text " Some servers, like " - , Kit.link - { label = "this one" - , url = "https://github.com/hacdias/webdav" - } - , text ", do. You can find the configuration for that server " - , Kit.link - { label = "here" - , url = "about/cors/#CORS__WebDAV" - } - , text "." - ] - - - --- RENAME - - -rename : Form -> List (Html Msg) -rename { context } = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - Navigation.local - [ ( Icon Icons.arrow_back - , Label "Go Back" Shown - , PerformMsg ReturnToIndex - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , (\h -> - form RenameSourceUsingForm - [ Kit.canisterForm h ] - ) - [ Kit.h2 "Name your source" - - -- Input - -------- - , [ name "name" - , onInput (SetFormData "name") - , value (Dict.fetch "name" "" context.data) - ] - |> Kit.textField - |> chunky [ "max-w-md", "mx-auto" ] - - -- Button - --------- - , chunk - [ "mt-10" ] - [ Kit.button - Normal - Bypass - (text "Save") - ] - ] - ] diff --git a/src/Core/Themes/Sunrise/Sources/View.elm b/src/Core/Themes/Sunrise/Sources/View.elm deleted file mode 100644 index 4c21f7ee2..000000000 --- a/src/Core/Themes/Sunrise/Sources/View.elm +++ /dev/null @@ -1,292 +0,0 @@ -module Themes.Sunrise.Sources.View exposing (view) - -import Chunky exposing (..) -import Conditional exposing (ifThenElse) -import Dict.Ext as Dict -import Html exposing (Html, text) -import Html.Attributes exposing (href) -import Html.Lazy as Lazy -import List.Extra as List -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Sources exposing (..) -import Themes.Sunrise.Kit as Kit -import Themes.Sunrise.List -import Themes.Sunrise.Navigation as Navigation -import Themes.Sunrise.Sources.Form as Form -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Sources.Page as Sources exposing (..) -import UI.Sources.Types exposing (Msg(..)) -import UI.Types as UI exposing (Model) - - - --- 🌳 - - -type alias PartialModel = - { processingContext : List ( String, Float ) - , processingError : Maybe { error : String, sourceId : String } - , sources : List Source - } - - - --- 🗺 - - -view : Sources.Page -> Model -> Html UI.Msg -view page model = - Html.map UI.SourcesMsg - (case page of - Index -> - Lazy.lazy2 - (\a b -> receptacle <| index a b) - (List.length model.tracks.untouched) - { processingContext = model.processingContext - , processingError = model.processingError - , sources = model.sources - } - - Edit _ -> - lazyForm model <| Form.edit - - New -> - lazyForm model <| Form.new { onboarding = False } - - NewOnboarding -> - lazyForm model <| Form.new { onboarding = True } - - NewThroughRedirect _ _ -> - lazyForm model <| Form.new { onboarding = False } - - Rename _ -> - lazyForm model <| Form.rename - ) - - -lazyForm model formView = - Lazy.lazy (formView >> receptacle) model.sourceForm - - -receptacle = - Kit.receptacle { scrolling = True } - - - --- INDEX - - -index : Int -> PartialModel -> List (Html Msg) -index amountOfTracks model = - [ ----------------------------------------- - -- Navigation - ----------------------------------------- - if List.isEmpty model.sources then - Navigation.local - [ ( Icon Icons.add - , Label "Add a new source" Shown - , NavigateToPage (Page.Sources New) - ) - ] - - else - Navigation.local - [ ( Icon Icons.add - , Label "Add a new source" Shown - , NavigateToPage (Page.Sources New) - ) - - -- Process - ---------- - , if List.isEmpty model.processingContext then - ( Icon Icons.sync - , Label "Process sources" Shown - , PerformMsg Process - ) - - else - ( Icon Icons.sync - , Label "Stop processing ..." Shown - , PerformMsg StopProcessing - ) - ] - - ----------------------------------------- - -- Content - ----------------------------------------- - , if List.isEmpty model.sources then - chunk - [ "relative" ] - [ chunk - [ "absolute", "left-0", "top-0" ] - [ Kit.canister [ Kit.h1 "Sources" ] ] - ] - - else - Kit.canister - [ Kit.h1 "Sources" - - -- Intro - -------- - , intro amountOfTracks - - -- List - ------- - , model.sources - |> List.sortBy - lowercaseName - |> List.map - (\source -> - { label = Html.text (Dict.fetch "name" "" source.data) - , actions = sourceActions model.processingContext model.processingError source - , msg = Nothing - , isSelected = False - } - ) - |> Themes.Sunrise.List.view Themes.Sunrise.List.Normal - ] - - -- - , if List.isEmpty model.sources then - Kit.centeredContent - [ slab - Html.a - [ href (Page.toString <| Page.Sources New) ] - [ "block" - , "opacity-30" - , "text-inherit" - ] - [ Icons.music_note 64 Inherit ] - , slab - Html.a - [ href (Page.toString <| Page.Sources New) ] - [ "block" - , "leading-normal" - , "mt-2" - , "opacity-40" - , "text-center" - , "text-inherit" - ] - [ text "A source is a place where music is stored," - , lineBreak - , text "add one so you can play some music " - , inline - [ "align-middle", "inline-block", "-mt-px" ] - [ Icons.add 14 Inherit ] - ] - ] - - else - nothing - ] - - -intro : Int -> Html Msg -intro amountOfTracks = - [ text "A source is a place where your music is stored." - , lineBreak - , text "By connecting a source, the application will scan it and keep a list of all the music in it." - , lineBreak - , text "You currently have " - , text (String.fromInt amountOfTracks) - , text " " - , text (ifThenElse (amountOfTracks == 1) "track" "tracks") - , text " in your collection." - ] - |> raw - |> Kit.intro - - -sourceActions : List ( String, Float ) -> Maybe { error : String, sourceId : String } -> Source -> List (Themes.Sunrise.List.Action Msg) -sourceActions processingContext processingError source = - let - processIndex = - List.findIndex (Tuple.first >> (==) source.id) processingContext - - process = - Maybe.andThen (\idx -> List.getAt idx processingContext) processIndex - in - List.append - (case ( process, processingError ) of - ( Just ( _, progress ), _ ) -> - [ { icon = - \_ _ -> - if progress < 0.05 then - inline - [ "inline-block", "opacity-70", "px-1" ] - [ case processIndex of - Just 0 -> - Html.text "Listing" - - _ -> - Html.text "Waiting" - ] - - else - progress - |> (*) 100 - |> round - |> String.fromInt - |> (\s -> s ++ "%") - |> Html.text - |> List.singleton - |> inline [ "inline-block", "opacity-70", "px-1" ] - , msg = Nothing - , title = "" - } - , { icon = Icons.sync - , msg = Nothing - , title = "Currently processing" - } - ] - - ( Nothing, Just { error, sourceId } ) -> - if sourceId == source.id then - [ { icon = \size _ -> Icons.error_outline size (Color Kit.colors.error) - , msg = Nothing - , title = error - } - ] - - else - [] - - _ -> - [] - ) - [ { icon = - if source.enabled then - Icons.check - - else - Icons.block - , msg = - { sourceId = source.id } - |> ToggleActivation - |> always - |> Just - , title = - if source.enabled then - "Enabled (click to disable)" - - else - "Disabled (click to enable)" - } - - -- - , { icon = Icons.more_vert - , msg = Just (SourceContextMenu source) - , title = "Menu" - } - ] - - - --- 🛠 - - -lowercaseName : Source -> String -lowercaseName = - .data >> Dict.fetch "name" "" >> String.toLower diff --git a/src/Core/Themes/Sunrise/Syncing/View.elm b/src/Core/Themes/Sunrise/Syncing/View.elm deleted file mode 100644 index b2843ca80..000000000 --- a/src/Core/Themes/Sunrise/Syncing/View.elm +++ /dev/null @@ -1,187 +0,0 @@ -module Themes.Sunrise.Syncing.View exposing (view) - -import Chunky exposing (..) -import Html exposing (Html, text) -import Html.Attributes as A exposing (attribute, placeholder, style, value) -import Html.Events as E exposing (onSubmit) -import Html.Events.Extra exposing (onClickStopPropagation) -import Html.Extra as Html -import Html.Lazy as Lazy -import Json.Decode -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Themes.Sunrise.Kit as Kit -import UI.Syncing.Types as Syncing exposing (..) -import UI.Types as UI exposing (..) - - - --- 🗺 - - -view : Model -> Html UI.Msg -view = - Html.map SyncingMsg << Lazy.lazy view_ << .syncing - - -view_ : State -> Html Syncing.Msg -view_ state = - case state of - InputScreen _ opts -> - Kit.receptacle - { scrolling = False } - [ inputScreen opts ] - - NewEncryptionKeyScreen method pass -> - Kit.receptacle - { scrolling = False } - [ encryptionKeyScreen - { ableToCancel = False - , withEncryption = ActivateSyncWithPassphrase method (Maybe.withDefault "" pass) - , withoutEncryption = ActivateSync method - } - ] - - UpdateEncryptionKeyScreen method pass -> - Kit.receptacle - { scrolling = False } - [ encryptionKeyScreen - { ableToCancel = True - , withEncryption = UpdateEncryptionKey method (Maybe.withDefault "" pass) - , withoutEncryption = RemoveEncryptionKey method - } - ] - - _ -> - Html.nothing - - - --- ENCRYPTION KEY - - -encryptionKeyScreen : { ableToCancel : Bool, withEncryption : Syncing.Msg, withoutEncryption : Syncing.Msg } -> Html Syncing.Msg -encryptionKeyScreen { ableToCancel, withEncryption, withoutEncryption } = - Kit.focusScreen - { icon = Icons.lock - , iconHref = Nothing - , text = [ text "Optional passphrase to encrypt/decrypt your data" ] - , textHref = Nothing - } - [ slab - Html.form - [ onSubmit withEncryption ] - [ "flex" - , "flex-col" - , "max-w-xs" - , "px-3" - , "w-screen" - - -- - , "sm:px-0" - ] - [ Kit.textArea - [ attribute "autocapitalize" "none" - , attribute "autocomplete" "off" - , attribute "autocorrect" "off" - , attribute "rows" "4" - , attribute "spellcheck" "false" - - -- - , placeholder "anQLS9Usw24gxUi11IgVBg76z8SCWZgLKkoWIeJ1ClVmBHLRlaiA0CtvONVAMGritbgd3U45cPTxrhFU0WXaOAa8pVt186KyEccfUNyAq97" - - -- - , style "-webkit-text-security" "disc" - - -- - , A.class "shadow" - , E.onInput KeepPassphraseInMemory - ] - , chunk - [ "flex" - , "items-stretch" - , "space-x-2.5" - ] - [ Kit.buttonWithOptions - Html.button - [ A.class "flex-1" ] - Kit.Gray - Kit.Filled - (Just Syncing.Bypass) - (text "Continue") - - -- - , if ableToCancel then - Kit.buttonWithOptions - Html.button - [ A.class "flex-1" - , E.stopPropagationOn "click" (Json.Decode.succeed ( CancelInput, True )) - ] - Kit.Gray - Kit.Normal - Nothing - (text "Cancel") - - else - nothing - ] - , brick - [ onClickStopPropagation withoutEncryption ] - [ "cursor-pointer" - , "flex" - , "items-center" - , "justify-center" - , "leading-snug" - , "mt-3" - , "opacity-50" - , "text-xs" - ] - [ inline [ "inline-block", "leading-none", "mr-2" ] [ Icons.warning 13 Inherit ] - , text "Continue without encryption" - ] - ] - ] - - - --- INPUT SCREEN - - -inputScreen : Question -> Html Syncing.Msg -inputScreen question = - Kit.focusScreen - { icon = question.icon - , iconHref = Nothing - , text = [ Kit.askForInput question.question ] - , textHref = Nothing - } - [ slab - Html.form - [ onSubmit ConfirmInput ] - [ "flex" - , "flex-col" - , "max-w-xs" - , "px-3" - , "w-screen" - - -- - , "sm:px-0" - ] - [ Kit.textFieldAlt - [ attribute "autocapitalize" "off" - , placeholder question.placeholder - , A.class "shadow" - , E.onInput Input - , value question.value - ] - , Kit.button - Kit.Filled - Syncing.Bypass - (text "Continue") - ] - ] - - - --- SPEECH BUBBLE --- 🖼 diff --git a/src/Core/Themes/Sunrise/Theme.elm b/src/Core/Themes/Sunrise/Theme.elm deleted file mode 100644 index 9bce3a782..000000000 --- a/src/Core/Themes/Sunrise/Theme.elm +++ /dev/null @@ -1,292 +0,0 @@ -module Themes.Sunrise.Theme exposing (theme) - -import Alfred exposing (Alfred) -import Chunky exposing (..) -import Common exposing (Switch(..)) -import Conditional exposing (..) -import ContextMenu exposing (ContextMenu) -import Html exposing (Html, section) -import Html.Attributes exposing (class, style) -import Html.Events exposing (on) -import Html.Lazy as Lazy -import Json.Decode -import Material.Icons as Icons -import Maybe.Extra as Maybe -import Theme exposing (Theme) -import Themes.Sunrise.Alfred.View as Alfred -import Themes.Sunrise.Console -import Themes.Sunrise.ContextMenu as ContextMenu -import Themes.Sunrise.Navigation as Navigation -import Themes.Sunrise.Notifications as Notifications -import Themes.Sunrise.Playlists.View as Playlists -import Themes.Sunrise.Queue.View as Queue -import Themes.Sunrise.Settings as Settings -import Themes.Sunrise.Sources.View as Sources -import Themes.Sunrise.Syncing.View as Syncing -import Themes.Sunrise.Tracks.View as Tracks -import UI.Backdrop as Backdrop -import UI.Page as Page -import UI.Settings.Page -import UI.Sources.Page -import UI.Syncing.Common as Syncing -import UI.Types exposing (..) -import User.Layer - - -theme : Theme Msg Model -theme = - { id = "sunrise" - , title = "Sunrise" - , icon = Icons.wb_sunny - , view = view - } - - - --- ㊙️ - - -view : Model -> Html Msg -view model = - section - (if Maybe.isJust model.contextMenu || Maybe.isJust model.alfred then - [ on "tap" (Json.Decode.succeed HideOverlay) ] - - else if model.showVolumeSlider then - [ on "tap" (Json.Decode.succeed <| ToggleVolumeSlider Off) ] - - else if model.isDragging then - [ class "dragging-something" - , on "mouseup" (Json.Decode.succeed StoppedDragging) - , on "touchcancel" (Json.Decode.succeed StoppedDragging) - , on "touchend" (Json.Decode.succeed StoppedDragging) - ] - - else if Maybe.isJust model.selectedQueueItem then - [ on "tap" (Json.Decode.succeed RemoveQueueSelection) ] - - else if not (List.isEmpty model.selectedTrackIndexes) then - [ on "tap" (Json.Decode.succeed RemoveTrackSelection) ] - - else - [] - ) - [ ----------------------------------------- - -- Alfred - ----------------------------------------- - Lazy.lazy2 Alfred.view model.alfred model.extractedBackdropColor - - ----------------------------------------- - -- Backdrop - ----------------------------------------- - , Lazy.lazy Backdrop.view model - - ----------------------------------------- - -- Context Menu - ----------------------------------------- - , Lazy.lazy2 ContextMenu.view model.viewport.width model.contextMenu - - ----------------------------------------- - -- Notifications - ----------------------------------------- - , Lazy.lazy2 Notifications.view model.extractedBackdropColor model.notifications - - ----------------------------------------- - -- Overlay - ----------------------------------------- - , Lazy.lazy2 overlay model.alfred model.contextMenu - - ----------------------------------------- - -- Content - ----------------------------------------- - , content - { justifyCenter = False - , scrolling = not model.isDragging - } - (defaultScreen model) - ] - - -defaultScreen : Model -> List (Html Msg) -defaultScreen model = - [ Lazy.lazy2 - (Navigation.global - [ ( Page.Index, "Tracks" ) - , ( Page.Sources UI.Sources.Page.Index, "Sources" ) - , ( Page.Settings UI.Settings.Page.Index, "Settings" ) - ] - ) - model.alfred - model.page - - ----------------------------------------- - -- Main - ----------------------------------------- - , vessel - [ Tracks.view model - - -- Pages - -------- - , case model.page of - Page.Index -> - nothing - - Page.Playlists subPage -> - Lazy.lazy6 - Playlists.view - subPage - model.playlists - model.selectedPlaylist - model.editPlaylistContext - model.extractedBackdropColor - (model.syncing - |> Syncing.extractMethod - |> Maybe.unwrap False User.Layer.methodSupportsPublicData - ) - - Page.Queue subPage -> - Queue.view subPage model - - Page.Settings subPage -> - Lazy.lazy2 Settings.view - subPage - { buildTimestamp = model.buildTimestamp - , chosenBackgroundImage = model.chosenBackdrop - , coverSelectionReducesPool = model.coverSelectionReducesPool - , currentTimeZone = model.currentTimeZone - , extractedBackdropColor = model.extractedBackdropColor - , hideDuplicateTracks = model.hideDuplicates - , lastFm = model.lastFm - , processAutomatically = model.processAutomatically - , rememberProgress = model.rememberProgress - , serviceWorkerStatus = model.serviceWorkerStatus - , syncMethod = Syncing.extractMethod model.syncing - , version = model.version - } - - Page.Sources subPage -> - Sources.view subPage model - - -- Syncing - ---------- - , Syncing.view model - ] - - ----------------------------------------- - -- Controls - ----------------------------------------- - , Themes.Sunrise.Console.view - model.nowPlaying - model.repeat - model.shuffle - ] - - - --- 🗺 ░░ BITS - - -content : { justifyCenter : Bool, scrolling : Bool } -> List (Html Msg) -> Html Msg -content { justifyCenter, scrolling } nodes = - brick - [ on "focusout" (Json.Decode.succeed Blur) - , on "focusin" inputFocusDecoder - ] - [ "overflow-x-hidden" - , "relative" - , "screen-height" - , "scrolling-touch" - , "w-screen" - , "z-10" - - -- - , ifThenElse scrolling "overflow-y-auto" "overflow-y-hidden" - ] - [ brick - [ style "min-width" "280px" ] - [ "flex" - , "flex-col" - , "items-center" - , "h-full" - , "px-3" - - -- - , "md:px-8" - , "lg:px-16" - - -- - , ifThenElse justifyCenter "justify-center" "justify-start" - ] - nodes - ] - - -inputFocusDecoder : Json.Decode.Decoder Msg -inputFocusDecoder = - Json.Decode.string - |> Json.Decode.at [ "target", "tagName" ] - |> Json.Decode.andThen - (\targetTagName -> - case targetTagName of - "INPUT" -> - Json.Decode.succeed FocusedOnInput - - "TEXTAREA" -> - Json.Decode.succeed FocusedOnInput - - _ -> - Json.Decode.fail "NOT_INPUT" - ) - - -overlay : Maybe (Alfred Msg) -> Maybe (ContextMenu Msg) -> Html Msg -overlay maybeAlfred maybeContextMenu = - let - isShown = - Maybe.isJust maybeAlfred || Maybe.isJust maybeContextMenu - in - brick - [] - [ "inset-0" - , "bg-darkest-hour" - , "duration-500" - , "ease-in-out" - , "fixed" - , "transition-opacity" - , "z-30" - - -- - , ifThenElse isShown "pointer-events-auto" "pointer-events-none" - , ifThenElse isShown "opacity-50" "opacity-0" - ] - [] - - -vessel : List (Html Msg) -> Html Msg -vessel = - brick - [ style "-webkit-mask-image" "-webkit-radial-gradient(white, black)" ] - [ "bg-white" - , "flex" - , "flex-col" - , "flex-grow" - , "overflow-hidden" - , "relative" - , "rounded" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - ] - >> bricky - [ style "min-height" "296px" ] - [ "flex" - , "flex-grow" - , "rounded" - , "shadow-lg" - , "w-full" - - -- - , "lg:max-w-insulation" - , "lg:min-w-3xl" - ] diff --git a/src/Core/Themes/Sunrise/Tracks/Scene.elm b/src/Core/Themes/Sunrise/Tracks/Scene.elm deleted file mode 100644 index ac2859411..000000000 --- a/src/Core/Themes/Sunrise/Tracks/Scene.elm +++ /dev/null @@ -1,75 +0,0 @@ -module Themes.Sunrise.Tracks.Scene exposing (..) - -import Chunky exposing (..) -import Conditional exposing (..) -import Html exposing (Html, text) -import Html.Attributes as A -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Tracks - - - --- 🗺 - - -group : { index : Int } -> Tracks.Identifiers -> Html msg -group { index } identifiers = - let - groupName = - identifiers.group - |> Maybe.map .name - |> Maybe.withDefault "Unknown" - in - brick - [ A.style "height" "18px" ] - [ "box-content" - , "font-display" - , "font-semibold" - , "leading-normal" - , "pb-3" - , "px-4" - , "text-base04" - , "text-xxs" - , "tracking-tad-further" - , "truncate" - - -- - , ifThenElse (0 == index) "pt-3" "pt-4" - ] - [ groupIcon - , inline [ "align-middle" ] [ text groupName ] - ] - - -shadow : Html msg -shadow = - chunk - [ "h-10" - , "left-0" - , "-mt-10" - , "-translate-y-full" - , "opacity-30" - , "right-0" - , "shadow-md" - , "sticky" - , "top-0" - , "transform" - , "z-10" - - -- Dark mode - ------------ - , "dark:shadow-md-darker" - ] - [] - - - --- ㊙️ - - -groupIcon : Html msg -groupIcon = - inline - [ "align-middle", "inline-block", "leading-0", "pr-2" ] - [ Icons.library_music 16 Inherit ] diff --git a/src/Core/Themes/Sunrise/Tracks/Scene/Covers.elm b/src/Core/Themes/Sunrise/Tracks/Scene/Covers.elm deleted file mode 100644 index d814c36b5..000000000 --- a/src/Core/Themes/Sunrise/Tracks/Scene/Covers.elm +++ /dev/null @@ -1,881 +0,0 @@ -module Themes.Sunrise.Tracks.Scene.Covers exposing (containerId, scrollToNowPlaying, scrollToTop, view) - -import Browser.Dom as Dom -import Chunky exposing (..) -import Color exposing (Color) -import Conditional exposing (ifThenElse) -import Coordinates -import Dict exposing (Dict) -import Html exposing (Html, text) -import Html.Attributes as A exposing (class, id, style, tabindex) -import Html.Events as E -import Html.Events.Extra.Mouse as Mouse -import Html.Lazy -import InfiniteList -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import Task -import Themes.Sunrise.Tracks.Scene as Scene -import Themes.Sunrise.Tracks.Scene.List -import Tracks exposing (..) -import UI.Tracks.Types exposing (Msg(..)) -import UI.Types as UI exposing (Msg(..)) - - - --- 🗺 - - -type alias Dependencies = - { bgColor : Maybe Color - , cachedCovers : Maybe (Dict String String) - , covers : List Cover - , darkMode : Bool - , favouritesOnly : Bool - , infiniteList : InfiniteList.Model - , isVisible : Bool - , nowPlaying : Maybe IdentifiedTrack - , selectedCover : Maybe Cover - , selectedTrackIndexes : List Int - , sortBy : SortBy - , sortDirection : SortDirection - , viewportHeight : Float - , viewportWidth : Float - } - - -type alias ItemDependencies = - { cachedCovers : Maybe (Dict String String) - , columns : Int - , containerWidth : Int - , nowPlaying : Maybe IdentifiedTrack - , sortBy : SortBy - } - - -view : Dependencies -> Html Msg -view deps = - Html.Lazy.lazy view_ deps - - -view_ : Dependencies -> Html Msg -view_ deps = - chunk - [ "flex" - , "flex-basis-0" - , "flex-col" - , "flex-grow" - , "relative" - ] - [ collectionView deps - , case deps.selectedCover of - Just cover -> - singleCoverView cover deps - - Nothing -> - nothing - ] - - - --- 🏞 ░░ COLLECTION - - -collectionView : Dependencies -> Html Msg -collectionView deps = - let - amountOfCovers = - List.length deps.covers - in - brick - (tabindex (ifThenElse deps.isVisible 0 -1) :: viewAttributes) - [ "flex-basis-0" - , "flex-grow" - , "outline-none" - , "overflow-x-hidden" - , "overflow-y-auto" - , "relative" - , "scrolling-touch" - , "text-almost-sm" - ] - [ Scene.shadow - , chunk - [ "bg-white" - , "flex" - , "items-center" - , "pt-5" - , "px-5" - , "relative" - , "z-20" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - ] - [ sortGroupButtons deps.sortBy - - -- - , chunk - [ "flex" - , "flex-auto" - , "items-center" - , "justify-end" - , "text-base05" - , "text-right" - , "text-xs" - ] - [ text (String.fromInt amountOfCovers) - , case deps.sortBy of - Album -> - text " albums" - - Artist -> - text " artists" - - _ -> - nothing - , text " " - , slab - Html.span - [ deps.sortBy - |> SortBy - |> TracksMsg - |> E.onClick - - -- - , case deps.sortDirection of - Asc -> - A.title "Sorted alphabetically ascending" - - Desc -> - A.title "Sorted alphabetically descending" - ] - [ "cursor-pointer" - , "ml-1" - , "opacity-60" - ] - [ case deps.sortDirection of - Asc -> - Icons.arrow_downward 16 Inherit - - Desc -> - Icons.arrow_upward 16 Inherit - ] - ] - ] - - -- - , infiniteListView deps - ] - - -containerId : String -containerId = - "diffuse__track-covers" - - -scrollToNowPlaying : Float -> List Cover -> IdentifiedTrack -> Cmd Msg -scrollToNowPlaying viewportWidth covers nowPlaying = - let - columns = - determineColumns viewportWidth - - containerWidth = - determineContainerWidth viewportWidth - - rowHeightArgs = - { columns = columns - , containerWidth = containerWidth - } - - { rows, nowPlayingRowIndex } = - coverRows (Just nowPlaying) columns covers - in - case nowPlayingRowIndex of - Just idx -> - rows - |> List.take idx - |> List.foldl (\a -> (+) <| dynamicRowHeight rowHeightArgs 0 a) 0 - |> toFloat - |> (+) 11 - |> Dom.setViewportOf containerId 0 - |> Task.attempt (always Bypass) - - Nothing -> - Cmd.none - - -scrollToTop : Cmd Msg -scrollToTop = - Task.attempt (always UI.Bypass) (Dom.setViewportOf containerId 0 0) - - -viewAttributes : List (Html.Attribute Msg) -viewAttributes = - [ InfiniteList.onScroll (InfiniteListMsg >> TracksMsg) - , id containerId - ] - - - --- 🏞 ░░ SINGLE COVER - - -singleCoverView : Cover -> Dependencies -> Html Msg -singleCoverView cover deps = - let - derivedColors = - Themes.Sunrise.Tracks.Scene.List.deriveColors - { bgColor = deps.bgColor - , darkMode = deps.darkMode - } - - columns = - determineColumns deps.viewportWidth - - condensedView = - columns < 4 - in - brick - [ tabindex (ifThenElse deps.isVisible 0 -1) ] - [ "absolute" - , "bg-white" - , "flex-basis-0" - , "flex-grow" - , "inset-0" - , "leading-tight" - , "outline-none" - , "overflow-x-hidden" - , "overflow-y-auto" - , "text-almost-sm" - , "z-30" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - ] - [ chunk - [ "flex" - , "font-semibold" - , "h-8" - , "items-center" - , "leading-none" - , "-ml-2" - , "mt-5" - , "px-5" - ] - [ headerButton - [ E.onClick (TracksMsg DeselectCover) ] - { active = False - , label = Icons.arrow_back 16 Inherit - } - - -- - , headerButton - [ Mouse.onClick (showCoverMenu cover) ] - { active = True - , label = Icons.more_horiz 16 Inherit - } - ] - - -- - , chunk - [ "mb-6" - , "-top-px" - , "mt-4" - , "relative" - - -- - , ifThenElse condensedView "block" "flex" - , ifThenElse condensedView "mx-5" "ml-5" - ] - [ itemView - { clickable = False, horizontal = condensedView } - (compileItemDependencies deps) - cover - - -- - , cover.tracks - |> List.indexedMap - (Themes.Sunrise.Tracks.Scene.List.defaultItemView - { derivedColors = derivedColors - , favouritesOnly = deps.favouritesOnly - , nowPlaying = deps.nowPlaying - , roundedCorners = True - , selectedTrackIndexes = deps.selectedTrackIndexes - , showAlbum = not cover.sameAlbum - , showArtist = deps.sortBy /= Artist && not cover.sameArtist - , showGroup = False - } - 0 - ) - |> chunk - [ ifThenElse condensedView "px-px" "px-0" ] - |> List.singleton - |> chunk - [ "flex-auto" - , "flex-basis-0" - , "overflow-hidden" - , "select-none" - - -- - , ifThenElse condensedView "-mx-5" "mx-5" - , ifThenElse condensedView "px-1" "px-0" - ] - ] - ] - - - --- 🧕 - - -headerButton attributes { active, label } = - brick - attributes - [ "cursor-pointer" - , "inline-flex" - , "h-8" - , "items-center" - , "overflow-hidden" - , "px-2" - , "rounded" - - -- - , ifThenElse active "bg-gray-300" "bg-transparent" - , ifThenElse active "dark:bg-base01" "dark:bg-transparent" - ] - [ chunk - [ "mt-px", "pt-px" ] - [ label ] - ] - - -showCoverMenu : Cover -> Mouse.Event -> Msg -showCoverMenu cover = - .clientPos - >> Coordinates.fromTuple - >> (TracksMsg << ShowCoverMenuWithSmallDelay cover) - - - --- SORTING - - -sortGroupButtons : SortBy -> Html Msg -sortGroupButtons sortBy = - chunk - [ "flex" - , "h-8" - , "items-center" - , "leading-none" - , "mr-3" - , "text-xs" - , "tracking-tad-further" - ] - [ sortGroupButton - { current = sortBy, btn = Artist } - (chunk - [ "inline-flex", "items-center" ] - [ inline [ "mr-px" ] [ Icons.people_alt 16 Inherit ] - , inline [ "ml-1", "mt-px", "pl-px", "pt-px" ] [ text "Artists" ] - ] - ) - - -- - , sortGroupButton - { current = sortBy, btn = Album } - (chunk - [ "inline-flex", "items-center" ] - [ inline [ "mr-px" ] [ Icons.album 16 Inherit ] - , inline [ "ml-1", "mt-px", "pt-px" ] [ text "Albums" ] - ] - ) - ] - - -sortGroupButton : { current : SortBy, btn : SortBy } -> Html Msg -> Html Msg -sortGroupButton { current, btn } label = - headerButton - [ btn - |> SortBy - |> TracksMsg - |> E.onClick - - -- - , class "mr-1" - ] - { active = current == btn - , label = label - } - - - --- INFINITE LIST - - -infiniteListView : Dependencies -> Html Msg -infiniteListView deps = - let - itemDeps = - compileItemDependencies deps - - rowHeightArgs = - { columns = itemDeps.columns - , containerWidth = itemDeps.containerWidth - } - in - { itemView = rowView itemDeps - , itemHeight = InfiniteList.withVariableHeight (dynamicRowHeight rowHeightArgs) - , containerHeight = round deps.viewportHeight - 262 - } - |> InfiniteList.config - |> InfiniteList.withCustomContainer infiniteListContainer - |> (\config -> - InfiniteList.view - config - deps.infiniteList - (deps.covers - |> coverRows Nothing itemDeps.columns - |> .rows - ) - ) - - -infiniteListContainer : - List ( String, String ) - -> List (Html msg) - -> Html msg -infiniteListContainer styles = - styles - |> List.filterMap - (\( k, v ) -> - if k == "padding" then - Nothing - - else - Just (style k v) - ) - |> List.append listStyles - |> Html.div - - -compileItemDependencies : Dependencies -> ItemDependencies -compileItemDependencies deps = - { cachedCovers = deps.cachedCovers - , columns = determineColumns deps.viewportWidth - , containerWidth = determineContainerWidth deps.viewportWidth - , nowPlaying = deps.nowPlaying - , sortBy = deps.sortBy - } - - -listStyles : List (Html.Attribute msg) -listStyles = - [ class "leading-tight" - , class "pl-5" - , class "pt-4" - ] - - - --- ROWS - - -determineContainerWidth : Float -> Int -determineContainerWidth viewportWidth = - min 768 (round viewportWidth - 32) - - -dynamicRowHeight : { columns : Int, containerWidth : Int } -> Int -> List Cover -> Int -dynamicRowHeight { columns, containerWidth } _ coverRow = - let - rowHeight = - (containerWidth - 16) // columns + (46 + 16) - in - let - shouldRenderGroup = - coverRow - |> List.head - |> Maybe.andThen (.tracks >> List.head) - |> Maybe.map (Tuple.first >> Tracks.shouldRenderGroup) - |> Maybe.withDefault False - in - if shouldRenderGroup then - 42 + rowHeight - - else - rowHeight - - -coverRows : - Maybe IdentifiedTrack - -> Int - -> List Cover - -> { nowPlayingRowIndex : Maybe Int, rows : List (List Cover) } -coverRows maybeNowPlaying columns covers = - covers - |> List.foldl - (\cover { collection, current, nowPlayingRowIdx, trackGroup } -> - let - trackGroupCurr = - cover.identifiedTrackCover - |> Tuple.first - |> .group - |> Maybe.map .name - - npr addition = - case ( maybeNowPlaying, nowPlayingRowIdx ) of - ( Just ( _, t ), Nothing ) -> - if List.member t.id cover.trackIds then - Just (List.length collection + ifThenElse addition 1 0) - - else - Nothing - - _ -> - nowPlayingRowIdx - in - if List.length current < columns && (Maybe.isNothing trackGroup || trackGroupCurr == trackGroup) then - { collection = collection - , current = current ++ [ cover ] - , nowPlayingRowIdx = npr False - , trackGroup = trackGroupCurr - } - - else - { collection = collection ++ [ current ] - , current = [ cover ] - , nowPlayingRowIdx = npr True - , trackGroup = trackGroupCurr - } - ) - { current = [] - , collection = [] - , nowPlayingRowIdx = Nothing - , trackGroup = Nothing - } - |> (\foldResult -> - { nowPlayingRowIndex = foldResult.nowPlayingRowIdx - , rows = foldResult.collection ++ [ foldResult.current ] - } - ) - - -rowView : - ItemDependencies - -> Int - -> Int - -> List Cover - -> Html Msg -rowView itemDeps _ idx row = - let - maybeIdentifiers = - row - |> List.head - |> Maybe.andThen (.tracks >> List.head) - |> Maybe.map Tuple.first - - shouldRenderGroup = - maybeIdentifiers - |> Maybe.map Tracks.shouldRenderGroup - |> Maybe.withDefault False - in - raw - [ case ( shouldRenderGroup, maybeIdentifiers ) of - ( True, Just identifiers ) -> - chunk - [ "-ml-4" ] - [ Scene.group { index = idx } identifiers ] - - _ -> - nothing - - -- - , chunk - [ "flex", "flex-wrap" ] - (List.map (itemView { clickable = True, horizontal = False } itemDeps) row) - ] - - - --- ITEMS / COLUMNS - - -determineColumns : Float -> Int -determineColumns viewportWidth = - let - containerWidth = - determineContainerWidth viewportWidth - in - if containerWidth < 260 then - 1 - - else if containerWidth < 480 then - 2 - - else if containerWidth <= 590 then - 3 - - else - 4 - - -type alias ItemViewOptions = - { clickable : Bool, horizontal : Bool } - - -itemView : ItemViewOptions -> ItemDependencies -> Cover -> Html Msg -itemView options deps cover = - chunk - [ "antialiased" - , "flex-shrink-0" - , "font-semibold" - - -- - , ifThenElse options.horizontal "flex" "block" - , ifThenElse options.horizontal "mb-0" "mb-5" - - -- - , case ( options.horizontal, deps.columns ) of - ( True, _ ) -> - "w-auto" - - ( False, 1 ) -> - "w-full" - - ( False, 2 ) -> - "w-1/2" - - ( False, 3 ) -> - "w-1/3" - - _ -> - "w-1/4" - ] - [ coverView options deps cover - , metadataView options deps cover - ] - - -coverView : ItemViewOptions -> ItemDependencies -> Cover -> Html Msg -coverView { clickable, horizontal } { cachedCovers, nowPlaying } cover = - let - nowPlayingId = - Maybe.unwrap "" (Tuple.second >> .id) nowPlaying - - missingTracks = - List.any - (Tuple.first >> .isMissing) - cover.tracks - - maybeBlobUrlFromCache = - cachedCovers - |> Maybe.withDefault Dict.empty - |> Dict.get cover.key - - hasBackgroundImage = - Maybe.isJust maybeBlobUrlFromCache && not missingTracks - - bgOrDataAttributes = - case ( missingTracks, maybeBlobUrlFromCache ) of - ( True, _ ) -> - [] - - ( False, Just blobUrl ) -> - [ A.style "background-image" ("url('" ++ blobUrl ++ "')") - ] - - ( False, Nothing ) -> - if Maybe.isJust cachedCovers then - let - ( identifiers, track ) = - cover.identifiedTrackCover - in - [ A.attribute "data-key" cover.key - , A.attribute "data-filename" identifiers.filename - , A.attribute "data-path" track.path - , A.attribute "data-source-id" track.sourceId - , A.attribute "data-various-artists" (ifThenElse cover.variousArtists "t" "f") - ] - - else - [] - in - chunk - [ "flex-shrink-0" - , "mr-5" - , "relative" - - -- - , ifThenElse clickable "cursor-pointer" "cursor-default" - , ifThenElse horizontal "h-32" "h-0" - , ifThenElse horizontal "mb-4" "pt-full" - , ifThenElse horizontal "w-32" "w-auto" - ] - [ brick - (List.append - bgOrDataAttributes - (if clickable then - [ E.onClick (TracksMsg <| SelectCover cover) - , Mouse.onContextMenu (showCoverMenu cover) - ] - - else - [] - ) - ) - [ "absolute" - , "bg-cover" - , "bg-gray-300" - , "mb-5" - , "inset-0" - , "rounded-md" - , "shadow" - - -- - , ifThenElse horizontal "h-32" "h-auto" - - -- Dark mode - ------------ - , "dark:bg-white-025" - ] - [ if not hasBackgroundImage then - chunk - [ "absolute" - , "left-1/2" - , "-translate-x-1/2" - , "-translate-y-1/2" - , "text-gray-400" - , "top-1/2" - , "transform" - - -- Dark mode - ------------ - , "dark:text-base01" - ] - [ Icons.album 26 Inherit ] - - else - nothing - - -- Now playing? - , if List.member nowPlayingId cover.trackIds then - let - dropShadow = - "drop-shadow(hsla(0, 0%, 0%, 0.275) 0px 0px 2.5px)" - in - brick - [ style "-webkit-filter" dropShadow - , style "filter" dropShadow - ] - [ "absolute" - , "bottom-0" - , "mb-3" - , "mr-3" - , "right-0" - , "text-white" - ] - [ Icons.headset 16 Inherit ] - - else - nothing - ] - ] - - -metadataView : ItemViewOptions -> ItemDependencies -> Cover -> Html Msg -metadataView { clickable, horizontal } { sortBy } cover = - let - { identifiedTrackCover } = - cover - - ( _, track ) = - identifiedTrackCover - - missingTracks = - List.any - (Tuple.first >> .isMissing) - cover.tracks - in - brick - (if clickable then - [ E.onClick (TracksMsg <| SelectCover cover) - , Mouse.onContextMenu (showCoverMenu cover) - ] - - else - [] - ) - [ "mr-5" - , "relative" - , "tracking-tad-closer" - , "z-10" - - -- - , ifThenElse clickable "cursor-pointer" "cursor-default" - , ifThenElse horizontal "mt-0" "-mt-5" - , ifThenElse horizontal "overflow-hidden" "overflow-auto" - , ifThenElse horizontal "pt-0" "pt-2" - ] - [ chunk - [ "mt-px" - , "pt-px" - , "truncate" - ] - [ case sortBy of - Album -> - if missingTracks then - text "Missing tracks" - - else - text (Maybe.withDefault "Unknown album" track.tags.album) - - Artist -> - if missingTracks then - text "Missing tracks" - - else - text (Maybe.withDefault "Unknown artist" track.tags.artist) - - _ -> - nothing - ] - - -- - , chunk - [ "mt-px" - , "pt-px" - , "text-base05" - , "text-xs" - , "truncate" - ] - [ case sortBy of - Album -> - if cover.variousArtists then - text "Various Artists" - - else if not missingTracks && Maybe.isJust track.tags.artist then - text (Maybe.withDefault "" track.tags.artist) - - else - case List.length cover.trackIds of - 1 -> - text "1 track" - - n -> - text (String.fromInt n ++ " tracks") - - Artist -> - case List.length cover.trackIds of - 1 -> - text "1 track" - - n -> - text (String.fromInt n ++ " tracks") - - _ -> - nothing - ] - ] diff --git a/src/Core/Themes/Sunrise/Tracks/Scene/List.elm b/src/Core/Themes/Sunrise/Tracks/Scene/List.elm deleted file mode 100644 index f3dd46eaf..000000000 --- a/src/Core/Themes/Sunrise/Tracks/Scene/List.elm +++ /dev/null @@ -1,851 +0,0 @@ -module Themes.Sunrise.Tracks.Scene.List exposing (Dependencies, DerivedColors, containerId, defaultItemView, deriveColors, scrollToNowPlaying, scrollToTop, view) - -import Browser.Dom as Dom -import Chunky exposing (..) -import Color exposing (Color) -import Color.Manipulate as Color -import Conditional exposing (ifThenElse) -import Coordinates -import Html exposing (Html, text) -import Html.Attributes exposing (class, id, style, tabindex) -import Html.Events -import Html.Events.Extra.Mouse as Mouse -import Html.Lazy -import InfiniteList -import Json.Decode as Decode -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import Task -import Themes.Sunrise.Kit as Kit -import Themes.Sunrise.Tracks.Scene as Scene -import Tracks exposing (..) -import UI.DnD as DnD -import UI.Queue.Types as Queue -import UI.Tracks.Types exposing (Msg(..)) -import UI.Types as UI exposing (Msg(..)) - - - --- 🗺 - - -type alias Dependencies = - { bgColor : Maybe Color - , darkMode : Bool - , height : Float - , isTouchDevice : Bool - , isVisible : Bool - , showAlbum : Bool - } - - -type alias DerivedColors = - { background : String - , subtle : String - , text : String - } - - -view : Dependencies -> List IdentifiedTrack -> InfiniteList.Model -> Bool -> Maybe IdentifiedTrack -> Maybe String -> SortBy -> SortDirection -> List Int -> Maybe (DnD.Model Int) -> Html Msg -view deps harvest infiniteList favouritesOnly nowPlaying searchTerm sortBy sortDirection selectedTrackIndexes maybeDnD = - brick - (tabindex (ifThenElse deps.isVisible 0 -1) :: viewAttributes) - [ "flex-basis-0" - , "flex-grow" - , "outline-none" - , "overflow-x-hidden" - , "relative" - , "select-none" - , "scrolling-touch" - , "text-xs" - - -- - , "md:text-almost-sm" - - -- - , case maybeDnD of - Just dnd -> - if deps.isTouchDevice && DnD.isDragging dnd then - "overflow-y-hidden" - - else - "overflow-y-auto" - - Nothing -> - "overflow-y-auto" - ] - [ Scene.shadow - - -- Header - --------- - , Html.Lazy.lazy4 - header - (Maybe.isJust maybeDnD) - deps.showAlbum - sortBy - sortDirection - - -- List - ------- - , Html.Lazy.lazy7 - infiniteListView - deps - harvest - infiniteList - favouritesOnly - searchTerm - ( nowPlaying, selectedTrackIndexes ) - maybeDnD - ] - - -containerId : String -containerId = - "diffuse__track-list" - - -scrollToNowPlaying : List IdentifiedTrack -> IdentifiedTrack -> Cmd Msg -scrollToNowPlaying harvest ( identifiers, _ ) = - harvest - |> List.take identifiers.indexInList - |> List.foldl (\a -> (+) <| dynamicRowHeight 0 a) 0 - |> (\n -> 22 - toFloat rowHeight / 2 + 2 + toFloat n) - |> Dom.setViewportOf containerId 0 - |> Task.attempt (always Bypass) - - -scrollToTop : Cmd Msg -scrollToTop = - Task.attempt (always UI.Bypass) (Dom.setViewportOf containerId 0 0) - - -viewAttributes : List (Html.Attribute Msg) -viewAttributes = - [ InfiniteList.onScroll (InfiniteListMsg >> TracksMsg) - , id containerId - , class "overscroll-none" - ] - - - --- HEADERS - - -header : Bool -> Bool -> SortBy -> SortDirection -> Html Msg -header isPlaylist showAlbum sortBy sortDirection = - let - sortIcon = - (if sortDirection == Desc then - Icons.expand_less - - else - Icons.expand_more - ) - 15 - Inherit - - maybeSortIcon s = - ifThenElse (sortBy == s) (Just sortIcon) Nothing - in - chunk - [ "antialiased" - , "bg-white" - , "border-b" - , "border-gray-300" - , "flex" - , "font-semibold" - , "relative" - , "text-base06" - , "text-xxs" - , "z-20" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:border-base01" - , "dark:text-base03" - ] - (if isPlaylist && showAlbum then - [ headerColumn "" 4.5 Nothing Bypass - , headerColumn "#" 4.5 Nothing Bypass - , headerColumn "Title" 36.0 Nothing Bypass - , headerColumn "Artist" 27.5 Nothing Bypass - , headerColumn "Album" 27.5 Nothing Bypass - ] - - else if isPlaylist then - [ headerColumn "" 4.5 Nothing Bypass - , headerColumn "#" 4.5 Nothing Bypass - , headerColumn "Title" 49.75 Nothing Bypass - , headerColumn "Artist" 41.25 Nothing Bypass - ] - - else if showAlbum then - [ headerColumn "" 4.5 Nothing Bypass - , headerColumn "Title" 37.5 (maybeSortIcon Title) (TracksMsg <| SortBy Title) - , headerColumn "Artist" 29.0 (maybeSortIcon Artist) (TracksMsg <| SortBy Artist) - , headerColumn "Album" 29.0 (maybeSortIcon Album) (TracksMsg <| SortBy Album) - ] - - else - [ headerColumn "" 5.75 Nothing Bypass - , headerColumn "Title" 51.25 (maybeSortIcon Title) (TracksMsg <| SortBy Title) - , headerColumn "Artist" 43 (maybeSortIcon Artist) (TracksMsg <| SortBy Artist) - ] - ) - - - --- HEADER COLUMN - - -headerColumn : String -> Float -> Maybe (Html Msg) -> Msg -> Html Msg -headerColumn text_ width maybeSortIcon msg = - brick - [ Html.Events.onClick msg - - -- - , style "min-width" columnMinWidth - , style "width" (String.fromFloat width ++ "%") - ] - [ "border-l" - , "border-gray-300" - , "leading-relaxed" - , "pl-2" - , "pr-2" - , "pt-px" - , "relative" - - -- - , case msg of - Bypass -> - "cursor-default" - - _ -> - "cursor-pointer" - - -- - , "first:border-l-0" - , "first:cursor-default" - , "first:pl-4" - , "last:pr-4" - - -- Dark mode - ------------ - , "dark:border-base01" - ] - [ chunk - [ "mt-px", "opacity-90", "pt-px" ] - [ Html.text text_ ] - , case maybeSortIcon of - Just sortIcon -> - chunk - [ "absolute" - , "-translate-y-1/2" - , "mr-1" - , "opacity-90" - , "right-0" - , "top-1/2" - , "transform" - ] - [ sortIcon ] - - Nothing -> - nothing - ] - - - --- INFINITE LIST - - -infiniteListView : Dependencies -> List IdentifiedTrack -> InfiniteList.Model -> Bool -> Maybe String -> ( Maybe IdentifiedTrack, List Int ) -> Maybe (DnD.Model Int) -> Html Msg -infiniteListView deps harvest infiniteList favouritesOnly searchTerm ( nowPlaying, selectedTrackIndexes ) maybeDnD = - let - derivedColors = - deriveColors { bgColor = deps.bgColor, darkMode = deps.darkMode } - in - { itemView = - case maybeDnD of - Just dnd -> - playlistItemView - favouritesOnly - nowPlaying - searchTerm - selectedTrackIndexes - dnd - deps.showAlbum - deps.darkMode - derivedColors - - _ -> - defaultItemView - { derivedColors = derivedColors - , favouritesOnly = favouritesOnly - , nowPlaying = nowPlaying - , roundedCorners = False - , selectedTrackIndexes = selectedTrackIndexes - , showAlbum = deps.showAlbum - , showArtist = True - , showGroup = True - } - - -- - , itemHeight = InfiniteList.withVariableHeight dynamicRowHeight - , containerHeight = round deps.height - } - |> InfiniteList.config - |> InfiniteList.withCustomContainer infiniteListContainer - |> (\config -> - InfiniteList.view - config - infiniteList - harvest - ) - - -infiniteListContainer : - List ( String, String ) - -> List (Html msg) - -> Html msg -infiniteListContainer styles = - styles - |> List.filterMap - (\( k, v ) -> - if k == "padding" then - Nothing - - else - Just (style k v) - ) - |> List.append listStyles - |> Html.div - - -deriveColors : { bgColor : Maybe Color, darkMode : Bool } -> DerivedColors -deriveColors { bgColor, darkMode } = - let - color = - Maybe.withDefault Kit.colors.text bgColor - in - if darkMode then - { background = Color.toCssString color - , subtle = Color.toCssString (Color.darken 0.1 color) - , text = Color.toCssString (Color.darken 0.475 color) - } - - else - { background = Color.toCssString (Color.fadeOut 0.625 color) - , subtle = Color.toCssString (Color.fadeOut 0.575 color) - , text = Color.toCssString (Color.darken 0.3 color) - } - - -listStyles : List (Html.Attribute msg) -listStyles = - [ class "pb-2 pt-1" ] - - -dynamicRowHeight : Int -> IdentifiedTrack -> Int -dynamicRowHeight _ ( i, t ) = - if Tracks.shouldRenderGroup i then - 16 + 18 + 12 + rowHeight - - else - rowHeight - - - --- INFINITE LIST ITEM - - -defaultItemView : - { derivedColors : DerivedColors - , favouritesOnly : Bool - , nowPlaying : Maybe IdentifiedTrack - , roundedCorners : Bool - , selectedTrackIndexes : List Int - , showAlbum : Bool - , showArtist : Bool - , showGroup : Bool - } - -> Int - -> Int - -> IdentifiedTrack - -> Html Msg -defaultItemView args _ idx identifiedTrack = - let - { derivedColors, favouritesOnly, nowPlaying, roundedCorners, selectedTrackIndexes, showAlbum, showArtist, showGroup } = - args - - ( identifiers, track ) = - identifiedTrack - - shouldRenderGroup = - showGroup && Tracks.shouldRenderGroup identifiers - - isSelected = - List.member idx selectedTrackIndexes - - isOddRow = - modBy 2 idx == 1 - - rowIdentifiers = - { isMissing = identifiers.isMissing - , isNowPlaying = Maybe.unwrap False (isNowPlaying identifiedTrack) nowPlaying - , isSelected = isSelected - } - - favIdentifiers = - { indexInList = identifiers.indexInList - , isFavourite = identifiers.isFavourite - , isNowPlaying = rowIdentifiers.isNowPlaying - , isSelected = isSelected - } - in - Html.div - [] - [ if shouldRenderGroup then - Scene.group { index = idx } identifiers - - else - nothing - - -- - , brick - (List.concat - [ rowStyles idx rowIdentifiers derivedColors - - -- - , List.append - (if isSelected then - [ touchContextMenuEvent identifiedTrack Nothing ] - - else - [] - ) - [ mouseContextMenuEvent identifiedTrack - , playEvent identifiedTrack - , selectEvent idx - ] - ] - ) - [ "flex" - , "items-center" - - -- - , ifThenElse identifiers.isMissing "cursor-default" "cursor-pointer" - , ifThenElse isSelected "font-semibold" "font-normal" - , ifThenElse roundedCorners "rounded" "border-r-0" - - -- - , ifThenElse - isOddRow - "bg-white" - "bg-gray-100" - - -- Dark mode - ------------ - , ifThenElse - isOddRow - "dark:bg-darkest-hour" - "dark:bg-near-darkest-hour" - ] - (if not showArtist && not showAlbum then - [ favouriteColumn "5.75%" favouritesOnly favIdentifiers derivedColors - , otherColumn "94.25%" False track.tags.title - ] - - else if not showArtist && showAlbum then - [ favouriteColumn "5.75%" favouritesOnly favIdentifiers derivedColors - , otherColumn "51.25%" False track.tags.title - , otherColumn "43%" False (Maybe.withDefault fallbackAlbum track.tags.album) - ] - - else if showArtist && not showAlbum then - [ favouriteColumn "5.75%" favouritesOnly favIdentifiers derivedColors - , otherColumn "51.25%" False track.tags.title - , otherColumn "43%" False (Maybe.withDefault fallbackArtist track.tags.artist) - ] - - else - [ favouriteColumn defFavColWidth favouritesOnly favIdentifiers derivedColors - , otherColumn "37.5%" False track.tags.title - , otherColumn "29.0%" False (Maybe.withDefault fallbackArtist track.tags.artist) - , otherColumn "29.0%" True (Maybe.withDefault fallbackAlbum track.tags.album) - ] - ) - ] - - -playlistItemView : Bool -> Maybe IdentifiedTrack -> Maybe String -> List Int -> DnD.Model Int -> Bool -> Bool -> DerivedColors -> Int -> Int -> IdentifiedTrack -> Html Msg -playlistItemView favouritesOnly nowPlaying _ selectedTrackIndexes dnd showAlbum darkMode derivedColors _ idx identifiedTrack = - let - ( identifiers, track ) = - identifiedTrack - - listIdx = - identifiers.indexInList - - dragEnv = - { model = dnd - , toMsg = DnD - } - - isSelected = - List.member idx selectedTrackIndexes - - isOddRow = - modBy 2 idx == 1 - - rowIdentifiers = - { isMissing = identifiers.isMissing - , isNowPlaying = Maybe.unwrap False (isNowPlaying identifiedTrack) nowPlaying - , isSelected = isSelected - } - - favIdentifiers = - { indexInList = identifiers.indexInList - , isFavourite = identifiers.isFavourite - , isNowPlaying = rowIdentifiers.isNowPlaying - , isSelected = isSelected - } - in - brick - (List.concat - [ rowStyles idx rowIdentifiers derivedColors - - -- - , List.append - (if isSelected then - [ touchContextMenuEvent identifiedTrack (Just dragEnv) - , DnD.listenToStart dragEnv listIdx - ] - - else - [] - ) - [ mouseContextMenuEvent identifiedTrack - , playEvent identifiedTrack - , selectEvent idx - ] - - -- - , DnD.listenToEnterLeave dragEnv listIdx - - -- - , if DnD.isBeingDraggedOver listIdx dnd then - [ dragIndicator darkMode ] - - else - [] - ] - ) - [ "flex" - , "items-center" - - -- - , ifThenElse identifiers.isMissing "cursor-default" "cursor-pointer" - , ifThenElse isSelected "font-semibold" "font-normal" - - -- - , ifThenElse - isOddRow - "bg-white" - "bg-gray-100" - - -- Dark mode - ------------ - , ifThenElse - isOddRow - "dark:bg-darkest-hour" - "dark:bg-near-darkest-hour" - ] - (if showAlbum then - [ favouriteColumn defFavColWidth favouritesOnly favIdentifiers derivedColors - , playlistIndexColumn (Maybe.withDefault 0 identifiers.indexInPlaylist) - , otherColumn "36.0%" False track.tags.title - , otherColumn "27.5%" False (Maybe.withDefault fallbackArtist track.tags.artist) - , otherColumn "27.5%" True (Maybe.withDefault fallbackAlbum track.tags.album) - ] - - else - [ favouriteColumn defFavColWidth favouritesOnly favIdentifiers derivedColors - , playlistIndexColumn (Maybe.withDefault 0 identifiers.indexInPlaylist) - , otherColumn "49.75%" False track.tags.title - , otherColumn "41.25%" False (Maybe.withDefault fallbackArtist track.tags.artist) - ] - ) - - -mouseContextMenuEvent : IdentifiedTrack -> Html.Attribute Msg -mouseContextMenuEvent ( i, _ ) = - Html.Events.custom - "contextmenu" - (Decode.map - (\event -> - { message = - if event.keys.shift then - Bypass - - else - event.clientPos - |> Coordinates.fromTuple - |> ShowTracksMenuWithSmallDelay - (Just i.indexInList) - { alt = event.keys.alt } - |> TracksMsg - - -- - , stopPropagation = True - , preventDefault = True - } - ) - Mouse.eventDecoder - ) - - -touchContextMenuEvent : IdentifiedTrack -> Maybe (DnD.Environment Int Msg) -> Html.Attribute Msg -touchContextMenuEvent ( i, _ ) maybeDragEnv = - Html.Events.custom - "longtap" - (Decode.map2 - (\x y -> - { message = - -- Only show menu when not dragging something - case Maybe.andThen (.model >> DnD.modelTarget) maybeDragEnv of - Just _ -> - Bypass - - Nothing -> - { x = x, y = y } - |> ShowTracksMenu - (Just i.indexInList) - { alt = False } - |> TracksMsg - - -- - , stopPropagation = False - , preventDefault = False - } - ) - (Decode.field "x" Decode.float) - (Decode.field "y" Decode.float) - ) - - -playEvent : IdentifiedTrack -> Html.Attribute Msg -playEvent ( i, t ) = - Html.Events.custom - "dbltap" - (Decode.succeed - { message = - if i.isMissing then - Bypass - - else - ( i, t ) - |> Queue.InjectFirstAndPlay - |> QueueMsg - - -- - , stopPropagation = True - , preventDefault = True - } - ) - - -selectEvent : Int -> Html.Attribute Msg -selectEvent idx = - Html.Events.custom - "tap" - (Decode.map2 - (\shiftKey button -> - { message = - case button of - 0 -> - { shiftKey = shiftKey } - |> MarkAsSelected idx - |> TracksMsg - - _ -> - Bypass - - -- - , stopPropagation = True - , preventDefault = False - } - ) - (Decode.at [ "originalEvent", "shiftKey" ] Decode.bool) - (Decode.oneOf - [ Decode.at [ "originalEvent", "button" ] Decode.int - , Decode.succeed 0 - ] - ) - ) - - - --- ROWS - - -rowHeight : Int -rowHeight = - 35 - - -rowStyles : Int -> { isMissing : Bool, isNowPlaying : Bool, isSelected : Bool } -> DerivedColors -> List (Html.Attribute msg) -rowStyles _ { isMissing, isNowPlaying } derivedColors = - let - bgColor = - if isNowPlaying then - derivedColors.background - - else - "" - - color = - if isNowPlaying then - derivedColors.text - - else if isMissing then - rowFontColors.gray - - else - "" - in - [ style "background-color" bgColor - , style "color" color - , style "height" (String.fromInt rowHeight ++ "px") - ] - - - --- COLUMNS - - -defFavColWidth = - "4.5%" - - -columnMinWidth = - "28px" - - -favouriteColumn : String -> Bool -> { isFavourite : Bool, indexInList : Int, isNowPlaying : Bool, isSelected : Bool } -> DerivedColors -> Html Msg -favouriteColumn columnWidth favouritesOnly identifiers derivedColors = - brick - (List.append - [ style "width" columnWidth - , identifiers.indexInList - |> ToggleFavourite - |> TracksMsg - |> Html.Events.onClick - ] - (favouriteColumnStyles favouritesOnly identifiers derivedColors) - ) - [ "flex-shrink-0" - , "font-normal" - , "pl-4" - , "text-gray-500" - - -- Dark mode - ------------ - , "dark:text-base02" - ] - [ if identifiers.isFavourite then - text "t" - - else - text "f" - ] - - -favouriteColumnStyles : Bool -> { isFavourite : Bool, indexInList : Int, isNowPlaying : Bool, isSelected : Bool } -> DerivedColors -> List (Html.Attribute msg) -favouriteColumnStyles favouritesOnly { isFavourite, isNowPlaying } derivedColors = - let - color = - if isNowPlaying && isFavourite then - derivedColors.text - - else if isNowPlaying then - derivedColors.subtle - - else if favouritesOnly || not isFavourite then - "" - - else - favColors.red - in - [ style "color" color - , style "font-family" "or-favourites" - , style "min-width" columnMinWidth - ] - - -playlistIndexColumn : Int -> Html msg -playlistIndexColumn indexInPlaylist = - brick - (otherColumnStyles "4.5%") - [ "pl-2" - , "pr-2" - , "pointer-events-none" - , "truncate" - ] - [ indexInPlaylist - |> (+) 1 - |> String.fromInt - |> text - ] - - -otherColumn : String -> Bool -> String -> Html msg -otherColumn width _ text_ = - brick - (otherColumnStyles width) - [ "pl-2" - , "pr-2" - , "pointer-events-none" - , "truncate" - - -- - , "last:pr-4" - ] - [ text text_ ] - - -otherColumnStyles : String -> List (Html.Attribute msg) -otherColumnStyles columnWidth = - [ style "min-width" columnMinWidth - , style "width" columnWidth - ] - - - --- 🖼 - - -favColors = - { gray = Color.toCssString (Color.rgb255 220 220 220) - , red = Color.toCssString Kit.colorKit.base08 - } - - -rowFontColors = - { gray = Color.toCssString Kit.colorKit.base04 - , white = Color.toCssString (Color.rgb 1 1 1) - } - - -dragIndicator : Bool -> Html.Attribute msg -dragIndicator darkMode = - let - color = - if darkMode then - Kit.colors.gray_300 - - else - Kit.colorKit.base03 - in - style "box-shadow" ("0 1px 0 0 " ++ Color.toCssString color ++ " inset") diff --git a/src/Core/Themes/Sunrise/Tracks/View.elm b/src/Core/Themes/Sunrise/Tracks/View.elm deleted file mode 100644 index 9ba7b00fd..000000000 --- a/src/Core/Themes/Sunrise/Tracks/View.elm +++ /dev/null @@ -1,582 +0,0 @@ -module Themes.Sunrise.Tracks.View exposing (view) - -import Chunky exposing (..) -import Color exposing (Color) -import Common exposing (Switch(..)) -import Conditional exposing (ifThenElse) -import Html exposing (Html, text) -import Html.Attributes as A exposing (attribute, placeholder, style, tabindex, title, value) -import Html.Events as E exposing (onClick, onInput) -import Html.Events.Extra.Mouse as Mouse -import Html.Lazy exposing (..) -import Keyboard exposing (Key(..)) -import Material.Icons.Round as Icons -import Material.Icons.Types exposing (Coloring(..)) -import Maybe.Extra as Maybe -import Playlists exposing (Playlist) -import Themes.Sunrise.Kit as Kit -import Themes.Sunrise.Navigation as Navigation -import Themes.Sunrise.Tracks.Scene.Covers -import Themes.Sunrise.Tracks.Scene.List -import Tracks exposing (..) -import UI.Audio.Types exposing (nowPlayingIdentifiedTrack) -import UI.Navigation exposing (..) -import UI.Page as Page -import UI.Playlists.Page -import UI.Queue.Page -import UI.Sources.Page as Sources -import UI.Syncing.Common as Syncing -import UI.Syncing.Types as Syncing -import UI.Tracks.Types exposing (..) -import UI.Types as UI exposing (..) -import User.Layer as User - - - --- 🌳 - - -type alias NavigationProperties = - { bgColor : Maybe Color - , favouritesOnly : Bool - , grouping : Maybe Grouping - , isOnIndexPage : Bool - , pressedShift : Bool - , scene : Scene - , searchTerm : Maybe String - , selectedPlaylist : Maybe Playlist - , showVolumeSlider : Bool - , volume : Float - } - - - --- 🗺 - - -view : Model -> Html UI.Msg -view model = - let - isOnIndexPage = - model.page == Page.Index - in - chunk - viewClasses - [ lazy - navigation - { bgColor = model.extractedBackdropColor - , favouritesOnly = model.favouritesOnly - , grouping = model.grouping - , isOnIndexPage = isOnIndexPage - , pressedShift = List.member Shift model.pressedKeys - , scene = model.scene - , searchTerm = model.searchTerm - , selectedPlaylist = model.selectedPlaylist - , showVolumeSlider = model.showVolumeSlider - , volume = model.eqSettings.volume - } - - -- - , if List.isEmpty model.tracks.harvested then - lazy5 - noTracksView - (List.map Tuple.first model.processingContext) - (List.length model.sources) - (List.length model.tracks.harvested) - (List.length model.favourites) - (Syncing.extractMethod model.syncing) - - else - case model.scene of - Covers -> - Themes.Sunrise.Tracks.Scene.Covers.view - { bgColor = model.extractedBackdropColor - , cachedCovers = model.cachedCovers - , covers = model.covers.harvested - , darkMode = model.darkMode - , favouritesOnly = model.favouritesOnly - , infiniteList = model.infiniteList - , isVisible = isOnIndexPage - , nowPlaying = Maybe.map nowPlayingIdentifiedTrack model.nowPlaying - , selectedCover = model.selectedCover - , selectedTrackIndexes = model.selectedTrackIndexes - , sortBy = model.sortBy - , sortDirection = model.sortDirection - , viewportHeight = model.viewport.height - , viewportWidth = model.viewport.width - } - - List -> - model.selectedPlaylist - |> Maybe.andThen - (\playlist -> - if playlist.collection || Maybe.isJust playlist.autoGenerated then - Nothing - - else - Just model.dnd - ) - |> Themes.Sunrise.Tracks.Scene.List.view - { bgColor = model.extractedBackdropColor - , darkMode = model.darkMode - , height = model.viewport.height - , isTouchDevice = model.isTouchDevice - , isVisible = isOnIndexPage - , showAlbum = model.viewport.width >= 720 - } - model.tracks.harvested - model.infiniteList - model.favouritesOnly - (Maybe.map nowPlayingIdentifiedTrack model.nowPlaying) - model.searchTerm - model.sortBy - model.sortDirection - model.selectedTrackIndexes - ] - - -viewClasses : List String -viewClasses = - [ "flex" - , "flex-col" - , "flex-grow" - , "relative" - ] - - -navigation : NavigationProperties -> Html UI.Msg -navigation { bgColor, favouritesOnly, grouping, isOnIndexPage, pressedShift, scene, searchTerm, selectedPlaylist, showVolumeSlider, volume } = - let - tabindex_ = - ifThenElse isOnIndexPage 0 -1 - in - chunk - [ "relative", "sm:flex" ] - [ ----------------------------------------- - -- Part 1 - ----------------------------------------- - slab - (Html.node "search") - [ attribute "role" "search" ] - [ "border-b" - , "border-r" - , "border-gray-300" - , "flex" - , "flex-grow" - , "h-12" - , "mt-px" - , "px-1" - , "overflow-hidden" - , "relative" - , "text-gray-600" - - -- Responsive - ------------- - , "sm:h-auto" - , "sm:px-0" - - -- Dark mode - ------------ - , "dark:border-base01" - , "dark:text-base04" - ] - [ -- Input - -------- - slab - Html.input - [ attribute "autocorrect" "off" - , attribute "autocapitalize" "none" - , attribute "spellcheck" "false" - , onInput (TracksMsg << SetSearchTerm) - , placeholder "Search" - , tabindex tabindex_ - , value (Maybe.withDefault "" searchTerm) - ] - [ "bg-transparent" - , "border-none" - , "flex-grow" - , "h-full" - , "min-w-0" - , "ml-1" - , "mt-px" - , "outline-none" - , "pl-8" - , "pr-2" - , "pt-px" - , "text-base02" - , "text-sm" - , "w-full" - - -- Dark mode - ------------ - , "dark:text-base06" - ] - [] - - -- Search icon - -------------- - , chunk - [ "absolute" - , "bottom-0" - , "flex" - , "items-center" - , "left-0" - , "ml-3" - , "mt-px" - , "pl-1" - , "top-0" - , "z-0" - - -- Responsive - ------------- - , "sm:pl-0" - ] - [ Icons.search 16 Inherit ] - - -- Actions - ---------- - , chunk - [ "flex" - , "items-center" - , "mr-3" - , "mt-px" - , "pt-px" - , "space-x-4" - - -- Responsive - ------------- - , "sm:space-x-2" - ] - [ -- 1 - case searchTerm of - Just _ -> - brick - [ onClick (TracksMsg ClearSearch) - , title "Clear search" - ] - [ "cursor-pointer" - , "mt-px" - ] - [ Icons.clear 16 Inherit ] - - Nothing -> - nothing - - -- 2 - , brick - [ onClick (TracksMsg ToggleFavouritesOnly) - , title "Toggle favourites-only" - ] - [ "cursor-pointer" ] - [ if favouritesOnly then - Icons.favorite 16 (Color Kit.colorKit.base08) - - else - Icons.favorite_border 16 Inherit - ] - - -- 3 - , case scene of - Covers -> - brick - [ attribute "title" "Switch to list view" - , List - |> ChangeScene - |> TracksMsg - |> onClick - ] - [ "ml-6" - , "mr-px" - , "cursor-pointer" - ] - [ chunk - [ "pl-1" ] - [ Icons.notes 18 Inherit ] - ] - - List -> - brick - [ attribute "title" "Switch to cover view" - , Covers - |> ChangeScene - |> TracksMsg - |> onClick - ] - [ "cursor-pointer" - , "mr-px" - ] - [ chunk - [ "pl-1" ] - [ Icons.burst_mode 20 Inherit ] - ] - - -- 4 - , brick - [ Mouse.onClick (TracksMsg << ShowViewMenu grouping) - , title "View settings" - ] - [ "cursor-pointer" ] - [ Icons.more_vert 16 Inherit ] - - -- 5 - , case selectedPlaylist of - Just playlist -> - brick - [ onClick DeselectPlaylist - , title "Deactivate playlist" - - -- - , bgColor - |> Maybe.withDefault Kit.colorKit.base01 - |> Color.toCssString - |> style "background-color" - ] - [ "antialiased" - , "cursor-pointer" - , "duration-500" - , "font-bold" - , "leading-none" - , "px-1" - , "py-1" - , "rounded" - , "truncate" - , "text-white-90" - , "text-xxs" - , "transition" - - -- Dark mode - ------------ - , "dark:text-white-60" - ] - [ chunk - [ "px-px", "pt-px" ] - [ text playlist.name ] - ] - - Nothing -> - nothing - ] - ] - , ----------------------------------------- - -- Part 2 - ----------------------------------------- - Navigation.localWithTabindex - tabindex_ - [ ( Icon Icons.waves - , Label "Collections & Playlists" Hidden - , if pressedShift then - PerformMsg AssistWithSelectingPlaylist - - else - NavigateToPage (Page.Playlists UI.Playlists.Page.Index) - ) - , ( Icon Icons.schedule - , Label "Queue" Hidden - , if pressedShift then - PerformMsg (ChangeUrlUsingPage <| Page.Queue UI.Queue.Page.History) - - else - NavigateToPage (Page.Queue UI.Queue.Page.Index) - ) - , ( if volume == 0 then - Icon Icons.volume_off - - else if volume < 0.5 then - Icon Icons.volume_down - - else - Icon Icons.volume_up - , Label "Volume" Hidden - , if pressedShift then - if volume == 0 then - PerformMsg (AdjustVolume 0.5) - - else - PerformMsg (AdjustVolume 0) - - else - PerformMsg (ToggleVolumeSlider <| ifThenElse showVolumeSlider Off On) - ) - ] - , ----------------------------------------- - -- Part 3 - ----------------------------------------- - if showVolumeSlider then - chunk - [ "absolute" - , "bg-white" - , "px-4" - , "py-3" - , "right-0" - , "rounded-bl" - , "shadow-lg" - , "top-full" - , "z-40" - - -- Dark mode - ------------ - , "dark:bg-darkest-hour" - , "dark:shadow-[rgba(0,0,0,.175)]" - ] - [ chunk - [ "leading-[0px]" - , "my-1" - , "pt-px" - , "text-[0px]" - ] - [ slab - Html.input - [ A.type_ "range" - , A.min "0" - , A.max "1" - , A.step "0.0125" - , A.value (String.fromFloat volume) - - -- - , E.onBlur SaveEnclosedUserData - , E.onInput (String.toFloat >> Maybe.unwrap Bypass AdjustVolume) - ] - [ "range-slider" ] - [] - ] - ] - - else - nothing - ] - - -noTracksView : List String -> Int -> Int -> Int -> Maybe User.Method -> Html UI.Msg -noTracksView processingContext amountOfSources amountOfTracks _ userLayerMethod = - chunk - [ "no-tracks-view" - - -- - , "flex" - , "flex-grow" - ] - [ Kit.centeredContent - [ if List.length processingContext > 0 then - message "Processing Tracks" - - else if amountOfSources == 0 then - chunk - [ "flex" - , "flex-col" - , "items-center" - , "justify-center" - , "px-3" - ] - [ slab - Html.img - [ A.src "images/diffuse-dark.svg" - , A.width 190 - ] - [ "dark:hidden" ] - [] - - -- - , slab - Html.img - [ A.src "images/diffuse-light.svg" - , A.width 190 - ] - [ "hidden dark:block" ] - [] - - -- - , chunk - [ "italic" - , "max-w-sm" - , "mt-6" - , "text-base05" - , "text-center" - , "text-sm" - - -- Dark mode - ------------ - , "dark:text-base03" - ] - [ Html.text "Play music" - , inline [ "not-italic", "font-normal", "inline-block", "mx-1", "pr-px" ] [ Html.text " ♫ " ] - , Html.text """from your Dropbox, -IPFS node, Amazon S3 bucket, or any other -cloud/distributed storage service you use. - """ - ] - - -- - , chunk - [ "flex", "mt-5", "space-x-3" ] - [ Kit.button - Kit.Normal - InsertDemo - (Html.text "Insert Demo") - , Kit.buttonWithColor - Kit.Accent - Kit.Filled - (ChangeUrlUsingPage <| Page.Sources Sources.New) - (Html.text "Add Music") - , case userLayerMethod of - Just method -> - Kit.buttonWithColor - Kit.Gray - Kit.Filled - (SyncingMsg Syncing.StopSync) - (text "Stop syncing") - - Nothing -> - Kit.buttonWithOptions - Html.button - [ Mouse.onClick (SyncingMsg << Syncing.ShowSyncDataMenu) ] - Kit.Gray - Kit.Filled - Nothing - (Html.text "Sync data") - ] - - -- - , chunk - [ "mt-4" ] - [ slab - Html.a - [ A.href "about/" - , A.target "_blank" - ] - [ "border-b" - , "border-current" - , "inline-block" - , "leading-snug" - , "text-base05" - , "text-xxs" - , "tracking-widest" - , "uppercase" - - -- Dark mode - ------------ - , "dark:text-base03" - ] - [ Html.text "Learn more" ] - ] - ] - - else if amountOfTracks == 0 then - message "No tracks found" - - else - message "No sources available" - ] - ] - - -message : String -> Html UI.Msg -message m = - chunk - [ "border-b-2" - , "border-current-color" - , "text-sm" - , "font-semibold" - , "leading-snug" - , "pb-1" - ] - [ text m ] diff --git a/src/Core/UI.elm b/src/Core/UI.elm deleted file mode 100644 index cbcf546d4..000000000 --- a/src/Core/UI.elm +++ /dev/null @@ -1,741 +0,0 @@ -module UI exposing (main) - -import Alien -import Browser -import Browser.Events -import Browser.Navigation as Nav -import Common exposing (ServiceWorkerStatus(..), Switch(..)) -import Debouncer.Basic as Debouncer -import Dict -import Equalizer -import InfiniteList -import Json.Decode as Json -import Keyboard -import LastFm -import Maybe.Extra as Maybe -import Notifications -import Random -import Return -import Task -import Time -import Tracks -import UI.Adjunct as Adjunct -import UI.Alfred.State as Alfred -import UI.Audio.State as Audio -import UI.Backdrop as Backdrop -import UI.Common.State as Common -import UI.DnD as DnD -import UI.Equalizer.State as Equalizer -import UI.Interface.State as Interface -import UI.Other.State as Other -import UI.Page as Page -import UI.Playlists.State as Playlists -import UI.Ports as Ports -import UI.Queue.State as Queue -import UI.Queue.Types as Queue -import UI.Routing.State as Routing -import UI.Services.State as Services -import UI.Sources.Form -import UI.Sources.State as Sources -import UI.Sources.Types as Sources -import UI.Syncing.State as Syncing -import UI.Syncing.Types as Syncing -import UI.Tracks.State as Tracks -import UI.Tracks.Types as Tracks -import UI.Types exposing (..) -import UI.User.State.Export as User -import UI.User.State.Import as User -import UI.View exposing (view) -import Url exposing (Url) -import Url.Ext as Url - - - --- ⛩ - - -main : Program Flags Model Msg -main = - Browser.application - { init = init - , view = view - , update = update - , subscriptions = subscriptions - , onUrlChange = UrlChanged - , onUrlRequest = LinkClicked - } - - - --- 🌳 - - -init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) -init flags url key = - let - rewrittenUrl = - Page.rewriteUrl url - - maybePage = - Page.fromUrl rewrittenUrl - - page = - Maybe.withDefault Page.Index maybePage - - serviceWorkerStatus = - if flags.isInstallingServiceWorker then - InstallingInitial - - else - Activated - in - { buildTimestamp = flags.buildTimestamp - , confirmation = Nothing - , currentTime = Time.millisToPosix flags.initialTime - , currentTimeZone = Time.utc - , darkMode = flags.darkMode - , downloading = Nothing - , dnd = DnD.initialModel - , focusedOnInput = False - , isDragging = False - , isLoading = True - , isOnline = flags.isOnline - , isTauri = flags.isTauri - , isTouchDevice = False - , lastFm = LastFm.initialModel - , navKey = key - , page = page - , pressedKeys = [] - , processAutomatically = True - , serviceWorkerStatus = serviceWorkerStatus - , theme = Nothing - , uuidSeed = Random.initialSeed flags.initialTime - , url = url - , version = flags.version - , viewport = flags.viewport - - ----------------------------------------- - -- Audio - ----------------------------------------- - , audioElements = [] - , nowPlaying = Nothing - , progress = Dict.empty - , rememberProgress = True - - ----------------------------------------- - -- Backdrop - ----------------------------------------- - , chosenBackdrop = Nothing - , extractedBackdropColor = Nothing - , fadeInBackdrop = True - , loadedBackdrops = [] - - ----------------------------------------- - -- Debouncing - ----------------------------------------- - , preloadDebouncer = - 30 - |> Debouncer.fromSeconds - |> Debouncer.debounce - |> Debouncer.toDebouncer - , progressDebouncer = - 30 - |> Debouncer.fromSeconds - |> Debouncer.throttle - |> Debouncer.emitWhenUnsettled Nothing - |> Debouncer.toDebouncer - , resizeDebouncer = - 0.25 - |> Debouncer.fromSeconds - |> Debouncer.debounce - |> Debouncer.toDebouncer - , searchDebouncer = - 0.5 - |> Debouncer.fromSeconds - |> Debouncer.debounce - |> Debouncer.toDebouncer - - ----------------------------------------- - -- Equalizer - ----------------------------------------- - , eqSettings = Equalizer.defaultSettings - , showVolumeSlider = False - - ----------------------------------------- - -- Instances - ----------------------------------------- - , alfred = Nothing - , contextMenu = Nothing - , notifications = [] - - ----------------------------------------- - -- Playlists - ----------------------------------------- - , editPlaylistContext = Nothing - , lastModifiedPlaylist = Nothing - , newPlaylistContext = Nothing - , playlists = [] - , playlistToActivate = Nothing - , selectedPlaylist = Nothing - - ----------------------------------------- - -- Queue - ----------------------------------------- - , dontPlay = [] - , playedPreviously = [] - , playingNext = [] - , selectedQueueItem = Nothing - - -- - , repeat = False - , shuffle = False - - ----------------------------------------- - -- Sources - ----------------------------------------- - , processingContext = [] - , processingError = Nothing - , processingNotificationId = Nothing - , sources = [] - , sourceForm = UI.Sources.Form.initialModel - - ----------------------------------------- - -- Tracks - ----------------------------------------- - , cachedCovers = Nothing - , cachedTracks = [] - , cachedTracksOnly = False - , cachingTracksInProgress = [] - , covers = { arranged = [], harvested = [] } - , coverSelectionReducesPool = True - , favourites = [] - , favouritesOnly = False - , grouping = Nothing - , hideDuplicates = False - , scene = Tracks.Covers - , searchResults = Nothing - , searchTerm = Nothing - , selectedCover = Nothing - , selectedTrackIndexes = [] - , sortBy = Tracks.Album - , sortDirection = Tracks.Asc - , tracks = Tracks.emptyCollection - - -- List scene - ------------- - , infiniteList = InfiniteList.init - - ----------------------------------------- - -- 🦉 Nested - ----------------------------------------- - , syncing = Syncing.initialModel url - } - |> Routing.transition - page - |> Return.command - (url - |> Syncing.initialCommand - |> Cmd.map SyncingMsg - ) - |> Return.command - (if Maybe.isNothing maybePage then - Routing.resetUrl key url page - - else - case Url.action url of - [ "authenticate", "dropbox" ] -> - Routing.resetUrl key url page - - _ -> - Cmd.none - ) - |> Return.command - (Task.perform SetCurrentTime Time.now) - |> Return.command - (Task.perform SetCurrentTimeZone Time.here) - - - --- 📣 - - -update : Msg -> Model -> ( Model, Cmd Msg ) -update msg = - case msg of - Bypass -> - Return.singleton - - ----------------------------------------- - -- Alfred - ----------------------------------------- - AssignAlfred a -> - Alfred.assign a - - GotAlfredInput a -> - Alfred.gotInput a - - SelectAlfredItem a -> - Alfred.runAction a - - ----------------------------------------- - -- Audio - ----------------------------------------- - AudioDurationChange a -> - Audio.durationChange a - - AudioEnded a -> - Audio.ended a - - AudioError a -> - Audio.error a - - AudioHasLoaded a -> - Audio.hasLoaded a - - AudioIsLoading a -> - Audio.isLoading a - - AudioPlaybackStateChanged a -> - Audio.playbackStateChanged a - - AudioPreloadDebounce a -> - Audio.preloadDebounce update a - - AudioTimeUpdated a -> - Audio.timeUpdated a - - NoteProgress a -> - Audio.noteProgress a - - NoteProgressDebounce a -> - Audio.noteProgressDebounce update a - - Pause -> - Audio.pause - - Play -> - Audio.play - - Seek a -> - Audio.seek a - - Stop -> - Audio.stop - - TogglePlay -> - Audio.playPause - - ToggleRememberProgress -> - Audio.toggleRememberProgress - - ----------------------------------------- - -- Backdrop - ----------------------------------------- - ExtractedBackdropColor a -> - Backdrop.extractedBackdropColor a - - ChooseBackdrop a -> - Backdrop.chooseBackdrop a - - LoadBackdrop a -> - Backdrop.loadBackdrop a - - ----------------------------------------- - -- Equalizer - ----------------------------------------- - AdjustVolume a -> - Equalizer.adjustVolume a - - ToggleVolumeSlider a -> - Equalizer.toggleVolumeSlider a - - ----------------------------------------- - -- Interface - ----------------------------------------- - AssistWithChangingTheme -> - Interface.assistWithChangingTheme - - Blur -> - Interface.blur - - ChangeTheme a -> - Interface.changeTheme a - - ContextMenuConfirmation a b -> - Interface.contextMenuConfirmation a b - - CopyToClipboard a -> - Interface.copyToClipboard a - - DismissNotification a -> - Common.dismissNotification a - - DnD a -> - Interface.dnd a - - FocusedOnInput -> - Interface.focusedOnInput - - HideOverlay -> - Interface.hideOverlay - - LostWindowFocus -> - Interface.lostWindowFocus - - MsgViaContextMenu a -> - Interface.msgViaContextMenu a - - PreferredColorSchemaChanged a -> - Interface.preferredColorSchemaChanged a - - RemoveNotification a -> - Interface.removeNotification a - - RemoveQueueSelection -> - Interface.removeQueueSelection - - RemoveTrackSelection -> - Interface.removeTrackSelection - - ResizeDebounce a -> - Interface.resizeDebounce update a - - ResizedWindow a -> - Interface.resizedWindow a - - SearchDebounce a -> - Interface.searchDebounce update a - - SetIsTouchDevice a -> - Interface.setIsTouchDevice a - - ShowNotification a -> - Common.showNotification a - - StoppedDragging -> - Interface.stoppedDragging - - ToggleLoadingScreen a -> - Common.toggleLoadingScreen a - - ----------------------------------------- - -- Playlists - ----------------------------------------- - ActivatePlaylist a -> - Playlists.activate a - - AddTracksToPlaylist a -> - Playlists.addTracksToPlaylist a - - AssistWithAddingTracksToCollection a -> - Playlists.assistWithAddingTracksToCollection a - - AssistWithAddingTracksToPlaylist a -> - Playlists.assistWithAddingTracksToPlaylist a - - AssistWithSelectingPlaylist -> - Playlists.assistWithSelectingPlaylist - - ConvertCollectionToPlaylist a -> - Playlists.convertCollectionToPlaylist a - - ConvertPlaylistToCollection a -> - Playlists.convertPlaylistToCollection a - - CreateCollection -> - Playlists.createCollection - - CreatePlaylist -> - Playlists.createPlaylist - - DeactivatePlaylist -> - Playlists.deactivate - - DeletePlaylist a -> - Playlists.delete a - - DeselectPlaylist -> - Playlists.deselect - - ModifyPlaylist -> - Playlists.modify - - MoveTrackInSelectedPlaylist a -> - Playlists.moveTrackInSelected a - - RemoveTracksFromPlaylist a b -> - Playlists.removeTracks a b - - SelectPlaylist a -> - Playlists.select a - - SetPlaylistCreationContext a -> - Playlists.setCreationContext a - - SetPlaylistModificationContext a b -> - Playlists.setModificationContext a b - - ShowPlaylistListMenu a b -> - Playlists.showListMenu a b - - TogglePlaylistVisibility a -> - Playlists.toggleVisibility a - - ----------------------------------------- - -- Routing - ----------------------------------------- - ChangeUrlUsingPage a -> - Common.changeUrlUsingPage a - - LinkClicked a -> - Routing.linkClicked a - - OpenUrlOnNewPage a -> - Routing.openUrlOnNewPage a - - PageChanged a -> - Routing.transition a - - UrlChanged a -> - Routing.urlChanged a - - ----------------------------------------- - -- Services - ----------------------------------------- - ConnectLastFm -> - Services.connectLastFm - - DisconnectLastFm -> - Services.disconnectLastFm - - GotLastFmSession a -> - Services.gotLastFmSession a - - Scrobble a -> - Services.scrobble a - - ----------------------------------------- - -- User - ----------------------------------------- - Export -> - User.export - - ImportFile a -> - User.importFile a - - ImportJson a -> - User.importJson a - - InsertDemo -> - User.insertDemo - - LoadEnclosedUserData a -> - User.loadEnclosedUserData a - - LoadHypaethralUserData a -> - User.loadHypaethralUserData a - - RequestImport -> - User.requestImport - - SaveEnclosedUserData -> - User.saveEnclosedUserData - - ----------------------------------------- - -- ⚗️ Adjunct - ----------------------------------------- - KeyboardMsg a -> - Adjunct.keyboardInput a - - ----------------------------------------- - -- 🦉 Nested - ----------------------------------------- - SyncingMsg a -> - Syncing.update a - - QueueMsg a -> - Queue.update a - - SourcesMsg a -> - Sources.update a - - TracksMsg a -> - Tracks.update a - - ----------------------------------------- - -- 📭 Other - ----------------------------------------- - InstalledServiceWorker -> - Other.installedServiceWorker - - InstallingServiceWorker -> - Other.installingServiceWorker - - RedirectToBrain a -> - Other.redirectToBrain a - - ReloadApp -> - Other.reloadApp - - SetCurrentTime a -> - Other.setCurrentTime a - - SetCurrentTimeZone a -> - Other.setCurrentTimeZone a - - SetIsOnline a -> - Other.setIsOnline a - - - --- 📰 - - -subscriptions : Model -> Sub Msg -subscriptions _ = - Sub.batch - [ Ports.fromAlien alien - - ----------------------------------------- - -- Audio - ----------------------------------------- - , Ports.audioDurationChange AudioDurationChange - , Ports.audioEnded AudioEnded - , Ports.audioError AudioError - , Ports.audioPlaybackStateChanged AudioPlaybackStateChanged - , Ports.audioIsLoading AudioIsLoading - , Ports.audioHasLoaded AudioHasLoaded - , Ports.audioTimeUpdated AudioTimeUpdated - , Ports.requestPause (always Pause) - , Ports.requestPlay (always Play) - , Ports.requestPlayPause (always TogglePlay) - , Ports.requestStop (always Stop) - - ----------------------------------------- - -- Backdrop - ----------------------------------------- - , Ports.setAverageBackgroundColor ExtractedBackdropColor - - ----------------------------------------- - -- Interface - ----------------------------------------- - , Browser.Events.onResize Interface.onResize - , Ports.indicateTouchDevice (\_ -> SetIsTouchDevice True) - , Ports.lostWindowFocus (always LostWindowFocus) - , Ports.preferredColorSchemaChanged PreferredColorSchemaChanged - , Ports.showErrorNotification (Notifications.error >> ShowNotification) - , Ports.showStickyErrorNotification (Notifications.stickyError >> ShowNotification) - - ----------------------------------------- - -- Queue - ----------------------------------------- - , Ports.requestNext (\_ -> QueueMsg Queue.Shift) - , Ports.requestPrevious (\_ -> QueueMsg Queue.Rewind) - - ----------------------------------------- - -- Services - ----------------------------------------- - , Ports.scrobble Scrobble - - ----------------------------------------- - -- Tracks - ----------------------------------------- - , Ports.downloadTracksFinished (\_ -> TracksMsg Tracks.DownloadFinished) - , Ports.insertCoverCache (TracksMsg << Tracks.InsertCoverCache) - - ----------------------------------------- - -- 📭 Other - ----------------------------------------- - , Ports.installedNewServiceWorker (\_ -> InstalledServiceWorker) - , Ports.installingNewServiceWorker (\_ -> InstallingServiceWorker) - , Ports.refreshedAccessToken (Alien.broadcast Alien.RefreshedAccessToken >> RedirectToBrain) - , Ports.setIsOnline SetIsOnline - , Sub.map KeyboardMsg Keyboard.subscriptions - , Time.every (60 * 1000) SetCurrentTime - ] - - - --- 👽 - - -alien : Alien.Event -> Msg -alien event = - case ( event.error, Alien.tagFromString event.tag ) of - ( Nothing, Just tag ) -> - translateAlienData tag event.data - - ( Just err, Just tag ) -> - translateAlienError tag event.data err - - _ -> - Bypass - - -translateAlienData : Alien.Tag -> Json.Value -> Msg -translateAlienData tag data = - case tag of - Alien.AddTracks -> - TracksMsg (Tracks.Add data) - - Alien.FinishedProcessingSource -> - SourcesMsg (Sources.FinishedProcessingSource data) - - Alien.FinishedProcessingSources -> - SourcesMsg Sources.FinishedProcessing - - Alien.GotCachedCover -> - TracksMsg (Tracks.GotCachedCover data) - - Alien.HideLoadingScreen -> - ToggleLoadingScreen Off - - Alien.LoadEnclosedUserData -> - LoadEnclosedUserData data - - Alien.LoadHypaethralUserData -> - LoadHypaethralUserData data - - Alien.ReloadTracks -> - TracksMsg (Tracks.Reload data) - - Alien.RemoveTracksByPath -> - TracksMsg (Tracks.RemoveByPaths data) - - Alien.ReportProcessingError -> - SourcesMsg (Sources.ReportProcessingError data) - - Alien.ReportProcessingProgress -> - SourcesMsg (Sources.ReportProcessingProgress data) - - Alien.SearchTracks -> - TracksMsg (Tracks.SetSearchResults data) - - Alien.StartedSyncing -> - SyncingMsg (Syncing.StartedSyncing data) - - Alien.StoreTracksInCache -> - TracksMsg (Tracks.StoredInCache data Nothing) - - Alien.SyncMethod -> - SyncingMsg (Syncing.GotSyncMethod data) - - Alien.UpdateSourceData -> - SourcesMsg (Sources.UpdateSourceData data) - - _ -> - Bypass - - -translateAlienError : Alien.Tag -> Json.Value -> String -> Msg -translateAlienError tag data err = - case tag of - Alien.StoreTracksInCache -> - TracksMsg (Tracks.StoredInCache data <| Just err) - - _ -> - if String.startsWith "There seems to be existing data that's encrypted, I will need the passphrase" err then - SyncingMsg (Syncing.NeedEncryptionKey { error = err }) - - else - ShowNotification (Notifications.stickyError err) diff --git a/src/Core/UI/Adjunct.elm b/src/Core/UI/Adjunct.elm deleted file mode 100644 index 9d8832c93..000000000 --- a/src/Core/UI/Adjunct.elm +++ /dev/null @@ -1,185 +0,0 @@ -module UI.Adjunct exposing (..) - -import Keyboard -import Maybe.Extra as Maybe -import Return -import UI.Alfred.State as Alfred -import UI.Audio.State as Audio -import UI.Commands.State as Commands -import UI.Common.State as Common -import UI.Interface.State exposing (hideOverlay) -import UI.Page as Page -import UI.Playlists.Page as Playlists -import UI.Playlists.State as Playlists -import UI.Queue.Page as Queue -import UI.Queue.State as Queue -import UI.Settings.Page as Settings -import UI.Sources.Page as Sources -import UI.Tracks.State as Tracks -import UI.Types exposing (..) - - - --- 📣 - - -keyboardInput : Keyboard.Msg -> Manager -keyboardInput msg model = - (\m -> - let - skip = - Return.singleton m - in - if m.focusedOnInput && Maybe.isNothing model.alfred then - case m.pressedKeys of - [ Keyboard.Escape ] -> - hideOverlay m - - _ -> - skip - - else if Maybe.isJust model.alfred then - case m.pressedKeys of - [ Keyboard.ArrowDown ] -> - Alfred.selectNext m - - [ Keyboard.ArrowUp ] -> - Alfred.selectPrevious m - - [ Keyboard.Enter ] -> - Alfred.runSelectedAction m - - [ Keyboard.Escape ] -> - hideOverlay m - - -- Meta key - -- - [ Keyboard.Character "K", Keyboard.Meta ] -> - Commands.showPalette m - - -- Ctrl key - -- - [ Keyboard.Character "K", Keyboard.Control ] -> - Commands.showPalette m - - [ Keyboard.Character "L", Keyboard.Control ] -> - Playlists.assistWithSelectingPlaylist m - - _ -> - skip - - else - case m.pressedKeys of - [ Keyboard.Character "[", Keyboard.Control ] -> - Queue.rewind m - - [ Keyboard.Character "]", Keyboard.Control ] -> - Queue.shift m - - [ Keyboard.Character "{", Keyboard.Shift, Keyboard.Control ] -> - case m.nowPlaying of - Just { duration, item, playbackPosition } -> - case duration of - Just d -> - Audio.seek - { trackId = (Tuple.second item.identifiedTrack).id - , progress = (playbackPosition - 10) / d - } - m - - Nothing -> - Return.singleton m - - Nothing -> - Return.singleton m - - [ Keyboard.Character "}", Keyboard.Shift, Keyboard.Control ] -> - case m.nowPlaying of - Just { duration, item, playbackPosition } -> - case duration of - Just d -> - Audio.seek - { trackId = (Tuple.second item.identifiedTrack).id - , progress = (playbackPosition + 10) / d - } - m - - Nothing -> - Return.singleton m - - Nothing -> - Return.singleton m - - -- Meta key - -- - [ Keyboard.Character "K", Keyboard.Meta ] -> - Commands.showPalette m - - -- Ctrl key - -- - [ Keyboard.Character "K", Keyboard.Control ] -> - Commands.showPalette m - - [ Keyboard.Character "L", Keyboard.Control ] -> - Playlists.assistWithSelectingPlaylist m - - [ Keyboard.Character "N", Keyboard.Control ] -> - Tracks.scrollToNowPlaying m - - [ Keyboard.Character "P", Keyboard.Control ] -> - Audio.playPause m - - [ Keyboard.Character "R", Keyboard.Control ] -> - Queue.toggleRepeat m - - [ Keyboard.Character "S", Keyboard.Control ] -> - Queue.toggleShuffle m - - -- - [ Keyboard.Character "1", Keyboard.Control ] -> - Common.changeUrlUsingPage Page.Index m - - [ Keyboard.Character "2", Keyboard.Control ] -> - Common.changeUrlUsingPage (Page.Playlists Playlists.Index) m - - [ Keyboard.Character "3", Keyboard.Control ] -> - Common.changeUrlUsingPage (Page.Queue Queue.Index) m - - [ Keyboard.Character "8", Keyboard.Control ] -> - Common.changeUrlUsingPage (Page.Sources Sources.Index) m - - [ Keyboard.Character "9", Keyboard.Control ] -> - Common.changeUrlUsingPage (Page.Settings Settings.Index) m - - -- - [ Keyboard.Escape ] -> - if Maybe.isJust m.contextMenu then - Return.singleton { m | contextMenu = Nothing } - - else if Maybe.isJust m.confirmation then - Return.singleton { m | confirmation = Nothing } - - else if Maybe.isJust m.alfred then - Return.singleton { m | alfred = Nothing } - - else if Maybe.isJust m.selectedCover then - Return.singleton { m | selectedCover = Nothing } - - else - case m.page of - Page.Playlists Playlists.Index -> - Return.singleton { m | page = Page.Index } - - Page.Playlists _ -> - Return.singleton { m | page = Page.Playlists Playlists.Index } - - Page.Queue _ -> - Return.singleton { m | page = Page.Index } - - _ -> - skip - - _ -> - skip - ) - { model | pressedKeys = Keyboard.update msg model.pressedKeys } diff --git a/src/Core/UI/Alfred/State.elm b/src/Core/UI/Alfred/State.elm deleted file mode 100644 index 0e646ebd4..000000000 --- a/src/Core/UI/Alfred/State.elm +++ /dev/null @@ -1,162 +0,0 @@ -module UI.Alfred.State exposing (..) - -import Alfred exposing (Alfred) -import Browser.Dom as Dom -import Keyboard -import Process -import Return exposing (return) -import Return.Ext as Return -import Task -import UI.Types as UI exposing (Manager) - - - --- 📣 - - -assign : Alfred UI.Msg -> Manager -assign instance model = - let - pressedKeys = - List.filter - (\k -> - case k of - Keyboard.Meta -> - True - - Keyboard.Control -> - True - - _ -> - False - ) - model.pressedKeys - in - 250 - |> Process.sleep - |> Task.andThen (\_ -> Dom.focus "diffuse__alfred") - |> Task.andThen (\_ -> Dom.setViewportOf "alfred__results" 0 0) - |> Task.attempt (\_ -> UI.Bypass) - -- The "K" key seems to stick when using CMD + K, - -- aka. Meta key + K, to show the command palette. - -- https://github.com/ohanhi/keyboard/issues/14 - |> return { model | alfred = Just instance, pressedKeys = pressedKeys } - - -gotInput : String -> Manager -gotInput searchTerm model = - model.alfred - |> Maybe.map (determineResults searchTerm) - |> (\a -> Return.singleton { model | alfred = a }) - - -runAction : Int -> Manager -runAction index model = - case model.alfred of - Just instance -> - { result = Alfred.getAt index instance - , searchTerm = instance.searchTerm - } - |> instance.action - |> List.map Return.task - |> Cmd.batch - |> return { model | alfred = Nothing } - - Nothing -> - Return.singleton { model | alfred = Nothing } - - -runSelectedAction : Manager -runSelectedAction model = - case model.alfred of - Just instance -> - runAction instance.focus model - - Nothing -> - Return.singleton model - - -scrollToFocus : Manager -scrollToFocus model = - let - task = - Task.map3 - (\innerE outerE outerV -> - outerV.viewport.y + innerE.element.y - outerE.element.y - 9 - ) - (Dom.getElement "alfred__results__focus") - (Dom.getElement "alfred__results") - (Dom.getViewportOf "alfred__results") - in - task - |> Task.andThen (\a -> Dom.setViewportOf "alfred__results" 0 a) - |> Task.attempt (\_ -> UI.Bypass) - |> return model - - -selectNext : Manager -selectNext model = - case model.alfred of - Just instance -> - let - total = - Alfred.length instance - in - instance - |> (\i -> { i | focus = min (i.focus + 1) (total - 1) }) - |> (\i -> { model | alfred = Just i }) - |> scrollToFocus - - Nothing -> - Return.singleton model - - -selectPrevious : Manager -selectPrevious model = - case model.alfred of - Just instance -> - instance - |> (\i -> { i | focus = max (i.focus - 1) 0 }) - |> (\i -> { model | alfred = Just i }) - |> scrollToFocus - - Nothing -> - Return.singleton model - - - --- ⚗️ - - -determineResults : String -> Alfred UI.Msg -> Alfred UI.Msg -determineResults searchTerm alfred = - let - lowerSearchTerm = - searchTerm - |> String.toLower - |> String.trim - in - if String.length lowerSearchTerm > 0 then - { alfred - | focus = 0 - , searchTerm = - Just searchTerm - , results = - List.map - (\group -> - group.items - |> List.filter - (.title >> String.toLower >> String.contains lowerSearchTerm) - |> (\items -> - { group | items = items } - ) - ) - alfred.index - } - - else - { alfred - | focus = 0 - , searchTerm = Nothing - , results = alfred.index - } diff --git a/src/Core/UI/Audio/State.elm b/src/Core/UI/Audio/State.elm deleted file mode 100644 index ffe0dfe78..000000000 --- a/src/Core/UI/Audio/State.elm +++ /dev/null @@ -1,374 +0,0 @@ -module UI.Audio.State exposing (..) - -import Base64 -import Common exposing (boolToString) -import Debouncer.Basic as Debouncer -import Dict -import LastFm -import List.Extra as List -import MediaSession -import Return exposing (return) -import Return.Ext as Return exposing (communicate) -import Tracks -import UI.Audio.Types exposing (..) -import UI.Common.State as Common -import UI.Common.Types exposing (DebounceManager) -import UI.Ports as Ports -import UI.Queue.State as Queue -import UI.Types as UI exposing (Manager, Msg(..)) -import UI.User.State.Export as User - - - --- 📣 ░░ EVENTS - - -durationChange : DurationChangeEvent -> Manager -durationChange { trackId, duration } = - onlyIfMatchesNowPlaying - { trackId = trackId } - (\nowPlaying -> - if nowPlaying.duration == Nothing || nowPlaying.duration /= Just duration then - durationChange_ { trackId = trackId, duration = duration } nowPlaying - - else - -- Ignore repeating events - Return.singleton - ) - - -durationChange_ : DurationChangeEvent -> NowPlaying -> Manager -durationChange_ { trackId, duration } nowPlaying model = - let - ( identifiers, track ) = - nowPlaying.item.identifiedTrack - - maybeCover = - List.find - (\c -> List.member trackId c.trackIds) - model.covers.arranged - - coverPrep = - Maybe.map - (\cover -> - { cacheKey = Base64.encode (Tracks.coverKey cover.variousArtists track) - , trackFilename = identifiers.filename - , trackPath = track.path - , trackSourceId = track.sourceId - , variousArtists = boolToString cover.variousArtists - } - ) - maybeCover - - coverLoaded = - case ( maybeCover, model.cachedCovers ) of - ( Just cover, Just cachedCovers ) -> - let - key = - Base64.encode (Tracks.coverKey cover.variousArtists track) - in - Dict.member key cachedCovers - - _ -> - False - - metadata = - { album = track.tags.album - , artist = track.tags.artist - , title = track.tags.title - - -- - , coverPrep = coverPrep - } - in - model - |> replaceNowPlaying { nowPlaying | coverLoaded = coverLoaded, duration = Just duration } - |> Return.command (Ports.setMediaSessionMetadata metadata) - |> Return.command (Ports.resetScrobbleTimer { duration = duration, trackId = trackId }) - |> Return.andThen (notifyScrobblersOfTrackPlaying { duration = duration }) - - -error : ErrorAudioEvent -> Manager -error { trackId, code } = - onlyIfMatchesNowPlaying - { trackId = trackId } - (\nowPlaying -> - replaceNowPlaying - (case code of - 2 -> - { nowPlaying | loadingState = NetworkError } - - 3 -> - { nowPlaying | loadingState = DecodeError } - - 4 -> - { nowPlaying | loadingState = NotSupportedError } - - _ -> - nowPlaying - ) - ) - - -ended : GenericAudioEvent -> Manager -ended { trackId } = - onlyIfMatchesNowPlaying - { trackId = trackId } - (\nowPlaying model -> - if model.repeat then - Return.command - (case nowPlaying.duration of - Just duration -> - Ports.resetScrobbleTimer { duration = duration, trackId = trackId } - - Nothing -> - Cmd.none - ) - (play model) - - else - Return.andThen - (if Maybe.map (\d -> Tracks.shouldNoteProgress { duration = d }) nowPlaying.duration == Just True then - noteProgress { trackId = trackId, progress = 1.0 } - - else - Return.singleton - ) - (Queue.shift model) - ) - - -hasLoaded : GenericAudioEvent -> Manager -hasLoaded { trackId } = - onlyIfMatchesNowPlaying - { trackId = trackId } - (\nowPlaying -> - replaceNowPlaying { nowPlaying | loadingState = Loaded } - ) - - -isLoading : GenericAudioEvent -> Manager -isLoading { trackId } = - onlyIfMatchesNowPlaying - { trackId = trackId } - (\nowPlaying -> - replaceNowPlaying { nowPlaying | loadingState = Loading } - ) - - -playbackStateChanged : PlaybackStateEvent -> Manager -playbackStateChanged { trackId, isPlaying } = - onlyIfMatchesNowPlaying - { trackId = trackId } - (\nowPlaying model -> - { model | nowPlaying = Just { nowPlaying | isPlaying = isPlaying } } - |> Return.singleton - |> Return.command - (if isPlaying then - Ports.startScrobbleTimer () - - else - Ports.pauseScrobbleTimer () - ) - |> Return.command - (Ports.setMediaSessionPlaybackState - (if isPlaying then - MediaSession.states.playing - - else - MediaSession.states.paused - ) - ) - ) - - -timeUpdated : TimeUpdatedEvent -> Manager -timeUpdated { trackId, currentTime, duration } = - onlyIfMatchesNowPlaying - { trackId = trackId } - (\nowPlaying model -> - let - dur = - Maybe.withDefault 0 duration - in - { model | nowPlaying = Just { nowPlaying | duration = duration, playbackPosition = currentTime } } - |> (if Tracks.shouldNoteProgress { duration = dur } then - { trackId = trackId - , progress = currentTime / dur - } - |> NoteProgress - |> Debouncer.provideInput - |> NoteProgressDebounce - |> Return.task - |> Return.communicate - - else - Return.singleton - ) - |> Return.command - (case duration of - Just d -> - Ports.setMediaSessionPositionState - { currentTime = currentTime - , duration = d - } - - Nothing -> - Cmd.none - ) - ) - - - --- 📣 ░░ COMMANDS - - -pause : Manager -pause model = - case model.nowPlaying of - Just { item } -> - communicate - (Ports.pause - { trackId = (Tuple.second item.identifiedTrack).id - } - ) - model - - Nothing -> - Return.singleton model - - -playPause : Manager -playPause model = - case model.nowPlaying of - Just { isPlaying } -> - if isPlaying then - pause model - - else - play model - - Nothing -> - play model - - -play : Manager -play model = - case model.nowPlaying of - Just { item } -> - communicate - (Ports.play - { trackId = (Tuple.second item.identifiedTrack).id - , volume = model.eqSettings.volume - } - ) - model - - Nothing -> - Queue.shift model - - -seek : { trackId : String, progress : Float } -> Manager -seek { trackId, progress } = - { percentage = progress, trackId = trackId } - |> Ports.seek - |> Return.communicate - - -stop : Manager -stop model = - model.audioElements - |> List.filter (.isPreload >> (==) True) - |> (\a -> { model | audioElements = a }) - |> Queue.changeActiveItem Nothing - |> Return.effect_ - (\m -> - Ports.renderAudioElements - { items = m.audioElements - , play = Nothing - , volume = m.eqSettings.volume - } - ) - - - --- 📣 - - -noteProgress : { trackId : String, progress : Float } -> Manager -noteProgress { trackId, progress } model = - let - updatedProgressTable = - if not model.rememberProgress then - model.progress - - else if progress > 0.975 then - Dict.remove trackId model.progress - - else - Dict.insert trackId progress model.progress - in - if model.rememberProgress then - User.saveProgress { model | progress = updatedProgressTable } - - else - Return.singleton model - - -noteProgressDebounce : DebounceManager -noteProgressDebounce = - Common.debounce - .progressDebouncer - (\d m -> { m | progressDebouncer = d }) - UI.NoteProgressDebounce - - -notifyScrobblersOfTrackPlaying : { duration : Float } -> Manager -notifyScrobblersOfTrackPlaying { duration } model = - case model.nowPlaying of - Just { item } -> - { duration = round duration - , msg = UI.Bypass - , track = Tuple.second item.identifiedTrack - } - |> LastFm.nowPlaying model.lastFm - |> return model - - Nothing -> - Return.singleton model - - -preloadDebounce : DebounceManager -preloadDebounce = - Common.debounce - .preloadDebouncer - (\d m -> { m | preloadDebouncer = d }) - UI.AudioPreloadDebounce - - -toggleRememberProgress : Manager -toggleRememberProgress model = - User.saveSettings { model | rememberProgress = not model.rememberProgress } - - - --- 🛠️ - - -onlyIfMatchesNowPlaying : { trackId : String } -> (NowPlaying -> Manager) -> Manager -onlyIfMatchesNowPlaying { trackId } fn model = - case model.nowPlaying of - Just ({ item } as nowPlaying) -> - if trackId == (Tuple.second item.identifiedTrack).id then - fn nowPlaying model - - else - Return.singleton model - - Nothing -> - Return.singleton model - - -replaceNowPlaying : NowPlaying -> Manager -replaceNowPlaying np model = - Return.singleton { model | nowPlaying = Just np } diff --git a/src/Core/UI/Audio/Types.elm b/src/Core/UI/Audio/Types.elm deleted file mode 100644 index 6b58a0174..000000000 --- a/src/Core/UI/Audio/Types.elm +++ /dev/null @@ -1,69 +0,0 @@ -module UI.Audio.Types exposing (..) - -import Queue -import Tracks exposing (IdentifiedTrack) - - - --- 🌳 - - -type AudioLoadingState - = Loading - | Loaded - -- Errors - | DecodeError - | NetworkError - | NotSupportedError - - -type alias CoverPrep = - { cacheKey : String - , trackFilename : String - , trackPath : String - , trackSourceId : String - , variousArtists : String - } - - -type alias NowPlaying = - { coverLoaded : Bool - , duration : Maybe Float - , isPlaying : Bool - , item : Queue.Item - , loadingState : AudioLoadingState - , playbackPosition : Float - } - - - --- 🌳 ░░ EVENTS - - -type alias DurationChangeEvent = - { trackId : String, duration : Float } - - -type alias ErrorAudioEvent = - { trackId : String, code : Int } - - -type alias GenericAudioEvent = - { trackId : String } - - -type alias PlaybackStateEvent = - { trackId : String, isPlaying : Bool } - - -type alias TimeUpdatedEvent = - { trackId : String, currentTime : Float, duration : Maybe Float } - - - --- 🛠️ - - -nowPlayingIdentifiedTrack : NowPlaying -> IdentifiedTrack -nowPlayingIdentifiedTrack { item } = - item.identifiedTrack diff --git a/src/Core/UI/Backdrop.elm b/src/Core/UI/Backdrop.elm deleted file mode 100644 index 2bc1481c4..000000000 --- a/src/Core/UI/Backdrop.elm +++ /dev/null @@ -1,251 +0,0 @@ -module UI.Backdrop exposing (..) - -import Chunky exposing (..) -import Color -import Html exposing (Html) -import Html.Attributes exposing (src, style) -import Html.Events exposing (on) -import Html.Lazy as Lazy -import Json.Decode -import Return exposing (return) -import UI.Ports as Ports -import UI.Types exposing (..) -import UI.User.State.Export as User - - - --- ⛩ - - -default : String -default = - "21.jpg" - - -options : List ( String, String ) -options = - [ ( "1.jpg", "Option 1" ) - , ( "2.jpg", "Option 2" ) - , ( "3.jpg", "Option 3" ) - , ( "4.jpg", "Option 4" ) - , ( "5.jpg", "Option 5" ) - , ( "6.jpg", "Option 6" ) - , ( "7.jpg", "Option 7" ) - , ( "8.jpg", "Option 8" ) - , ( "9.jpg", "Option 9" ) - , ( "10.jpg", "Option 10" ) - , ( "11.jpg", "Option 11" ) - , ( "12.jpg", "Option 12" ) - , ( "13.jpg", "Option 13" ) - , ( "14.jpg", "Option 14" ) - , ( "15.jpg", "Option 15" ) - , ( "16.jpg", "Option 16" ) - , ( "17.jpg", "Option 17" ) - , ( "18.jpg", "Option 18" ) - , ( "19.jpg", "Option 19" ) - , ( "20.jpg", "Option 20" ) - , ( "21.jpg", "Option 21 (default)" ) - , ( "22.jpg", "Option 22" ) - , ( "23.jpg", "Option 23" ) - , ( "24.jpg", "Option 24" ) - , ( "25.jpg", "Option 25" ) - , ( "26.jpg", "Option 26" ) - , ( "27.jpg", "Option 27" ) - , ( "28.jpg", "Option 28" ) - , ( "29.jpg", "Option 29" ) - , ( "30.jpg", "Option 30" ) - ] - - - --- 📣 - - -extractedBackdropColor : { r : Int, g : Int, b : Int } -> Manager -extractedBackdropColor { r, g, b } model = - Return.singleton { model | extractedBackdropColor = Just (Color.rgb255 r g b) } - - -chooseBackdrop : String -> Manager -chooseBackdrop backdrop model = - User.saveSettings { model | chosenBackdrop = Just backdrop } - - -loadBackdrop : String -> Manager -loadBackdrop backdrop model = - return - { model | loadedBackdrops = model.loadedBackdrops ++ [ backdrop ] } - (Ports.pickAverageBackgroundColor backdrop) - - -setDefault : Manager -setDefault model = - Return.singleton { model | chosenBackdrop = Just default } - - - --- 🗺 - - -view : Model -> Html Msg -view model = - chunk - [ "fixed" - , "-inset-px" - , "z-0" - ] - [ Lazy.lazy chosen model.chosenBackdrop - , Lazy.lazy2 loaded model.loadedBackdrops model.fadeInBackdrop - - -- Shadow - --------- - , brick - [ style "background" "linear-gradient(#0000, rgba(0, 0, 0, 0.175))" ] - [ "absolute" - , "bottom-0" - , "h-64" - , "inset-x-0" - , "z-10" - ] - [] - ] - - -backgroundImage : String -> Html.Attribute msg -backgroundImage backdrop = - style "background-image" ("url(images/Background/" ++ backdrop ++ ")") - - -backgroundPositioning : String -> Html.Attribute msg -backgroundPositioning filename = - case filename of - "2.jpg" -> - style "background-position" "center 68%" - - "3.jpg" -> - style "background-position" "center 30%" - - "4.jpg" -> - style "background-position" "center 96.125%" - - "6.jpg" -> - style "background-position" "center 40%" - - "11.jpg" -> - style "background-position" "center 67.25%" - - "19.jpg" -> - style "background-position" "center 13%" - - "20.jpg" -> - style "background-position" "center 39.75%" - - "21.jpg" -> - style "background-position" "center 52.5%" - - "22.jpg" -> - style "background-position" "center top" - - "23.jpg" -> - style "background-position" "center 92.5%" - - "24.jpg" -> - style "background-position" "center top" - - "25.jpg" -> - style "background-position" "center 50%" - - "27.jpg" -> - style "background-position" "center top" - - _ -> - style "background-position" "center bottom" - - - ------------------------------------------ --- ㊙️ ------------------------------------------ - - -chosen : Maybe String -> Html Msg -chosen maybeChosen = - case maybeChosen of - Just c -> - let - loadingDecoder = - c - |> LoadBackdrop - |> Json.Decode.succeed - in - slab - Html.img - [ on "load" loadingDecoder - , src ("images/Background/" ++ c) - , style "opacity" "0.00001" - ] - [ "fixed" - , "h-px" - , "left-full" - , "overflow-hidden" - , "top-full" - , "w-px" - ] - [] - - Nothing -> - nothing - - -loaded : List String -> Bool -> Html Msg -loaded list fadeIn = - let - amount = - List.length list - - indexedMapFn idx = - image fadeIn (idx + 1 < amount) - in - list - |> List.indexedMap indexedMapFn - |> raw - - -image : Bool -> Bool -> String -> Html msg -image fadeIn isPrevious loadedBackdrop = - let - defaultClasses = - [ "bg-cover" - , "fixed" - , "inset-0" - - -- Opacity - ---------- - , if isPrevious || not fadeIn then - "opacity-100" - - else - "opacity-0" - ] - - animationClasses = - if not isPrevious && fadeIn then - [ "animation-2s" - , "animation-delay-50ms" - , "animation-fadeIn" - , "animation-fill-forwards" - , "animation-once" - ] - - else - [] - in - brick - [ backgroundImage loadedBackdrop - , backgroundPositioning loadedBackdrop - ] - (List.append - defaultClasses - animationClasses - ) - [] diff --git a/src/Core/UI/Commands/Alfred.elm b/src/Core/UI/Commands/Alfred.elm deleted file mode 100644 index 1fab0dcd9..000000000 --- a/src/Core/UI/Commands/Alfred.elm +++ /dev/null @@ -1,492 +0,0 @@ -module UI.Commands.Alfred exposing (commands, palette) - -import Alfred exposing (..) -import Conditional exposing (ifThenElse) -import List.Extra as List -import Material.Icons.Round as Icons -import Playlists.Matching -import Tracks exposing (Grouping(..), SortBy(..)) -import UI.Page as Page -import UI.Queue.Types as Queue -import UI.Sources.Page as Sources -import UI.Sources.Types as Sources -import UI.Tracks.Types as Tracks -import UI.Types as UI - - -palette : UI.Model -> Alfred UI.Msg -palette model = - Alfred.create - { action = action - , index = commands model - , message = "Run a command." - , operation = Query - } - - - --- ⛰ - - -commands : UI.Model -> List (Alfred.Group UI.Msg) -commands model = - [ { name = Just "Currently playing", items = nowPlayingCommands model } - , { name = Just "Track selection", items = selectionCommands model } - , { name = Just "Collection / Playlist", items = playlistCommands model } - , { name = Just "Tracks", items = tracksCommands model } - , { name = Just "View", items = viewCommands model } - , { name = Just "Playback", items = playbackCommands model } - , { name = Just "Sources", items = sourcesCommands model } - , { name = Just "Data", items = dataCommands model } - , { name = Just "Misc", items = miscCommands model } - ] - - - --- - - -dataCommands model = - [ { icon = Just (Icons.offline_bolt 16) - , title = "Clear tracks cache" - , value = Command (UI.TracksMsg Tracks.ClearCache) - } - , { icon = Just (Icons.save 16) - , title = "Download data snapshot" - , value = Command UI.Export - } - , { icon = Just (Icons.save 16) - , title = "Import data snapshot (⚠️ will override current data)" - , value = Command UI.RequestImport - } - ] - - -miscCommands model = - [ { icon = Just (Icons.help 16) - , title = "Open help" - , value = Command (UI.OpenUrlOnNewPage "./about/#UI") - } - ] - - -nowPlayingCommands : UI.Model -> List (Item UI.Msg) -nowPlayingCommands model = - case model.nowPlaying of - Just { item } -> - let - ( queueItemIdentifiers, _ ) = - item.identifiedTrack - - identifiedTrack = - model.tracks.harvested - |> List.getAt queueItemIdentifiers.indexInList - |> Maybe.withDefault item.identifiedTrack - - ( identifiers, track ) = - identifiedTrack - in - [ { icon = Just (Icons.search 16) - , title = "Show current track in list" - , value = Command (UI.TracksMsg Tracks.ScrollToNowPlaying) - } - - -- - , { icon = Just (Icons.favorite 14) - , title = ifThenElse identifiers.isFavourite "Remove favourite" "Mark as favourite" - , value = Command (UI.TracksMsg <| Tracks.ToggleFavourite identifiers.indexInList) - } - - -- - , { icon = Just (Icons.queue_music 16) - , title = "Add current track to collection" - , value = Command (UI.AssistWithAddingTracksToCollection <| [ identifiedTrack ]) - } - - -- - , { icon = Just (Icons.queue_music 16) - , title = "Add current track to playlist" - , value = Command (UI.AssistWithAddingTracksToPlaylist <| [ identifiedTrack ]) - } - - -- - , { icon = Just (Icons.offline_bolt 16) - , title = "Add current track to cache" - , value = - [ track ] - |> Tracks.StoreInCache - |> UI.TracksMsg - |> Command - } - ] - - Nothing -> - [] - - -playbackCommands model = - [ if Maybe.map .isPlaying model.nowPlaying == Just True then - { icon = Just (Icons.pause 16) - , title = "Pause" - , value = Command UI.TogglePlay - } - - else - { icon = Just (Icons.play_arrow 16) - , title = "Play" - , value = Command UI.TogglePlay - } - - -- - , { icon = Just (Icons.fast_rewind 18) - , title = "Previous track" - , value = Command (UI.QueueMsg Queue.Rewind) - } - , { icon = Just (Icons.fast_forward 18) - , title = "Next track" - , value = Command (UI.QueueMsg Queue.Shift) - } - , { icon = Just (Icons.repeat 16) - , title = toggle model.repeat "repeat" - , value = Command (UI.QueueMsg Queue.ToggleRepeat) - } - , { icon = Just (Icons.shuffle 16) - , title = toggle model.shuffle "shuffle" - , value = Command (UI.QueueMsg Queue.ToggleShuffle) - } - ] - - -playlistCommands model = - let - selection = - case model.selectedPlaylist of - Just playlist -> - let - identifiedTracksFromPlaylist = - model.tracks.identified - |> Playlists.Matching.match playlist - |> Tuple.first - - tracksFromPlaylist = - identifiedTracksFromPlaylist - |> (if playlist.collection then - identity - - else - Tracks.sortByIndexInPlaylist - ) - |> List.map Tuple.second - in - [ { icon = Just (Icons.waves 16) - , title = "Deactivate " ++ ifThenElse playlist.collection "collection" "playlist" - , value = Command UI.DeselectPlaylist - } - - -- - , { icon = Just (Icons.update 16) - , title = "Add to queue" - , value = - { inFront = False, tracks = identifiedTracksFromPlaylist } - |> Queue.AddTracks - |> UI.QueueMsg - |> Command - } - - -- - , { icon = Just (Icons.offline_bolt 16) - , title = "Store in cache" - , value = - tracksFromPlaylist - |> Tracks.StoreInCache - |> UI.TracksMsg - |> Command - } - - -- - , { icon = Just (Icons.archive 16) - , title = "Download as zip file" - , value = - tracksFromPlaylist - |> Tracks.Download - { prefixTrackNumber = not playlist.collection - , zipName = playlist.name - } - |> UI.TracksMsg - |> Command - } - - -- - , { icon = Just (Icons.waves 16) - , title = - if playlist.collection then - "Convert to playlist" - - else - "Convert to collection" - , value = - if playlist.collection then - { name = playlist.name } - |> UI.ConvertCollectionToPlaylist - |> Command - - else - { name = playlist.name } - |> UI.ConvertPlaylistToCollection - |> Command - } - ] - - Nothing -> - [] - in - [ { icon = Just (Icons.waves 16) - , title = - case model.selectedPlaylist of - Just _ -> - "Select other collection or playlist" - - Nothing -> - "Select collection or playlist" - , value = Command UI.AssistWithSelectingPlaylist - } - ] - ++ selection - - -selectionCommands model = - let - ( selection, _, amountOfFavs ) = - List.foldr - (\( i, t ) ( acc, selected, favouriteCounter ) -> - case List.findIndex ((==) i.indexInList) selected of - Just s -> - ( ( i, t ) :: acc - , List.removeAt s selected - , ifThenElse i.isFavourite (favouriteCounter + 1) favouriteCounter - ) - - Nothing -> - ( acc, selected, favouriteCounter ) - ) - ( [] - , model.selectedTrackIndexes - , 0 - ) - model.tracks.harvested - in - case selection of - [] -> - [] - - tracks -> - List.concat - [ [ { icon = Just (Icons.queue_music 16) - , title = "Add current selection to collection" - , value = Command (UI.AssistWithAddingTracksToCollection tracks) - } - , { icon = Just (Icons.queue_music 16) - , title = "Add current selection to playlist" - , value = Command (UI.AssistWithAddingTracksToPlaylist tracks) - } - ] - - -- - , if amountOfFavs > 0 then - [ { icon = Just (Icons.favorite 14) - , title = "Remove current selection from favourites" - , value = Command (UI.TracksMsg <| Tracks.RemoveFavourites tracks) - } - ] - - else - [] - - -- - , if amountOfFavs < List.length selection then - [ { icon = Just (Icons.favorite 14) - , title = "Add current selection to favourites" - , value = Command (UI.TracksMsg <| Tracks.AddFavourites tracks) - } - ] - - else - [] - ] - - -sourcesCommands model = - [ { icon = Just (Icons.sync 16) - , title = "Process sources" - , value = Command (UI.SourcesMsg Sources.Process) - } - - -- - , { icon = Just (Icons.add 16) - , title = "Add new source" - , value = Command (UI.ChangeUrlUsingPage <| Page.Sources Sources.New) - } - ] - - -tracksCommands model = - let - groupCommands = - [ AddedOn, Directory, FirstAlphaCharacter, TrackYear ] - |> (case model.grouping of - Just group -> - List.remove group - - Nothing -> - identity - ) - |> List.map - (\group -> - { icon = - Just (Icons.library_music 16) - , title = - case group of - AddedOn -> - "Group by processing date" - - Directory -> - "Group by directory" - - FirstAlphaCharacter -> - "Group by first letter" - - TrackYear -> - "Group by track year" - , value = - Command (UI.TracksMsg <| Tracks.GroupBy group) - } - ) - |> (\list -> - case model.grouping of - Just _ -> - { icon = Just (Icons.library_music 16) - , title = "Disable grouping" - , value = Command (UI.TracksMsg Tracks.DisableGrouping) - } - :: list - - Nothing -> - list - ) - in - [ { icon = Just (Icons.favorite 14) - , title = toggle model.favouritesOnly "favourites-only mode" - , value = Command (UI.TracksMsg Tracks.ToggleFavouritesOnly) - } - - -- - , { icon = Just (Icons.filter_list 16) - , title = toggle model.cachedTracksOnly "cached-tracks-only mode" - , value = Command (UI.TracksMsg Tracks.ToggleCachedOnly) - } - ] - ++ groupCommands - ++ [ { icon = Just (Icons.filter_list 16) - , title = - if model.hideDuplicates then - "Allow duplicates" - - else - "Remove duplicates" - , value = Command (UI.TracksMsg Tracks.ToggleHideDuplicates) - } - - -- - , { icon = Just (Icons.photo 16) - , title = - if model.coverSelectionReducesPool then - "Track pool is limited to selected cover (Select to disable)" - - else - "Track pool is not restricted by selected cover (Select to enable)" - , value = Command (UI.TracksMsg Tracks.ToggleCoverSelectionReducesPool) - } - ] - - -viewCommands model = - let - sortCommands = - (case Maybe.andThen .autoGenerated model.selectedPlaylist of - Nothing -> - [] - - _ -> - case model.scene of - Tracks.Covers -> - [ Album, Artist ] - - Tracks.List -> - [ Album, Artist, Title ] - ) - |> List.remove - model.sortBy - |> List.map - (\sortBy -> - { icon = - Just (Icons.sort 16) - , title = - case sortBy of - Artist -> - "Sort tracks by artist" - - Album -> - "Sort tracks by album" - - PlaylistIndex -> - "Sort tracks by playlist index" - - Title -> - "Sort tracks by title" - , value = - Command (UI.TracksMsg <| Tracks.SortBy sortBy) - } - ) - in - [ case model.scene of - Tracks.Covers -> - { icon = Just (Icons.notes 16) - , title = "Switch to list view" - , value = Command (UI.TracksMsg <| Tracks.ChangeScene Tracks.List) - } - - Tracks.List -> - { icon = Just (Icons.burst_mode 18) - , title = "Switch to cover view" - , value = Command (UI.TracksMsg <| Tracks.ChangeScene Tracks.Covers) - } - - -- - , { icon = Just (Icons.sort 16) - , title = "Change sort direction" - , value = Command (UI.TracksMsg <| Tracks.SortBy model.sortBy) - } - ] - ++ sortCommands - ++ [ { icon = Just (Icons.brush 14) - , title = "Change application theme" - , value = Command UI.AssistWithChangingTheme - } - ] - - - --- ㊙️ - - -action { result } = - case Maybe.andThen (.value >> Alfred.command) result of - Just msg -> - [ msg ] - - Nothing -> - [] - - -toggle bool suffix = - ifThenElse bool "Disable" "Enable" ++ " " ++ suffix diff --git a/src/Core/UI/Commands/State.elm b/src/Core/UI/Commands/State.elm deleted file mode 100644 index 3e6237811..000000000 --- a/src/Core/UI/Commands/State.elm +++ /dev/null @@ -1,16 +0,0 @@ -module UI.Commands.State exposing (..) - -import UI.Alfred.State as Alfred -import UI.Commands.Alfred -import UI.Types exposing (Manager) - - - --- 📣 - - -showPalette : Manager -showPalette model = - Alfred.assign - (UI.Commands.Alfred.palette model) - model diff --git a/src/Core/UI/Common/State.elm b/src/Core/UI/Common/State.elm deleted file mode 100644 index 7774a1ff1..000000000 --- a/src/Core/UI/Common/State.elm +++ /dev/null @@ -1,149 +0,0 @@ -module UI.Common.State exposing (..) - -import Browser.Navigation as Nav -import Common exposing (..) -import ContextMenu exposing (ContextMenu) -import Debouncer.Basic as Debouncer exposing (Debouncer) -import List.Extra as List -import Maybe.Extra as Maybe -import Monocle.Lens as Lens exposing (Lens) -import Notifications exposing (Notification) -import Return exposing (return) -import UI.Notifications -import UI.Page as Page exposing (Page) -import UI.Playlists.Directory -import UI.Syncing.Types as Syncing -import UI.Types as UI exposing (Manager, Model, Msg) -import User.Layer exposing (Method) - - - --- 📣 - - -changeUrlUsingPage : Page -> Manager -changeUrlUsingPage page model = - page - |> Page.toString - |> Nav.pushUrl model.navKey - |> return model - - -debounce : (Model -> Debouncer Msg Msg) -> (Debouncer Msg Msg -> Model -> Model) -> (Debouncer.Msg Msg -> Msg) -> (Msg -> Model -> ( Model, Cmd Msg )) -> Debouncer.Msg Msg -> Manager -debounce getter setter debouncerMsgContainer update debouncerMsg model = - let - ( subModel, subCmd, emittedMsg ) = - Debouncer.update debouncerMsg (getter model) - - mappedCmd = - Cmd.map debouncerMsgContainer subCmd - - updatedModel = - setter subModel model - in - case emittedMsg of - Just emitted -> - updatedModel - |> update emitted - |> Return.command mappedCmd - - Nothing -> - return updatedModel mappedCmd - - -dismissNotification : { id : Int } -> Manager -dismissNotification options model = - options - |> UI.Notifications.dismiss model.notifications - |> Return.map (\n -> { model | notifications = n }) - - -forceTracksRerender : Manager -forceTracksRerender model = - -- - -- TODO: - -- - -- let - -- containerId = - -- case model.scene of - -- Tracks.Covers -> - -- UI.Tracks.Scene.Covers.containerId - -- Tracks.List -> - -- UI.Tracks.Scene.List.containerId - -- in - -- Browser.Dom.setViewportOf containerId 0 1 - -- |> Task.attempt (always UI.Bypass) - -- |> return model - -- - Return.singleton model - - -generateDirectoryPlaylists : Manager -generateDirectoryPlaylists model = - let - nonDirectoryPlaylists = - List.filterNot - (.autoGenerated >> Maybe.isJust) - model.playlists - - directoryPlaylists = - UI.Playlists.Directory.generate - model.sources - model.tracks.untouched - in - [ nonDirectoryPlaylists - , directoryPlaylists - ] - |> List.concat - |> (\c -> { model | playlists = c }) - |> Return.singleton - - -showContextMenuWithModel : UI.Model -> ContextMenu Msg -> ( UI.Model, Cmd UI.Msg ) -showContextMenuWithModel model contextMenu = - Return.singleton { model | contextMenu = Just contextMenu } - - -showNotification : Notification Msg -> Manager -showNotification notification model = - model.notifications - |> UI.Notifications.show notification - |> Return.map (\n -> { model | notifications = n }) - - -showNotificationWithModel : UI.Model -> Notification Msg -> ( UI.Model, Cmd UI.Msg ) -showNotificationWithModel model notification = - showNotification notification model - - -showSyncingNotification : Method -> Manager -showSyncingNotification method model = - let - notification = - Notifications.stickyCasual "Syncing user data ..." - - syncing = - Syncing.Syncing { method = method, notificationId = Notifications.id notification } - in - showNotification - notification - { model | syncing = syncing } - - -toggleLoadingScreen : Switch -> Manager -toggleLoadingScreen switch model = - case switch of - On -> - Return.singleton { model | isLoading = True } - - Off -> - Return.singleton { model | isLoading = False } - - - --- 🛠 - - -modifySingleton : Lens a b -> (b -> b) -> a -> ( a, Cmd msg ) -modifySingleton lens modifier = - Lens.modify lens modifier >> Return.singleton diff --git a/src/Core/UI/Common/Types.elm b/src/Core/UI/Common/Types.elm deleted file mode 100644 index 4853e7a43..000000000 --- a/src/Core/UI/Common/Types.elm +++ /dev/null @@ -1,12 +0,0 @@ -module UI.Common.Types exposing (..) - -import Debouncer.Basic as Debouncer -import UI.Types exposing (Manager, Model, Msg) - - - --- 🌳 - - -type alias DebounceManager = - (Msg -> Model -> ( Model, Cmd Msg )) -> Debouncer.Msg Msg -> Manager diff --git a/src/Core/UI/Demo.elm b/src/Core/UI/Demo.elm deleted file mode 100644 index 2b1c8b799..000000000 --- a/src/Core/UI/Demo.elm +++ /dev/null @@ -1,203 +0,0 @@ -module UI.Demo exposing (tape) - -import Base64 -import Dict -import Json.Encode as Json -import Sources exposing (Service(..), Source) -import Sources.Encoding as Sources -import Time -import Tracks exposing (Favourite, Track) -import Tracks.Encoding as Tracks - - - --- ⛩ - - -tape : Time.Posix -> Json.Value -tape currentTime = - Json.object - [ ( "favourites", Json.list Tracks.encodeFavourite favourites ) - , ( "sources", Json.list Sources.encode sources ) - , ( "tracks", Json.list Tracks.encodeTrack <| tracks currentTime ) - ] - - - --- ㊙️ - - -favourites : List Favourite -favourites = - [ { artist = Just "James Blake" - , title = "Essential Mix (09-17-2011)" - } - ] - - -sources : List Source -sources = - let - key = - "QUtJQTZPUTNFVk1BWFZDRFFINkI=" - |> Base64.decode - |> Result.withDefault "" - - secret = - "Z0hPQkdHRzU1aXc0a0RDbjdjWlRJYTVTUDRZWnpERkRzQnFCYWI4Mg==" - |> Base64.decode - |> Result.withDefault "" - in - [ { id = "15076402187342" - , data = - Dict.fromList - [ ( "accessKey", key ) - , ( "bucketName", "ongaku-ryoho-demo" ) - , ( "directoryPath", "/" ) - , ( "name", "Demo" ) - , ( "region", "us-east-1" ) - , ( "secretKey", secret ) - ] - , directoryPlaylists = True - , enabled = True - , service = AmazonS3 - } - ] - - -tracks : Time.Posix -> List Track -tracks insertedAt = - [ { id = "MTUwNzY0MDIxODczNDIvL0ZyZWUgbXVzaWMvKFNXTDAxMyktb3JpSmFudXMtV0VCLTIwMTQtRlJFRS8wMS1ib25pdGEubXAz" - , path = "Free music/(SWL013)-oriJanus-WEB-2014-FREE/01-bonita.mp3" - , sourceId = "15076402187342" - , insertedAt = insertedAt - , tags = - { disc = 1 - , nr = 1 - - -- - , album = Just "Soulection White Label: 013" - , artist = Just "oriJanus" - , title = "Bonita" - - -- - , genre = Just "Soulection" - , picture = Nothing - , year = Nothing - } - } - , { id = "MTUwNzY0MDIxODczNDIvL0ZyZWUgbXVzaWMvKFNXTDAxMyktb3JpSmFudXMtV0VCLTIwMTQtRlJFRS8wMi02Lm1wMw" - , path = "Free music/(SWL013)-oriJanus-WEB-2014-FREE/02-6.mp3" - , sourceId = "15076402187342" - , insertedAt = insertedAt - , tags = - { disc = 1 - , nr = 2 - - -- - , album = Just "Soulection White Label: 013" - , artist = Just "oriJanus" - , title = "6" - - -- - , genre = Just "Soulection" - , picture = Nothing - , year = Nothing - } - } - , { id = "MTUwNzY0MDIxODczNDIvL0ZyZWUgbXVzaWMvKFNXTDAxMyktb3JpSmFudXMtV0VCLTIwMTQtRlJFRS8wMy1ob3RfcmVtaXhfZnQuX3Rlay5sdW5fJl96aWtvbW8ubXAz" - , path = "Free music/(SWL013)-oriJanus-WEB-2014-FREE/03-hot_remix_ft._tek.lun_&_zikomo.mp3" - , sourceId = "15076402187342" - , insertedAt = insertedAt - , tags = - { disc = 1 - , nr = 3 - - -- - , album = Just "Soulection White Label: 013" - , artist = Just "oriJanus" - , title = "Hot Remix ft. Tek.Lun & Zikomo" - - -- - , genre = Just "Soulection" - , picture = Nothing - , year = Nothing - } - } - , { id = "MTUwNzY0MDIxODczNDIvL0ZyZWUgbXVzaWMvQ29tX1RydWlzZS1DaGVtaWNhbF9MZWdzLTIwMTItRlJFRS8wMS1jb21fdHJ1aXNlLWNoZW1pY2FsX2xlZ3MubXAz" - , path = "Free music/Com_Truise-Chemical_Legs-2012-FREE/01-com_truise-chemical_legs.mp3" - , sourceId = "15076402187342" - , insertedAt = insertedAt - , tags = - { disc = 1 - , nr = 9 - - -- - , album = Just "Adult Swim Singles Project 2012" - , artist = Just "Com Truise" - , title = "Chemical Legs" - - -- - , genre = Nothing - , picture = Nothing - , year = Just 2012 - } - } - , { id = "MTUwNzY0MDIxODczNDIvL0ZyZWUgbXVzaWMvTWFudWVsZV9BdHplbmlfLV8wNF8tX0xpdHRsZV9TdGFyLm1wMw" - , path = "Free music/Manuele_Atzeni_-_04_-_Little_Star.mp3" - , sourceId = "15076402187342" - , insertedAt = insertedAt - , tags = - { disc = 1 - , nr = 4 - - -- - , album = Just "The Miyazaki Tour EP" - , artist = Just "Manuele Atzeni" - , title = "Little Star" - - -- - , genre = Just "Funk" - , picture = Nothing - , year = Nothing - } - } - , { id = "MTUwNzY0MDIxODczNDIvL0ZyZWUgbXVzaWMvUGF0cmlja19MZWVfLV8wMl8tX1F1aXR0aW5fVGltZS5tcDM" - , path = "Free music/Patrick_Lee_-_02_-_Quittin_Time.mp3" - , sourceId = "15076402187342" - , insertedAt = insertedAt - , tags = - { disc = 1 - , nr = 2 - - -- - , album = Just "The Last Thing" - , artist = Just "Patrick Lee" - , title = "Quittin' Time" - - -- - , genre = Just "Electronic" - , picture = Nothing - , year = Nothing - } - } - , { id = "MTUwNzY0MDIxODczNDIvL1JhZGlvL2phbWVzX2JsYWtlLWVzc2VudGlhbF9taXgtc2F0LTA5LTE3LTIwMTEubXAz" - , path = "Radio/james_blake-essential_mix-sat-09-17-2011.mp3" - , sourceId = "15076402187342" - , insertedAt = insertedAt - , tags = - { disc = 1 - , nr = 1 - - -- - , album = Just "Essential Mix-SAT-09-17" - , artist = Just "James Blake" - , title = "Essential Mix (09-17-2011)" - - -- - , genre = Just "Electronic" - , picture = Nothing - , year = Nothing - } - } - ] diff --git a/src/Core/UI/DnD.elm b/src/Core/UI/DnD.elm deleted file mode 100644 index 4ae11ed29..000000000 --- a/src/Core/UI/DnD.elm +++ /dev/null @@ -1,245 +0,0 @@ -module UI.DnD exposing (Environment, Model, Msg, hasDropped, initialModel, isBeingDraggedOver, isDragging, isDraggingOver, listenToEnterLeave, listenToStart, modelSubject, modelTarget, startDragging, stoppedDragging, update) - -import Html exposing (Attribute) -import Html.Events.Extra.Mouse as Mouse -import Html.Events.Extra.Pointer as Pointer - - - --- 🌳 - - -type Model context - = NotDragging - | Dragging { subject : context } - | DraggingOver { subject : context, target : context } - | Dropped { subject : context, target : context } - - -type Msg context - = Start context - | Enter context - | Leave context - | Stop - - -type alias Environment context msg = - { model : Model context - , toMsg : Msg context -> msg - } - - -type alias Response = - { initiated : Bool } - - -initialModel : Model context -initialModel = - NotDragging - - - --- 📣 - - -update : Msg context -> Model context -> ( Model context, Response ) -update msg model = - ( ------------------------------------ - -- Model - ------------------------------------ - case msg of - Start context -> - Dragging { subject = context } - - Enter context -> - case model of - NotDragging -> - NotDragging - - Dragging { subject } -> - DraggingOver { subject = subject, target = context } - - DraggingOver { subject } -> - DraggingOver { subject = subject, target = context } - - Dropped _ -> - NotDragging - - Leave context -> - case model of - NotDragging -> - NotDragging - - Dragging env -> - Dragging env - - DraggingOver { subject, target } -> - if context == target then - Dragging { subject = subject } - - else - model - - Dropped _ -> - NotDragging - - Stop -> - case model of - DraggingOver { subject, target } -> - if subject /= target then - Dropped { subject = subject, target = target } - - else - NotDragging - - _ -> - NotDragging - ------------------------------------ - -- Response - ------------------------------------ - , case msg of - Start _ -> - { initiated = True } - - _ -> - { initiated = False } - ) - - - --- 🔱 ░░ EVENTS & MESSAGES - - -listenToStart : Environment context msg -> context -> Attribute msg -listenToStart { toMsg } context = - Pointer.onWithOptions - "pointerdown" - { stopPropagation = True - , preventDefault = False - } - (\event -> - case ( event.pointer.button, event.isPrimary ) of - ( Mouse.MainButton, True ) -> - toMsg (Start context) - - _ -> - toMsg Stop - ) - - -listenToEnterLeave : Environment context msg -> context -> List (Attribute msg) -listenToEnterLeave { model, toMsg } context = - case model of - NotDragging -> - [] - - _ -> - [ context - |> Enter - |> toMsg - |> always - |> Pointer.onEnter - , context - |> Leave - |> toMsg - |> always - |> Pointer.onLeave - ] - - -startDragging : context -> Msg context -startDragging = - Start - - -stoppedDragging : Msg context -stoppedDragging = - Stop - - - --- 🔱 ░░ MODEL - - -isBeingDraggedOver : context -> Model context -> Bool -isBeingDraggedOver context model = - case model of - DraggingOver { target } -> - context == target - - _ -> - False - - -isDragging : Model context -> Bool -isDragging model = - case model of - NotDragging -> - False - - Dragging _ -> - True - - DraggingOver _ -> - True - - Dropped _ -> - False - - -isDraggingOver : context -> Model context -> Bool -isDraggingOver context model = - case model of - NotDragging -> - False - - Dragging _ -> - False - - DraggingOver { target } -> - target == context - - Dropped _ -> - False - - -hasDropped : Model context -> Bool -hasDropped model = - case model of - Dropped _ -> - True - - _ -> - False - - -modelSubject : Model context -> Maybe context -modelSubject model = - case model of - NotDragging -> - Nothing - - Dragging { subject } -> - Just subject - - DraggingOver { subject } -> - Just subject - - Dropped { subject } -> - Just subject - - -modelTarget : Model context -> Maybe context -modelTarget model = - case model of - NotDragging -> - Nothing - - Dragging _ -> - Nothing - - DraggingOver { target } -> - Just target - - Dropped { target } -> - Just target diff --git a/src/Core/UI/Equalizer/State.elm b/src/Core/UI/Equalizer/State.elm deleted file mode 100644 index d5425338d..000000000 --- a/src/Core/UI/Equalizer/State.elm +++ /dev/null @@ -1,61 +0,0 @@ -module UI.Equalizer.State exposing (..) - -import Common exposing (Switch(..)) -import Equalizer exposing (..) -import Return exposing (return) -import UI.Ports as Ports -import UI.Types exposing (..) - - - --- 📣 - - -adjustVolume : Float -> Manager -adjustVolume volume model = - let - settings = - model.eqSettings - in - return - { model | eqSettings = { settings | volume = volume } } - (adjustKnobUsingPort Volume volume) - - -toggleVolumeSlider : Switch -> Manager -toggleVolumeSlider switch model = - case switch of - On -> - Return.singleton { model | showVolumeSlider = True } - - Off -> - Return.singleton { model | showVolumeSlider = False } - - - --- ⚗️ - - -adjustKnobUsingPort : Knob -> Float -> Cmd Msg -adjustKnobUsingPort knobType value = - Ports.adjustEqualizerSetting - { value = value - , knob = - case knobType of - Low -> - "LOW" - - Mid -> - "MID" - - High -> - "HIGH" - - Volume -> - "VOLUME" - } - - -adjustAllKnobs : Settings -> Cmd Msg -adjustAllKnobs eqSettings = - adjustKnobUsingPort Volume eqSettings.volume diff --git a/src/Core/UI/Interface/State.elm b/src/Core/UI/Interface/State.elm deleted file mode 100644 index ac80ec940..000000000 --- a/src/Core/UI/Interface/State.elm +++ /dev/null @@ -1,261 +0,0 @@ -module UI.Interface.State exposing (..) - -import Alfred -import Debouncer.Basic as Debouncer -import Maybe.Extra as Maybe -import Notifications -import Return exposing (return) -import Return.Ext as Return -import Theme -import Tracks -import UI.Alfred.State as Alfred -import UI.Common.State as Common -import UI.Common.Types exposing (DebounceManager) -import UI.DnD as DnD -import UI.Page as Page -import UI.Playlists.State as Playlists -import UI.Ports as Ports -import UI.Queue.State as Queue -import UI.Theme -import UI.Types exposing (..) -import UI.User.State.Export exposing (saveEnclosedUserData) - - - --- 🔱 - - -assistWithChangingTheme : Manager -assistWithChangingTheme model = - { action = - \{ result } -> - case result of - Just { value } -> - value - |> Alfred.command - |> Maybe.map List.singleton - |> Maybe.withDefault [] - - Nothing -> - [] - , index = - [ { name = Just "Themes" - , items = - List.map - (\theme -> - { icon = Just (theme.icon 16) - , title = theme.title - , value = Alfred.Command (ChangeTheme { id = theme.id }) - } - ) - UI.Theme.list - } - ] - , message = "Choose a theme." - , operation = Alfred.Query - } - |> Alfred.create - |> (\a -> Alfred.assign a model) - - -blur : Manager -blur model = - Return.singleton { model | focusedOnInput = False, pressedKeys = [] } - - -changeTheme : Theme.Id -> Manager -changeTheme id model = - saveEnclosedUserData { model | theme = Just id } - - -contextMenuConfirmation : String -> Msg -> Manager -contextMenuConfirmation conf msg model = - return - { model | confirmation = Just conf } - (Return.task msg) - - -copyToClipboard : String -> Manager -copyToClipboard string = - string - |> Ports.copyToClipboard - |> Return.communicate - - -resizeDebounce : DebounceManager -resizeDebounce = - Common.debounce - .resizeDebouncer - (\d m -> { m | resizeDebouncer = d }) - ResizeDebounce - - -dnd : DnD.Msg Int -> Manager -dnd dragMsg model = - let - ( d, { initiated } ) = - DnD.update dragMsg model.dnd - - m = - if initiated then - { model | dnd = d, isDragging = True } - - else - { model | dnd = d } - in - if DnD.hasDropped d then - case m.page of - Page.Queue _ -> - let - ( from, to ) = - ( Maybe.withDefault 0 <| DnD.modelSubject d - , Maybe.withDefault 0 <| DnD.modelTarget d - ) - - newFuture = - Queue.moveItem - { from = from, to = to, shuffle = m.shuffle } - m.playingNext - in - Queue.fill { m | playingNext = newFuture } - - Page.Index -> - let - trackCanBeMoved = - not m.favouritesOnly && Maybe.isNothing m.searchTerm - in - case m.scene of - Tracks.Covers -> - -- TODO - Return.singleton m - - Tracks.List -> - if trackCanBeMoved then - Playlists.moveTrackInSelected - { to = Maybe.withDefault 0 (DnD.modelTarget d) } - m - - else - "Can't move tracks in a playlist whilst using favourites-only mode, or while searching." - |> Notifications.casual - |> Common.showNotificationWithModel m - - _ -> - Return.singleton m - - else - Return.singleton m - - -focusedOnInput : Manager -focusedOnInput model = - Return.singleton { model | focusedOnInput = True } - - -hideOverlay : Manager -hideOverlay model = - if Maybe.isJust model.contextMenu then - Return.singleton { model | contextMenu = Nothing } - - else if Maybe.isJust model.confirmation then - Return.singleton { model | confirmation = Nothing } - - else if Maybe.isJust model.alfred then - Return.singleton { model | alfred = Nothing } - - else - Return.singleton model - - -lostWindowFocus : Manager -lostWindowFocus model = - Return.singleton { model | focusedOnInput = False, pressedKeys = [] } - - -preferredColorSchemaChanged : { dark : Bool } -> Manager -preferredColorSchemaChanged { dark } model = - Return.singleton { model | darkMode = dark } - - -msgViaContextMenu : Msg -> Manager -msgViaContextMenu msg model = - return - (case msg of - ContextMenuConfirmation _ _ -> - model - - _ -> - { model | confirmation = Nothing, contextMenu = Nothing } - ) - (Return.task msg) - - -removeNotification : { id : Int } -> Manager -removeNotification { id } model = - model.notifications - |> List.filter (Notifications.id >> (/=) id) - |> (\n -> { model | notifications = n }) - |> Return.singleton - - -removeQueueSelection : Manager -removeQueueSelection model = - Return.singleton { model | selectedQueueItem = Nothing } - - -removeTrackSelection : Manager -removeTrackSelection model = - Return.singleton { model | selectedTrackIndexes = [] } - - -resizedWindow : ( Int, Int ) -> Manager -resizedWindow ( width, height ) model = - Return.singleton - { model - | contextMenu = Nothing - , viewport = { height = toFloat height, width = toFloat width } - } - - -searchDebounce : DebounceManager -searchDebounce = - Common.debounce - .searchDebouncer - (\d m -> { m | searchDebouncer = d }) - SearchDebounce - - -setIsTouchDevice : Bool -> Manager -setIsTouchDevice bool model = - Return.singleton { model | isTouchDevice = bool } - - -stoppedDragging : Manager -stoppedDragging model = - let - notDragging = - { model | isDragging = False } - in - -- Depending on where we stopped dragging something, - -- do the appropriate thing. - case model.page of - Page.Queue _ -> - dnd DnD.stoppedDragging notDragging - - Page.Index -> - dnd DnD.stoppedDragging notDragging - - _ -> - Return.singleton notDragging - - - --- MESSAGES - - -onResize : Int -> Int -> Msg -onResize w h = - ( w, h ) - |> ResizedWindow - |> Debouncer.provideInput - |> ResizeDebounce diff --git a/src/Core/UI/Navigation.elm b/src/Core/UI/Navigation.elm deleted file mode 100644 index 28c80194e..000000000 --- a/src/Core/UI/Navigation.elm +++ /dev/null @@ -1,30 +0,0 @@ -module UI.Navigation exposing (Action(..), Icon(..), Label(..), LabelType(..)) - -import Html.Events.Extra.Mouse as Mouse -import Material.Icons.Types exposing (Coloring) -import Svg exposing (Svg) -import UI.Page exposing (Page) - - - --- 🌳 - - -type Action msg - = NavigateToPage Page - | OpenLinkInNewPage String - | PerformMsg msg - | PerformMsgWithMouseEvent (Mouse.Event -> msg) - - -type Icon msg - = Icon (Int -> Coloring -> Svg msg) - - -type Label - = Label String LabelType - - -type LabelType - = Hidden - | Shown diff --git a/src/Core/UI/Notifications.elm b/src/Core/UI/Notifications.elm deleted file mode 100644 index 106df8c5b..000000000 --- a/src/Core/UI/Notifications.elm +++ /dev/null @@ -1,66 +0,0 @@ -module UI.Notifications exposing (Model, dismiss, show, showWithModel) - -import Notifications exposing (..) -import Process -import Task -import UI.Types exposing (Msg(..)) - - - --- 🌳 - - -type alias Model = - List (Notification Msg) - - - --- 📣 - - -dismiss : Model -> { id : Int } -> ( Model, Cmd Msg ) -dismiss collection { id } = - ( List.map - (\notification -> - if Notifications.id notification == id then - Notifications.dismiss notification - - else - notification - ) - collection - , Task.perform - (\_ -> RemoveNotification { id = id }) - (Process.sleep 500) - ) - - -show : Notification Msg -> Model -> ( Model, Cmd Msg ) -show notification collection = - let - existingNotificationIds = - List.map Notifications.id collection - in - if List.member (Notifications.id notification) existingNotificationIds then - -- Don't show duplicate notifications - ( collection - , Cmd.none - ) - - else - ( notification :: collection - -- Hide notification after a certain amount of time, - -- unless it's a sticky notification. - , if (Notifications.options notification).sticky then - Cmd.none - - else - Task.perform - (\_ -> DismissNotification { id = Notifications.id notification }) - (Process.sleep 7500) - ) - - -showWithModel : Model -> Notification Msg -> ( Model, Cmd Msg ) -showWithModel model notification = - show notification model diff --git a/src/Core/UI/Other/State.elm b/src/Core/UI/Other/State.elm deleted file mode 100644 index 72e8c9098..000000000 --- a/src/Core/UI/Other/State.elm +++ /dev/null @@ -1,96 +0,0 @@ -module UI.Other.State exposing (..) - -import Alien -import Common exposing (ServiceWorkerStatus(..)) -import Dict -import Notifications -import Return exposing (return) -import Time -import UI.Common.State as Common -import UI.Ports as Ports -import UI.Types exposing (..) - - - --- 🔱 - - -installedServiceWorker : Manager -installedServiceWorker model = - case model.serviceWorkerStatus of - InstallingNew -> - Return.singleton { model | serviceWorkerStatus = WaitingForActivation } - - _ -> - Return.singleton { model | serviceWorkerStatus = Activated } - - -installingServiceWorker : Manager -installingServiceWorker model = - Return.singleton { model | serviceWorkerStatus = InstallingNew } - - -redirectToBrain : Alien.Event -> Manager -redirectToBrain event model = - return model (Ports.toBrain event) - - -reloadApp : Manager -reloadApp model = - return model (Ports.reloadApp ()) - - -setIsOnline : Bool -> Manager -setIsOnline bool model = - { model | isOnline = bool } - |> Return.singleton - |> Return.command - (case model.nowPlaying of - Just { isPlaying, item } -> - let - trackId = - (Tuple.second item.identifiedTrack).id - in - Ports.reloadAudioNodeIfNeeded - { play = isPlaying - , progress = - if model.rememberProgress then - Dict.get trackId model.progress - - else - Nothing - , trackId = trackId - } - - Nothing -> - Cmd.none - ) - |> Return.andThen - (case ( model.isOnline, bool ) of - ( False, True ) -> - syncHypaethralData - - _ -> - Return.singleton - ) - - -setCurrentTime : Time.Posix -> Manager -setCurrentTime time model = - Return.singleton { model | currentTime = time } - - -setCurrentTimeZone : Time.Zone -> Manager -setCurrentTimeZone zone model = - Return.singleton { model | currentTimeZone = zone } - - - --- ⚗️ - - -syncHypaethralData : Manager -syncHypaethralData model = - model - |> Common.showNotification (Notifications.casual "Syncing") - |> Return.command (Ports.toBrain <| Alien.trigger Alien.SyncHypaethralData) diff --git a/src/Core/UI/Page.elm b/src/Core/UI/Page.elm deleted file mode 100644 index 8ebb0a062..000000000 --- a/src/Core/UI/Page.elm +++ /dev/null @@ -1,245 +0,0 @@ -module UI.Page exposing (Page(..), fromUrl, rewriteUrl, sameBase, sources, toString) - -import Maybe.Extra as Maybe -import Sources exposing (Service(..)) -import UI.Playlists.Page as Playlists -import UI.Queue.Page as Queue -import UI.Settings.Page as Settings -import UI.Sources.Page as Sources -import Url exposing (Url) -import Url.Ext as Url -import Url.Parser exposing (..) -import Url.Parser.Query as Query - - - --- 🌳 - - -type Page - = Index - | Queue Queue.Page - | Playlists Playlists.Page - | Settings Settings.Page - | Sources Sources.Page - - - --- 🔱 - - -fromUrl : Url -> Maybe Page -fromUrl = - parse route - - -rewriteUrl : Url -> Url -rewriteUrl url = - if Maybe.unwrap False (String.contains "path=") url.query then - -- Sometimes we have to use this kind of routing when doing redirections - let - maybePath = - Url.extractQueryParam "path" url - - path = - Maybe.withDefault "" maybePath - in - if - Maybe.unwrap - False - (\f -> String.contains "token=" f || String.contains "code=" f) - url.fragment - then - -- For some oauth stuff, replace the query with the fragment - { url | path = path, query = url.fragment } - - else - { url | path = path } - - else - -- Otherwise do hash-based routing and replace the path with the fragment - { url | path = Maybe.withDefault "" url.fragment } - - -toString : Page -> String -toString = - toString_ >> (++) "#/" - - -toString_ : Page -> String -toString_ page = - case page of - Index -> - "" - - ----------------------------------------- - -- Playlists - ----------------------------------------- - Playlists Playlists.Index -> - "playlists" - - Playlists Playlists.NewCollection -> - "playlists/new/collection" - - Playlists Playlists.NewPlaylist -> - "playlists/new/playlist" - - Playlists (Playlists.Edit playlistName) -> - "playlists/edit/" ++ playlistName - - ----------------------------------------- - -- Queue - ----------------------------------------- - Queue Queue.History -> - "queue/history" - - Queue Queue.Index -> - "queue" - - ----------------------------------------- - -- Settings - ----------------------------------------- - Settings Settings.Data -> - "settings/data" - - Settings Settings.Index -> - "settings" - - Settings Settings.Sync -> - "settings/sync" - - ----------------------------------------- - -- Sources - ----------------------------------------- - Sources (Sources.Edit sourceId) -> - "sources/edit/" ++ sourceId - - Sources Sources.Index -> - "sources" - - Sources Sources.New -> - "sources/new" - - Sources Sources.NewOnboarding -> - "sources/welcome" - - Sources (Sources.NewThroughRedirect Dropbox _) -> - "sources/new/dropbox" - - Sources (Sources.NewThroughRedirect Google _) -> - "sources/new/google" - - Sources (Sources.NewThroughRedirect _ _) -> - "sources/new" - - Sources (Sources.Rename sourceId) -> - "sources/rename/" ++ sourceId - - -{-| Are the bases of these two pages the same? --} -sameBase : Page -> Page -> Bool -sameBase a b = - case ( a, b ) of - ( Playlists _, Playlists _ ) -> - True - - ( Queue _, Queue _ ) -> - True - - ( Settings _, Settings _ ) -> - True - - ( Sources _, Sources _ ) -> - True - - ( Index, Playlists _ ) -> - True - - ( Index, Queue _ ) -> - True - - _ -> - a == b - - - --- 🔱 ░░ SPECIFIC - - -sources : Page -> Maybe Sources.Page -sources page = - case page of - Sources s -> - Just s - - _ -> - Nothing - - - --- ⚗️ - - -route : Parser (Page -> a) a -route = - oneOf - [ map Index top - - ----------------------------------------- - -- Playlists - ----------------------------------------- - , map (Playlists Playlists.Index) (s "playlists") - , map (Playlists << Playlists.Edit) (s "playlists" s "edit" string) - , map (Playlists Playlists.NewCollection) (s "playlists" s "new" s "collection") - , map (Playlists Playlists.NewPlaylist) (s "playlists" s "new" s "playlist") - - ----------------------------------------- - -- Queue - ----------------------------------------- - , map (Queue Queue.Index) (s "queue") - , map (Queue Queue.History) (s "queue" s "history") - - ----------------------------------------- - -- Settings - ----------------------------------------- - , map (Settings Settings.Data) (s "settings" s "data") - , map (Settings Settings.Sync) (s "settings" s "sync") - , map (Settings Settings.Index) (s "settings") - - ----------------------------------------- - -- Sources - ----------------------------------------- - , map (Sources Sources.Index) (s "sources") - , map (Sources << Sources.Edit) (s "sources" s "edit" string) - , map (Sources Sources.New) (s "sources" s "new") - , map (Sources Sources.NewOnboarding) (s "sources" s "welcome") - , map (Sources << Sources.Rename) (s "sources" s "rename" string) - - -- Oauth - -------- - , map - (\token state -> - { codeOrToken = token, state = state } - |> Sources.NewThroughRedirect Dropbox - |> Sources - ) - (s "sources" - s "new" - s "dropbox" - Query.string "access_token" - Query.string "state" - ) - , map - (\code state -> - { codeOrToken = code, state = state } - |> Sources.NewThroughRedirect Google - |> Sources - ) - (s "sources" - s "new" - s "google" - Query.string "code" - Query.string "state" - ) - ] diff --git a/src/Core/UI/Playlists/Alfred.elm b/src/Core/UI/Playlists/Alfred.elm deleted file mode 100644 index f14130b86..000000000 --- a/src/Core/UI/Playlists/Alfred.elm +++ /dev/null @@ -1,144 +0,0 @@ -module UI.Playlists.Alfred exposing (create, select) - -import Alfred exposing (..) -import Conditional exposing (ifThenElse) -import Dict -import Dict.Extra as Dict -import List.Extra as List -import Material.Icons.Round as Icons -import Playlists exposing (..) -import Tracks exposing (IdentifiedTrack) -import UI.Types as UI - - - --- CREATE - - -create : { collectionMode : Bool } -> List IdentifiedTrack -> List Playlist -> Alfred UI.Msg -create { collectionMode } tracks playlists = - let - index = - makeIndex playlists - - subject = - ifThenElse collectionMode "collection" "playlist" - in - Alfred.create - { action = createAction collectionMode tracks - , index = index - , message = - if List.length tracks == 1 then - "Choose or create a " ++ subject ++ " to add this track to." - - else - "Choose or create a " ++ subject ++ " to add these tracks to." - , operation = QueryOrMutation - } - - -createAction : Bool -> List IdentifiedTrack -> Alfred.Action UI.Msg -createAction collectionMode tracks ctx = - let - playlistTracks = - Tracks.toPlaylistTracks tracks - in - case ctx.result of - Just result -> - -- Add to playlist - -- - case Alfred.stringValue result.value of - Just playlistName -> - [ UI.AddTracksToPlaylist - { collection = collectionMode - , playlistName = playlistName - , tracks = playlistTracks - } - ] - - Nothing -> - [] - - Nothing -> - -- Create playlist, - -- if given a search term. - -- - case ctx.searchTerm of - Just searchTerm -> - [ UI.AddTracksToPlaylist - { collection = collectionMode - , playlistName = searchTerm - , tracks = playlistTracks - } - ] - - Nothing -> - [] - - - --- SELECT - - -select : List Playlist -> Alfred UI.Msg -select playlists = - let - index = - makeIndex playlists - in - Alfred.create - { action = selectAction playlists - , index = index - , message = "Select a playlist to play tracks from." - , operation = Query - } - - -selectAction : List Playlist -> Alfred.Action UI.Msg -selectAction playlists { result } = - case Maybe.andThen (\r -> List.find (.name >> Just >> (==) (stringValue r.value)) playlists) result of - Just playlist -> - [ UI.SelectPlaylist playlist ] - - Nothing -> - [] - - - --- ㊙️ - - -makeIndex playlists = - playlists - |> Dict.groupBy - (\p -> - case ( p.autoGenerated, p.collection ) of - ( Just _, _ ) -> - "9 - AutoGenerated Directory Playlists" - - ( Nothing, True ) -> - "1 - Your Collections" - - ( Nothing, False ) -> - "2 - Your Playlists" - ) - |> Dict.toList - |> List.map - (\( k, v ) -> - ( String.dropLeft 4 k - , v - |> List.uniqueBy .name - |> List.map - (\playlist -> - { icon = Just (Icons.queue_music 16) - , title = playlist.name - , value = Alfred.StringValue playlist.name - } - ) - |> List.sortBy (.title >> String.toLower) - ) - ) - |> List.map - (\( k, v ) -> - { name = Just k, items = v } - ) diff --git a/src/Core/UI/Playlists/ContextMenu.elm b/src/Core/UI/Playlists/ContextMenu.elm deleted file mode 100644 index 989544c23..000000000 --- a/src/Core/UI/Playlists/ContextMenu.elm +++ /dev/null @@ -1,201 +0,0 @@ -module UI.Playlists.ContextMenu exposing (listMenu) - -import ContextMenu exposing (..) -import Coordinates exposing (Coordinates) -import Html.Events.Extra.Mouse -import Material.Icons.Round as Icons -import Playlists exposing (Playlist) -import Playlists.Matching -import Tracks exposing (IdentifiedTrack) -import UI.Page -import UI.Playlists.Page -import UI.Queue.Types as Queue -import UI.Tracks.Types as Tracks -import UI.Types exposing (Msg(..)) -import Url - - - --- 🔱 - - -listMenu : Playlist -> List IdentifiedTrack -> Maybe String -> Coordinates -> ContextMenu Msg -listMenu playlist allTracks confirmation coordinates = - let - identifiedTracksFromPlaylist = - case playlist.autoGenerated of - Just _ -> - List.filter - (Tuple.second >> Tracks.matchesAutoGeneratedPlaylist playlist) - allTracks - - Nothing -> - allTracks - |> Playlists.Matching.match playlist - |> Tuple.first - - tracksFromPlaylist = - identifiedTracksFromPlaylist - |> (if playlist.collection then - identity - - else - Tracks.sortByIndexInPlaylist - ) - |> List.map Tuple.second - - menuMsg = - ShowPlaylistListMenu - playlist - { button = Html.Events.Extra.Mouse.MainButton - , clientPos = Coordinates.toTuple coordinates - , keys = { alt = False, ctrl = False, shift = False } - , offsetPos = ( 0, 0 ) - , pagePos = ( 0, 0 ) - , screenPos = ( 0, 0 ) - } - in - case playlist.autoGenerated of - Just _ -> - ContextMenu - [ addToQueue identifiedTracksFromPlaylist - , convertToRegularCollection tracksFromPlaylist playlist - , downloadAsZip tracksFromPlaylist playlist - , storeInCache tracksFromPlaylist - ] - coordinates - - Nothing -> - ContextMenu - [ addToQueue identifiedTracksFromPlaylist - , downloadAsZip tracksFromPlaylist playlist - , removePlaylist menuMsg confirmation playlist - , renamePlaylist playlist - , storeInCache tracksFromPlaylist - , convert playlist - ] - coordinates - - - --- ITEMS - - -addToQueue identifiedTracks = - Item - { icon = Icons.update - , label = "Add to queue" - , msg = - { inFront = False, tracks = identifiedTracks } - |> Queue.AddTracks - |> QueueMsg - - -- - , active = False - } - - -convert playlist = - Item - { icon = Icons.waves - , label = - if playlist.collection then - "Convert to playlist" - - else - "Convert to collection" - , msg = - if playlist.collection then - ConvertCollectionToPlaylist { name = playlist.name } - - else - ConvertPlaylistToCollection { name = playlist.name } - - -- - , active = False - } - - -convertToRegularCollection tracksFromPlaylist playlist = - Item - { icon = Icons.waves - , label = "Save as regular collection" - , msg = - AddTracksToPlaylist - { collection = True - , playlistName = playlist.name - , tracks = List.map Tracks.playlistTrackFromTrack tracksFromPlaylist - } - - -- - , active = False - } - - -downloadAsZip tracksFromPlaylist playlist = - Item - { icon = Icons.archive - , label = "Download as zip file" - , msg = - tracksFromPlaylist - |> Tracks.Download - { prefixTrackNumber = not playlist.collection - , zipName = playlist.name - } - |> TracksMsg - - -- - , active = False - } - - -removePlaylist menuMsg confirmation playlist = - let - playlistId = - "Playlist - " ++ playlist.name - - askForConfirmation = - confirmation == Just playlistId - in - Item - { icon = Icons.delete - , label = - if askForConfirmation then - "Are you sure?" - - else - "Remove playlist" - , msg = - if askForConfirmation then - DeletePlaylist { playlistName = playlist.name } - - else - ContextMenuConfirmation playlistId menuMsg - , active = - askForConfirmation - } - - -renamePlaylist playlist = - Item - { icon = Icons.font_download - , label = "Rename playlist" - , msg = - playlist.name - |> Url.percentEncode - |> UI.Playlists.Page.Edit - |> UI.Page.Playlists - |> ChangeUrlUsingPage - - -- - , active = False - } - - -storeInCache tracksFromPlaylist = - Item - { icon = Icons.offline_bolt - , label = "Store in cache" - , msg = TracksMsg (Tracks.StoreInCache tracksFromPlaylist) - , active = False - } diff --git a/src/Core/UI/Playlists/Directory.elm b/src/Core/UI/Playlists/Directory.elm deleted file mode 100644 index addc860c0..000000000 --- a/src/Core/UI/Playlists/Directory.elm +++ /dev/null @@ -1,99 +0,0 @@ -module UI.Playlists.Directory exposing (generate) - -import Dict exposing (Dict) -import List.Extra as List -import Playlists exposing (..) -import Set exposing (Set) -import Sources exposing (Source) -import String.Ext as String -import Tracks exposing (Track) - - - --- 🔱 - - -generate : List Source -> List Track -> List Playlist -generate sources tracks = - let - sourceDirectories = - List.foldl - (\s -> - if s.enabled && s.directoryPlaylists then - s.data - |> Dict.get "directoryPath" - |> Maybe.map fixPath - |> Maybe.withDefault "" - |> Dict.insert s.id - - else - identity - ) - Dict.empty - sources - - playlistNames = - List.foldr - (reducer sourceDirectories) - Set.empty - tracks - in - playlistNames - |> Set.toList - |> List.map - (\n -> - let - s = - String.split "/" n - in - { autoGenerated = Just { level = List.length s - 1 } - , collection = True - , name = Maybe.withDefault n (List.last s) - , public = False - , tracks = [] - } - ) - - -fixPath : String -> String -fixPath string = - string - |> String.chopStart "/" - |> (\s -> - if String.isEmpty s || String.endsWith "/" s then - s - - else - s ++ "/" - ) - - - --- ㊙️ - - -reducer : Dict String String -> Track -> Set String -> Set String -reducer sourceDirectories track = - case Dict.get track.sourceId sourceDirectories of - Just prefix -> - let - path = - String.dropLeft (String.length prefix) track.path - in - case String.split "/" path of - "" :: _ :: _ -> - identity - - a :: _ :: _ -> - case prefix of - "" -> - Set.insert a - - _ -> - Set.insert (prefix ++ "/" ++ a) - - _ -> - identity - - Nothing -> - identity diff --git a/src/Core/UI/Playlists/Page.elm b/src/Core/UI/Playlists/Page.elm deleted file mode 100644 index b7b2264af..000000000 --- a/src/Core/UI/Playlists/Page.elm +++ /dev/null @@ -1,10 +0,0 @@ -module UI.Playlists.Page exposing (Page(..)) - --- 🌳 - - -type Page - = Edit String - | Index - | NewCollection - | NewPlaylist diff --git a/src/Core/UI/Playlists/State.elm b/src/Core/UI/Playlists/State.elm deleted file mode 100644 index 65345db60..000000000 --- a/src/Core/UI/Playlists/State.elm +++ /dev/null @@ -1,539 +0,0 @@ -module UI.Playlists.State exposing (..) - -import Conditional exposing (ifThenElse) -import Coordinates -import Html.Events.Extra.Mouse as Mouse -import List.Ext as List -import List.Extra as List -import Maybe.Extra as Maybe -import Notifications -import Playlists exposing (..) -import Return exposing (andThen) -import Tracks exposing (IdentifiedTrack) -import Tracks.Collection -import UI.Alfred.State as Alfred -import UI.Common.State as Common -import UI.Page as Page -import UI.Playlists.Alfred -import UI.Playlists.ContextMenu as Playlists -import UI.Playlists.Page exposing (..) -import UI.Tracks.State as Tracks -import UI.Types exposing (..) -import UI.User.State.Export as User - - - --- 🔱 - - -activate : Playlist -> Manager -activate playlist model = - model - |> select playlist - |> andThen (Common.changeUrlUsingPage Page.Index) - - -addTracksToPlaylist : { collection : Bool, playlistName : String, tracks : List PlaylistTrackWithoutMetadata } -> Manager -addTracksToPlaylist { collection, playlistName, tracks } model = - let - properPlaylistName = - String.trim playlistName - - playlistIndex = - List.findIndex - (\p -> Maybe.isNothing p.autoGenerated && p.name == properPlaylistName) - model.playlists - - ( tracksAlreadyInPlaylist, newTracks ) = - playlistIndex - |> Maybe.andThen - (\a -> - if collection then - Just a - - else - Nothing - ) - |> Maybe.andThen (\idx -> List.getAt idx model.playlists) - |> Maybe.map - (\p -> - List.foldl - (\track ( a, b, c ) -> - case - List.findIndex - (\x -> - track.title == x.title && track.album == x.album && track.artist == x.artist - ) - c - of - Just idx -> - ( track :: a, b, List.removeAt idx c ) - - Nothing -> - ( a, track :: b, c ) - ) - ( [], [], p.tracks ) - tracks - ) - |> Maybe.map (\( a, b, _ ) -> ( a, b )) - |> Maybe.withDefault ( [], tracks ) - |> Tuple.mapSecond - (List.map - (\track -> - let - newTrack : PlaylistTrack - newTrack = - { album = track.album - , artist = track.artist - , title = track.title - - -- - , insertedAt = model.currentTime - } - in - newTrack - ) - ) - - newInventory = - case playlistIndex of - Just idx -> - List.updateAt - idx - (\p -> { p | tracks = p.tracks ++ newTracks }) - model.playlists - - Nothing -> - { autoGenerated = Nothing - , collection = collection - , name = properPlaylistName - , public = False - , tracks = newTracks - } - :: model.playlists - - newModel = - { model - | playlists = newInventory - , lastModifiedPlaylist = - Just - { collection = collection - , name = properPlaylistName - } - } - - subject = - ifThenElse collection "collection" "playlist" - in - case newTracks of - [] -> - if collection then - (case tracksAlreadyInPlaylist of - [ t ] -> - "__" ++ t.title ++ "__ was" - - l -> - "__" ++ String.fromInt (List.length l) ++ " tracks__ were" - ) - |> (\s -> s ++ " already added to the __" ++ properPlaylistName ++ "__ collection") - |> Notifications.casual - |> Common.showNotificationWithModel model - - else - Return.singleton model - - _ -> - (case newTracks of - [ t ] -> - "Added __" ++ t.title ++ "__" - - l -> - "Added __" ++ String.fromInt (List.length l) ++ " tracks__" - ) - |> (\s -> s ++ " to the __" ++ properPlaylistName ++ "__ " ++ subject) - |> Notifications.success - |> Common.showNotificationWithModel newModel - |> andThen User.savePlaylists - - -assistWithAddingTracksToCollection : List IdentifiedTrack -> Manager -assistWithAddingTracksToCollection tracks model = - model.playlists - |> List.filter (\p -> p.autoGenerated == Nothing && p.collection == True) - |> UI.Playlists.Alfred.create { collectionMode = True } tracks - |> (\a -> Alfred.assign a model) - - -assistWithAddingTracksToPlaylist : List IdentifiedTrack -> Manager -assistWithAddingTracksToPlaylist tracks model = - model.playlists - |> List.filter (\p -> p.autoGenerated == Nothing && p.collection == False) - |> UI.Playlists.Alfred.create { collectionMode = False } tracks - |> (\a -> Alfred.assign a model) - - -assistWithSelectingPlaylist : Manager -assistWithSelectingPlaylist model = - model.playlists - |> UI.Playlists.Alfred.select - |> (\a -> Alfred.assign a model) - - -convertCollectionToPlaylist : { name : String } -> Manager -convertCollectionToPlaylist { name } model = - case - List.findIndex - (\p -> Maybe.isNothing p.autoGenerated && p.name == name) - model.playlists - of - Just playlistIndex -> - model.playlists - |> List.updateAt - playlistIndex - (\p -> { p | collection = False }) - |> (\newInventory -> - { model - | playlists = newInventory - , selectedPlaylist = - Maybe.map - (\p -> - if p.name == name then - { p | collection = False } - - else - p - ) - model.selectedPlaylist - } - ) - |> Return.singleton - |> andThen User.savePlaylists - - Nothing -> - Return.singleton model - - -convertPlaylistToCollection : { name : String } -> Manager -convertPlaylistToCollection { name } model = - case - List.findIndex - (\p -> Maybe.isNothing p.autoGenerated && p.name == name) - model.playlists - of - Just playlistIndex -> - model.playlists - |> List.updateAt - playlistIndex - (\p -> { p | collection = True }) - |> (\newInventory -> - { model - | playlists = newInventory - , selectedPlaylist = - Maybe.map - (\p -> - if p.name == name then - { p | collection = True } - - else - p - ) - model.selectedPlaylist - } - ) - |> Return.singleton - |> andThen User.savePlaylists - - Nothing -> - Return.singleton model - - -create : { collection : Bool } -> Manager -create { collection } model = - case model.newPlaylistContext of - Just playlistName -> - let - alreadyExists = - List.find - (.name >> (==) playlistName) - (List.filterNot (.autoGenerated >> Maybe.isJust) model.playlists) - - playlist = - { autoGenerated = Nothing - , collection = collection - , name = playlistName - , public = False - , tracks = [] - } - in - case alreadyExists of - Just existingPlaylist -> - (if existingPlaylist.collection then - "There's already a collection using this name" - - else - "There's already a playlist using this name" - ) - |> Notifications.error - |> Common.showNotificationWithModel model - - Nothing -> - { model - | lastModifiedPlaylist = - Just - { collection = playlist.collection - , name = playlist.name - } - , newPlaylistContext = Nothing - , playlists = playlist :: model.playlists - } - |> User.savePlaylists - |> andThen redirectToPlaylistIndexPage - - Nothing -> - Return.singleton model - - -createCollection : Manager -createCollection = - create { collection = True } - - -createPlaylist : Manager -createPlaylist = - create { collection = False } - - -deactivate : Manager -deactivate = - deselect - - -deselect : Manager -deselect model = - { model | selectedPlaylist = Nothing } - |> Tracks.reviseCollection Tracks.Collection.arrange - |> andThen User.saveEnclosedUserData - - -delete : { playlistName : String } -> Manager -delete { playlistName } model = - let - selectedPlaylist = - Maybe.map - (\p -> ( p.autoGenerated, p.name )) - model.selectedPlaylist - - ( selectedPlaylistChanged, newSelectedPlaylist ) = - if selectedPlaylist == Just ( Nothing, playlistName ) then - ( True, Nothing ) - - else - ( False, model.selectedPlaylist ) - in - model.playlists - |> List.filter - (\p -> - if Maybe.isJust p.autoGenerated then - True - - else - p.name /= playlistName - ) - |> (\col -> - { model - | playlists = col - , selectedPlaylist = newSelectedPlaylist - } - ) - |> (if selectedPlaylistChanged then - Tracks.reviseCollection Tracks.Collection.arrange - - else - Return.singleton - ) - |> andThen User.savePlaylists - - -modify : Manager -modify model = - case model.editPlaylistContext of - Just { oldName, newName } -> - let - properName = - String.trim newName - - validName = - String.isEmpty properName == False - - ( autoGenerated, notAutoGenerated ) = - List.partition (.autoGenerated >> Maybe.isJust) model.playlists - - alreadyExists = - List.find - (.name >> (==) properName) - notAutoGenerated - - newCollection = - List.map - (\p -> ifThenElse (p.name == oldName) { p | name = properName } p) - notAutoGenerated - in - case alreadyExists of - Just existingPlaylist -> - (if existingPlaylist.collection then - "There's already a collection using this name" - - else - "There's already a playlist using this name" - ) - |> Notifications.error - |> Common.showNotificationWithModel - { model | editPlaylistContext = Nothing } - - Nothing -> - if validName then - { model - | editPlaylistContext = Nothing - , lastModifiedPlaylist = - case model.lastModifiedPlaylist of - Just l -> - if l.name == oldName then - Just { l | name = newName } - - else - Just l - - Nothing -> - Nothing - , playlists = newCollection ++ autoGenerated - } - |> User.savePlaylists - |> andThen redirectToPlaylistIndexPage - - else - redirectToPlaylistIndexPage model - - Nothing -> - redirectToPlaylistIndexPage model - - -moveTrackInSelected : { to : Int } -> Manager -moveTrackInSelected { to } model = - case model.selectedPlaylist of - Just playlist -> - let - moveParams = - { from = Maybe.withDefault 0 (List.head model.selectedTrackIndexes) - , to = to - , amount = List.length model.selectedTrackIndexes - } - - updatedPlaylist = - { playlist | tracks = List.move moveParams playlist.tracks } - - updatedPlaylistCollection = - List.map - (\p -> - ifThenElse - (p.autoGenerated == Nothing && p.name == updatedPlaylist.name) - updatedPlaylist - p - ) - model.playlists - in - { model - | playlists = updatedPlaylistCollection - , selectedPlaylist = Just updatedPlaylist - } - |> Tracks.reviseCollection Tracks.Collection.arrange - |> andThen User.savePlaylists - - Nothing -> - Return.singleton model - - -removeTracks : Playlist -> List IdentifiedTrack -> Manager -removeTracks playlist tracks model = - let - updatedPlaylist = - Tracks.removeFromPlaylist tracks playlist - in - model.playlists - |> List.map - (\p -> - if p.name == playlist.name then - updatedPlaylist - - else - p - ) - |> (\c -> { model | playlists = c }) - |> select updatedPlaylist - |> andThen User.savePlaylists - - -select : Playlist -> Manager -select playlist model = - { model | page = Page.Index, selectedPlaylist = Just playlist } - |> Tracks.reviseCollection Tracks.Collection.arrange - |> andThen User.saveEnclosedUserData - - -setCreationContext : String -> Manager -setCreationContext playlistName model = - Return.singleton { model | newPlaylistContext = Just playlistName } - - -setModificationContext : String -> String -> Manager -setModificationContext oldName newName model = - let - context = - { oldName = oldName - , newName = newName - } - in - Return.singleton { model | editPlaylistContext = Just context } - - -showListMenu : Playlist -> Mouse.Event -> Manager -showListMenu playlist mouseEvent model = - let - coordinates = - Coordinates.fromTuple mouseEvent.clientPos - - contextMenu = - Playlists.listMenu - playlist - model.tracks.identified - model.confirmation - coordinates - in - Return.singleton { model | contextMenu = Just contextMenu } - - -toggleVisibility : Playlist -> Manager -toggleVisibility playlist model = - let - updatedPlaylist = - { playlist | public = not playlist.public } - in - model.playlists - |> List.map - (\p -> - if p.name == playlist.name then - updatedPlaylist - - else - p - ) - |> (\c -> { model | playlists = c }) - |> User.savePlaylists - - - --- ㊙️ - - -redirectToPlaylistIndexPage : Manager -redirectToPlaylistIndexPage = - Common.changeUrlUsingPage (Page.Playlists Index) diff --git a/src/Core/UI/Ports.elm b/src/Core/UI/Ports.elm deleted file mode 100644 index ab535ea07..000000000 --- a/src/Core/UI/Ports.elm +++ /dev/null @@ -1,181 +0,0 @@ -port module UI.Ports exposing (..) - -import Alien -import Json.Encode as Json -import Queue -import UI.Audio.Types as Audio - - - --- 📣 - - -port activeQueueItemChanged : Maybe Queue.EngineItem -> Cmd msg - - -port adjustEqualizerSetting : { knob : String, value : Float } -> Cmd msg - - -port copyToClipboard : String -> Cmd msg - - -port downloadJsonUsingTauri : { filename : String, json : String } -> Cmd msg - - -port loadAlbumCovers : { list : Bool, coverView : Bool } -> Cmd msg - - -port openUrlOnNewPage : String -> Cmd msg - - -port pause : { trackId : String } -> Cmd msg - - -port pauseScrobbleTimer : () -> Cmd msg - - -port pickAverageBackgroundColor : String -> Cmd msg - - -port play : { trackId : String, volume : Float } -> Cmd msg - - -port reloadAudioNodeIfNeeded : { play : Bool, progress : Maybe Float, trackId : String } -> Cmd msg - - -port preloadAudio : Queue.EngineItem -> Cmd msg - - -port reloadApp : () -> Cmd msg - - -port renderAudioElements : { items : List Queue.EngineItem, play : Maybe String, volume : Float } -> Cmd msg - - -port resetScrobbleTimer : { duration : Float, trackId : String } -> Cmd msg - - -port seek : { percentage : Float, trackId : String } -> Cmd msg - - -port sendTask : Json.Value -> Cmd msg - - -port setMediaSessionArtwork : { blobUrl : String, imageType : String } -> Cmd msg - - -port setMediaSessionMetadata : { album : Maybe String, artist : Maybe String, title : String, coverPrep : Maybe Audio.CoverPrep } -> Cmd msg - - -port setMediaSessionPlaybackState : String -> Cmd msg - - -port setMediaSessionPositionState : { currentTime : Float, duration : Float } -> Cmd msg - - -port startScrobbleTimer : () -> Cmd msg - - -port toBrain : Alien.Event -> Cmd msg - - - --- 📰 - - -port audioDurationChange : (Audio.DurationChangeEvent -> msg) -> Sub msg - - -port audioEnded : (Audio.GenericAudioEvent -> msg) -> Sub msg - - -port audioError : (Audio.ErrorAudioEvent -> msg) -> Sub msg - - -port audioPlaybackStateChanged : (Audio.PlaybackStateEvent -> msg) -> Sub msg - - -port audioIsLoading : (Audio.GenericAudioEvent -> msg) -> Sub msg - - -port audioHasLoaded : (Audio.GenericAudioEvent -> msg) -> Sub msg - - -port audioTimeUpdated : (Audio.TimeUpdatedEvent -> msg) -> Sub msg - - -port downloadTracksFinished : (() -> msg) -> Sub msg - - -port fromAlien : (Alien.Event -> msg) -> Sub msg - - -port lostWindowFocus : (() -> msg) -> Sub msg - - -port indicateTouchDevice : (() -> msg) -> Sub msg - - -port insertCoverCache : (Json.Value -> msg) -> Sub msg - - -port installedNewServiceWorker : (() -> msg) -> Sub msg - - -port installingNewServiceWorker : (() -> msg) -> Sub msg - - -port refreshedAccessToken : (Json.Value -> msg) -> Sub msg - - -port preferredColorSchemaChanged : ({ dark : Bool } -> msg) -> Sub msg - - -port receiveTask : (Json.Value -> msg) -> Sub msg - - -port requestNext : (() -> msg) -> Sub msg - - -port requestPause : (() -> msg) -> Sub msg - - -port requestPlay : (() -> msg) -> Sub msg - - -port requestPlayPause : (() -> msg) -> Sub msg - - -port requestPrevious : (() -> msg) -> Sub msg - - -port requestStop : (() -> msg) -> Sub msg - - -port scrobble : ({ duration : Int, timestamp : Int, trackId : String } -> msg) -> Sub msg - - -port setAverageBackgroundColor : ({ r : Int, g : Int, b : Int } -> msg) -> Sub msg - - -port setIsOnline : (Bool -> msg) -> Sub msg - - -port showErrorNotification : (String -> msg) -> Sub msg - - -port showStickyErrorNotification : (String -> msg) -> Sub msg - - - --- 🔱 - - -giveBrain : Alien.Tag -> Json.Value -> Cmd msg -giveBrain tag data = - toBrain (Alien.broadcast tag data) - - -nudgeBrain : Alien.Tag -> Cmd msg -nudgeBrain tag = - toBrain (Alien.trigger tag) diff --git a/src/Core/UI/Queue/ContextMenu.elm b/src/Core/UI/Queue/ContextMenu.elm deleted file mode 100644 index 864df73ae..000000000 --- a/src/Core/UI/Queue/ContextMenu.elm +++ /dev/null @@ -1,155 +0,0 @@ -module UI.Queue.ContextMenu exposing (futureMenu, futureNavigationMenu, historyMenu) - -import ContextMenu exposing (..) -import Coordinates exposing (Coordinates) -import Material.Icons.Round as Icons -import Queue -import UI.Queue.Types as Queue -import UI.Tracks.ContextMenu -import UI.Types exposing (Msg(..)) - - - --- 🔱 - - -futureMenu : - { cached : List String, cachingInProgress : List String, itemIndex : Int } - -> Queue.Item - -> Coordinates - -> ContextMenu Msg -futureMenu { cached, cachingInProgress, itemIndex } item = - let - tracks = - [ item.identifiedTrack ] - in - ContextMenu - [ Item - { icon = Icons.update - , label = "Move to the top" - , msg = - { index = itemIndex } - |> Queue.MoveItemToFirst - |> QueueMsg - - -- - , active = False - } - , Item - { icon = Icons.update - , label = "Move to the end of my picks" - , msg = - { index = itemIndex } - |> Queue.MoveItemToLast - |> QueueMsg - - -- - , active = False - } - , Item - { icon = Icons.waves - , label = "Add to collection" - , msg = AssistWithAddingTracksToCollection tracks - , active = False - } - , Item - { icon = Icons.waves - , label = "Add to playlist" - , msg = AssistWithAddingTracksToPlaylist tracks - , active = False - } - , UI.Tracks.ContextMenu.cacheAction - { cached = cached, cachingInProgress = cachingInProgress } - tracks - ] - - -futureNavigationMenu : { manualEntries : List Queue.Item } -> Coordinates -> ContextMenu Msg -futureNavigationMenu { manualEntries } = - [ [ Item - { icon = Icons.not_interested - , label = "Reset ignored" - , msg = QueueMsg Queue.ResetIgnored - - -- - , active = False - } - ] - , -- - if List.isEmpty manualEntries then - [] - - else - [ Item - { icon = Icons.waves - , label = "Add queue picks to collection" - , msg = - manualEntries - |> List.map .identifiedTrack - |> AssistWithAddingTracksToCollection - , active = False - } - , Item - { icon = Icons.waves - , label = "Add queue picks to playlist" - , msg = - manualEntries - |> List.map .identifiedTrack - |> AssistWithAddingTracksToPlaylist - , active = False - } - ] - ] - |> List.concat - |> ContextMenu - - -historyMenu : - { cached : List String, cachingInProgress : List String } - -> Queue.Item - -> Coordinates - -> ContextMenu Msg -historyMenu { cached, cachingInProgress } item = - let - tracks = - [ item.identifiedTrack ] - in - ContextMenu - [ Item - { icon = Icons.update - , label = "Play next" - , msg = - { inFront = True, tracks = tracks } - |> Queue.AddTracks - |> QueueMsg - - -- - , active = False - } - , Item - { icon = Icons.update - , label = "Add to queue" - , msg = - { inFront = False, tracks = tracks } - |> Queue.AddTracks - |> QueueMsg - - -- - , active = False - } - , Item - { icon = Icons.waves - , label = "Add to collection" - , msg = AssistWithAddingTracksToCollection tracks - , active = False - } - , Item - { icon = Icons.waves - , label = "Add to playlist" - , msg = AssistWithAddingTracksToPlaylist tracks - , active = False - } - , UI.Tracks.ContextMenu.cacheAction - { cached = cached, cachingInProgress = cachingInProgress } - tracks - ] diff --git a/src/Core/UI/Queue/Fill.elm b/src/Core/UI/Queue/Fill.elm deleted file mode 100644 index 6fb44e6b0..000000000 --- a/src/Core/UI/Queue/Fill.elm +++ /dev/null @@ -1,251 +0,0 @@ -module UI.Queue.Fill exposing (State, cleanAutoGenerated, ordered, queueLength, shuffled) - -{-| These functions will return a new list for the `future` property. --} - -import Array -import List.Extra as List -import Maybe.Ext as Maybe -import Maybe.Extra as Maybe -import Queue exposing (Item, makeItem) -import Random exposing (Generator, Seed) -import Time -import Tracks exposing (IdentifiedTrack) - - - --- ⛩ - - -queueLength : Int -queueLength = - 30 - - - --- 🌳 - - -type alias State = - { activeItem : Maybe Item - , future : List Item - , ignored : List Item - , past : List Item - } - - - --- 🔱 ░░ ORDERED - - -ordered : Time.Posix -> List IdentifiedTrack -> State -> List Item -ordered _ unfilteredTracks state = - let - tracks = - state.ignored - |> List.map itemTrackId - |> Tuple.pair [] - |> purifier unfilteredTracks - |> Tuple.first - - manualEntries = - List.filter (.manualEntry >> (==) True) state.future - - remaining = - max (queueLength - List.length manualEntries) 0 - - focus = - Maybe.preferFirst (List.last manualEntries) state.activeItem - in - case focus of - Just item -> - let - maybeNowPlayingIndex = - List.findIndex - (Tracks.isNowPlaying item.identifiedTrack) - tracks - in - maybeNowPlayingIndex - |> Maybe.map (\idx -> List.drop (idx + 1) tracks) - |> Maybe.withDefault tracks - |> List.take remaining - |> (\a -> - let - actualRemaining = - remaining - List.length a - - n = - Maybe.withDefault (List.length tracks) maybeNowPlayingIndex - in - a ++ List.take (min n actualRemaining) tracks - ) - |> List.map (makeItem False) - |> List.append manualEntries - - Nothing -> - tracks - |> List.take remaining - |> List.map (makeItem False) - |> List.append manualEntries - - - --- 🔱 ░░ SHUFFLED - - -shuffled : Time.Posix -> List IdentifiedTrack -> State -> List Item -shuffled timestamp unfilteredTracks state = - let - idsToIgnoreWithoutPast = - [ state.ignored - , Maybe.unwrap [] List.singleton state.activeItem - ] - |> List.map (List.map itemTrackId) - |> List.concat - |> List.unique - - ( tracksWithPast, idsToIgnoreWithoutPastAfterFilter ) = - purifier unfilteredTracks ( [], idsToIgnoreWithoutPast ) - - idsToIgnoreWithPast = - List.unique (idsToIgnoreWithoutPastAfterFilter ++ List.map itemTrackId state.past) - - ( tracksWithoutPast, idsToIgnoreWithPastAfterFilter ) = - purifier tracksWithPast ( [], idsToIgnoreWithPast ) - - idsToIgnoreWithFuture = - List.unique (idsToIgnoreWithoutPastAfterFilter ++ List.map itemTrackId state.future) - - ( tracksList, _ ) = - purifier - (case tracksWithoutPast of - [] -> - tracksWithPast - - t -> - t - ) - ( [], idsToIgnoreWithFuture ) - - tracks = - Array.fromList tracksList - - amountOfTracks = - Array.length tracks - - generator = - Random.int 0 (amountOfTracks - 1) - - toAmount = - max (queueLength - List.length state.future) 0 - - howMany = - min toAmount amountOfTracks - in - if howMany > 0 then - timestamp - |> Time.posixToMillis - |> Random.initialSeed - |> generateIndexes generator howMany [] - |> List.foldl - (\idx acc -> - case Array.get idx tracks of - Just track -> - makeItem False track :: acc - - Nothing -> - acc - ) - [] - |> List.append state.future - - else - state.future - - - --- 🔱 - - -cleanAutoGenerated : Bool -> String -> List Item -> List Item -cleanAutoGenerated shuffle trackId future = - if shuffle then - List.filterNot - (\i -> i.manualEntry == False && itemTrackId i == trackId) - future - - else - future - - - --- ㊙️ - - -{-| Generated random indexes. - - `squirrel` = accumulator, ie. collected indexes - --} -generateIndexes : Generator Int -> Int -> List Int -> Seed -> List Int -generateIndexes generator howMany squirrel seed = - let - ( index, newSeed ) = - Random.step generator seed - in - if List.member index squirrel then - generateIndexes generator howMany squirrel newSeed - - else if howMany - 1 > 0 then - generateIndexes generator (howMany - 1) (index :: squirrel) newSeed - - else - index :: squirrel - - - --- PURIFY - - -purifier : - List IdentifiedTrack - -> ( List IdentifiedTrack, List String ) - -> ( List IdentifiedTrack, List String ) -purifier tracks ( acc, idsToIgnore ) = - case idsToIgnore of - [] -> - -- Nothing more to ignore, - -- stop here. - ( acc ++ tracks, [] ) - - _ -> - case tracks of - [] -> - -- No more tracks left, - -- end of the road. - ( acc, idsToIgnore ) - - (( _, track ) as identifiedTrack) :: rest -> - case List.elemIndex track.id idsToIgnore of - Just ignoreIdx -> - -- It's a track to ignore, - -- remove it from the ignore list and carry on. - purifier - rest - ( acc, List.removeAt ignoreIdx idsToIgnore ) - - Nothing -> - -- It's not a track to ignore, - -- add it to the to-keep list and carry on. - purifier - rest - ( identifiedTrack :: acc, idsToIgnore ) - - - --- COMMON - - -itemTrackId : Item -> String -itemTrackId = - .identifiedTrack >> Tuple.second >> .id diff --git a/src/Core/UI/Queue/Page.elm b/src/Core/UI/Queue/Page.elm deleted file mode 100644 index c641eb1a8..000000000 --- a/src/Core/UI/Queue/Page.elm +++ /dev/null @@ -1,8 +0,0 @@ -module UI.Queue.Page exposing (Page(..)) - --- 🌳 - - -type Page - = Index - | History diff --git a/src/Core/UI/Queue/State.elm b/src/Core/UI/Queue/State.elm deleted file mode 100644 index 2dc2f7ed8..000000000 --- a/src/Core/UI/Queue/State.elm +++ /dev/null @@ -1,584 +0,0 @@ -module UI.Queue.State exposing (..) - -import Coordinates -import Debouncer.Basic as Debouncer -import Dict -import Html.Events.Extra.Mouse as Mouse -import List.Extra as List -import Notifications -import Queue exposing (..) -import Return exposing (andThen) -import Return.Ext as Return -import Tracks exposing (..) -import UI.Audio.Types exposing (AudioLoadingState(..)) -import UI.Common.State as Common -import UI.Ports as Ports -import UI.Queue.ContextMenu as Queue -import UI.Queue.Fill as Fill -import UI.Queue.Types as Queue exposing (..) -import UI.Types exposing (..) -import UI.User.State.Export exposing (..) - - - --- 📣 - - -update : Queue.Msg -> Manager -update msg = - case msg of - Clear -> - clear - - PreloadNext -> - preloadNext - - Reset -> - reset - - ResetIgnored -> - resetIgnored - - Rewind -> - rewind - - Shift -> - shift - - Select a -> - select a - - ShowFutureMenu a b c -> - showFutureMenu a b c - - ShowFutureNavigationMenu a -> - showFutureNavigationMenu a - - ShowHistoryMenu a b -> - showHistoryMenu a b - - ToggleRepeat -> - toggleRepeat - - ToggleShuffle -> - toggleShuffle - - ------------------------------------ - -- Future - ------------------------------------ - AddTracks a -> - addTracks a - - InjectFirst a b -> - injectFirst a b - - InjectLast a b -> - injectLast a b - - InjectFirstAndPlay a -> - injectFirstAndPlay a - - MoveItemToFirst a -> - moveItemToFirst a - - MoveItemToLast a -> - moveItemToLast a - - RemoveItem a -> - removeItem a - - - --- 🛠 - - -changeActiveItem : Maybe Item -> Manager -changeActiveItem maybeItem model = - let - maybeNowPlaying = - Maybe.map - (\item -> - { coverLoaded = False - , duration = Nothing - , isPlaying = False - , item = item - , loadingState = Loading - , playbackPosition = 0 - } - ) - maybeItem - in - maybeItem - |> Maybe.map (.identifiedTrack >> Tuple.second) - |> Maybe.map - (Queue.makeEngineItem - False - model.currentTime - model.sources - model.cachedTracks - (if model.rememberProgress then - model.progress - - else - Dict.empty - ) - ) - |> Maybe.map insertTrack - |> Maybe.withDefault Return.singleton - |> (\fn -> fn { model | nowPlaying = maybeNowPlaying }) - |> andThen fill - - -clear : Manager -clear model = - fill { model | playingNext = [] } - - -fill : Manager -fill model = - let - ( availableTracks, timestamp ) = - ( case ( model.selectedCover, model.coverSelectionReducesPool ) of - ( Just cover, True ) -> - Tuple.first <| List.foldl coverTracksHarvester ( [], cover.trackIds ) model.tracks.harvested - - _ -> - model.tracks.harvested - -- - , model.currentTime - ) - - nonMissingTracks = - List.filter - (Tuple.second >> .id >> (/=) Tracks.missingId) - availableTracks - in - model - |> (\m -> - -- Empty the ignored list when we are ignoring all the tracks - if List.length model.dontPlay == List.length nonMissingTracks then - { m | dontPlay = [] } - - else - m - ) - |> (\m -> - if m.shuffle && List.length model.playingNext >= Fill.queueLength then - m - - else - let - fillState = - { activeItem = Maybe.map .item m.nowPlaying - , future = m.playingNext - , ignored = m.dontPlay - , past = m.playedPreviously - } - in - -- Fill using the appropiate method - if m.shuffle then - { m | playingNext = Fill.shuffled timestamp nonMissingTracks fillState } - - else - { m | playingNext = Fill.ordered timestamp nonMissingTracks fillState } - ) - |> Return.communicate - (Queue.PreloadNext - |> QueueMsg - |> Debouncer.provideInput - |> AudioPreloadDebounce - |> Return.task - ) - - -insertTrack : EngineItem -> Manager -insertTrack item model = - item - |> (\engineItem -> - if - List.any - (\a -> engineItem.trackId == a.trackId) - model.audioElements - then - List.map - (\a -> - if engineItem.trackId == a.trackId then - { a | isPreload = False } - - else - a - ) - model.audioElements - - else - model.audioElements ++ [ engineItem ] - ) - |> List.filter - (\a -> - if item.isPreload then - True - - else if a.trackId /= item.trackId && not a.isPreload then - False - - else - True - ) - |> (\a -> { model | audioElements = a }) - |> Return.singleton - |> Return.effect_ - (\m -> - Ports.renderAudioElements - { items = m.audioElements - , play = - if item.isPreload then - Nothing - - else - Just item.trackId - , volume = m.eqSettings.volume - } - ) - - -preloadNext : Manager -preloadNext model = - case List.head model.playingNext of - Just item -> - item - |> .identifiedTrack - |> Tuple.second - |> Queue.makeEngineItem - True - model.currentTime - model.sources - model.cachedTracks - (if model.rememberProgress then - model.progress - - else - Dict.empty - ) - |> (\engineItem -> - insertTrack engineItem model - ) - - Nothing -> - Return.singleton model - - -rewind : Manager -rewind model = - changeActiveItem - (List.last model.playedPreviously) - { model - | playingNext = - model.nowPlaying - |> Maybe.map (\{ item } -> item :: model.playingNext) - |> Maybe.withDefault model.playingNext - , playedPreviously = - model.playedPreviously - |> List.init - |> Maybe.withDefault [] - } - - -{-| Renew the queue, meaning that the auto-generated items in the queue are removed and new items are added. --} -reset : Manager -reset model = - model.playingNext - |> List.filter (.manualEntry >> (==) True) - |> (\f -> { model | playingNext = f }) - |> fill - - -resetIgnored : Manager -resetIgnored model = - fill { model | dontPlay = [] } - - -select : Item -> Manager -select item model = - Return.singleton { model | selectedQueueItem = Just item } - - -shift : Manager -shift model = - changeActiveItem - (List.head model.playingNext) - { model - | playingNext = - model.playingNext - |> List.drop 1 - , playedPreviously = - model.nowPlaying - |> Maybe.map (.item >> List.singleton) - |> Maybe.map (List.append model.playedPreviously) - |> Maybe.withDefault model.playedPreviously - } - - -showFutureMenu : Item -> { index : Int } -> Mouse.Event -> Manager -showFutureMenu item { index } mouseEvent model = - mouseEvent.clientPos - |> Coordinates.fromTuple - |> Queue.futureMenu - { cached = model.cachedTracks - , cachingInProgress = model.cachingTracksInProgress - , itemIndex = index - } - item - |> Just - |> (\c -> { model | contextMenu = c }) - |> Return.singleton - - -showFutureNavigationMenu : Mouse.Event -> Manager -showFutureNavigationMenu mouseEvent model = - mouseEvent.clientPos - |> Coordinates.fromTuple - |> Queue.futureNavigationMenu { manualEntries = List.filter .manualEntry model.playingNext } - |> Just - |> (\c -> { model | contextMenu = c }) - |> Return.singleton - - -showHistoryMenu : Item -> Mouse.Event -> Manager -showHistoryMenu item mouseEvent model = - mouseEvent.clientPos - |> Coordinates.fromTuple - |> Queue.historyMenu - { cached = model.cachedTracks - , cachingInProgress = model.cachingTracksInProgress - } - item - |> Just - |> (\c -> { model | contextMenu = c }) - |> Return.singleton - - -toggleRepeat : Manager -toggleRepeat model = - saveEnclosedUserData { model | repeat = not model.repeat } - - -toggleShuffle : Manager -toggleShuffle model = - { model | shuffle = not model.shuffle } - |> reset - |> andThen saveEnclosedUserData - - - --- 🛠 ░░ FUTURE - - -addTracks : { inFront : Bool, tracks : List IdentifiedTrack } -> Manager -addTracks { inFront, tracks } = - (if inFront then - injectFirst - - else - injectLast - ) - { showNotification = True } - tracks - - -{-| Add an item in front of the queue. --} -injectFirst : { showNotification : Bool } -> List IdentifiedTrack -> Manager -injectFirst { showNotification } identifiedTracks model = - let - ( items, tracks ) = - ( List.map (makeItem True) identifiedTracks - , List.map Tuple.second identifiedTracks - ) - - cleanedFuture = - List.foldl - (.id >> Fill.cleanAutoGenerated model.shuffle) - model.playingNext - tracks - - notification = - case tracks of - [ t ] -> - ("__" ++ t.tags.title ++ "__ will be played next") - |> Notifications.casual - - list -> - list - |> List.length - |> String.fromInt - |> (\s -> "__" ++ s ++ " tracks__ will be played next") - |> Notifications.casual - in - { model | playingNext = items ++ cleanedFuture } - |> (if showNotification then - Common.showNotification notification - - else - Return.singleton - ) - |> andThen fill - - -injectFirstAndPlay : IdentifiedTrack -> Manager -injectFirstAndPlay identifiedTrack model = - model - |> injectFirst { showNotification = False } [ identifiedTrack ] - |> andThen shift - - -{-| Add an item after the last manual entry -(ie. after the last injected item). --} -injectLast : { showNotification : Bool } -> List IdentifiedTrack -> Manager -injectLast { showNotification } identifiedTracks model = - let - ( items, tracks ) = - ( List.map (makeItem True) identifiedTracks - , List.map Tuple.second identifiedTracks - ) - - cleanedFuture = - List.foldl - (.id >> Fill.cleanAutoGenerated model.shuffle) - model.playingNext - tracks - - manualItems = - cleanedFuture - |> List.filter (.manualEntry >> (==) True) - |> List.length - - newFuture = - [] - ++ List.take manualItems cleanedFuture - ++ items - ++ List.drop manualItems cleanedFuture - - notification = - case tracks of - [ t ] -> - ("__" ++ t.tags.title ++ "__ was added to the queue") - |> Notifications.casual - - list -> - list - |> List.length - |> String.fromInt - |> (\s -> "__" ++ s ++ " tracks__ were added to the queue") - |> Notifications.casual - in - { model | playingNext = newFuture } - |> (if showNotification then - Common.showNotification notification - - else - Return.singleton - ) - |> andThen fill - - -moveItemToFirst : { index : Int } -> Manager -moveItemToFirst { index } model = - model.playingNext - |> moveItem { from = index, to = 0, shuffle = model.shuffle } - |> (\f -> { model | playingNext = f }) - |> fill - - -moveItemToLast : { index : Int } -> Manager -moveItemToLast { index } model = - let - to = - model.playingNext - |> List.filter (.manualEntry >> (==) True) - |> List.length - in - model.playingNext - |> moveItem { from = index, to = to, shuffle = model.shuffle } - |> (\f -> { model | playingNext = f }) - |> fill - - -removeItem : { index : Int, item : Item } -> Manager -removeItem { index, item } model = - let - newFuture = - List.removeAt index model.playingNext - - newIgnored = - if item.manualEntry then - model.dontPlay - - else - item :: model.dontPlay - in - fill { model | playingNext = newFuture, dontPlay = newIgnored } - - - --- ⚗️ - - -coverTracksHarvester : - IdentifiedTrack - -> ( List IdentifiedTrack, List String ) - -> ( List IdentifiedTrack, List String ) -coverTracksHarvester ( i, t ) ( acc, coverTrackIds ) = - case List.findIndex ((==) t.id) coverTrackIds of - Just idx -> - ( acc ++ [ ( i, t ) ] - , List.removeAt idx coverTrackIds - ) - - Nothing -> - ( acc - , coverTrackIds - ) - - -moveItem : { from : Int, to : Int, shuffle : Bool } -> List Item -> List Item -moveItem { from, to, shuffle } collection = - let - subjectItem = - collection - |> List.getAt from - |> Maybe.map (\s -> { s | manualEntry = True }) - - fixedTarget = - if to > from then - to - 1 - - else - to - in - collection - |> List.removeAt from - |> List.indexedFoldr - (\idx existingItem acc -> - if idx == fixedTarget then - case subjectItem of - Just itemToInsert -> - List.append [ itemToInsert, existingItem ] acc - - Nothing -> - existingItem :: acc - - else if idx < fixedTarget then - { existingItem | manualEntry = True } :: acc - - else - existingItem :: acc - ) - [] - |> (if shuffle then - identity - - else - List.filter (.manualEntry >> (==) True) - ) diff --git a/src/Core/UI/Queue/Types.elm b/src/Core/UI/Queue/Types.elm deleted file mode 100644 index 9cc99e02e..000000000 --- a/src/Core/UI/Queue/Types.elm +++ /dev/null @@ -1,34 +0,0 @@ -module UI.Queue.Types exposing (..) - -import Html.Events.Extra.Mouse as Mouse -import Queue exposing (Item) -import Tracks exposing (IdentifiedTrack) - - - --- 📣 - - -type Msg - = Clear - | PreloadNext - | Reset - | ResetIgnored - | Rewind - | Select Item - | Shift - | ShowFutureMenu Item { index : Int } Mouse.Event - | ShowFutureNavigationMenu Mouse.Event - | ShowHistoryMenu Item Mouse.Event - | ToggleRepeat - | ToggleShuffle - ------------------------------------ - -- Future - ------------------------------------ - | AddTracks { inFront : Bool, tracks : List IdentifiedTrack } - | InjectFirst { showNotification : Bool } (List IdentifiedTrack) - | InjectLast { showNotification : Bool } (List IdentifiedTrack) - | InjectFirstAndPlay IdentifiedTrack - | MoveItemToFirst { index : Int } - | MoveItemToLast { index : Int } - | RemoveItem { index : Int, item : Item } diff --git a/src/Core/UI/Routing/State.elm b/src/Core/UI/Routing/State.elm deleted file mode 100644 index b8b512278..000000000 --- a/src/Core/UI/Routing/State.elm +++ /dev/null @@ -1,161 +0,0 @@ -module UI.Routing.State exposing (linkClicked, openUrlOnNewPage, resetUrl, transition, urlChanged) - -import Browser exposing (UrlRequest) -import Browser.Navigation as Nav -import List.Extra as List -import Monocle.Lens as Lens -import Return exposing (return) -import Sources -import Sources.Services.Dropbox -import Sources.Services.Google -import UI.Common.State as Common -import UI.Page as Page exposing (Page) -import UI.Ports as Ports -import UI.Sources.Form -import UI.Sources.Page -import UI.Sources.State as Sources -import UI.Sources.Types as Sources -import UI.Types as UI exposing (Manager) -import Url exposing (Url) - - - --- 🔱 - - -linkClicked : UrlRequest -> Manager -linkClicked urlRequest model = - case urlRequest of - Browser.Internal urlWithFragment -> - let - url = - if urlWithFragment.fragment == Just "/" then - { urlWithFragment | fragment = Nothing } - - else - urlWithFragment - in - if url.path /= model.url.path then - return model (Nav.load url.path) - - else - return model (Nav.pushUrl model.navKey <| Url.toString url) - - Browser.External href -> - return model (Nav.load href) - - -openUrlOnNewPage : String -> Manager -openUrlOnNewPage url model = - url - |> Ports.openUrlOnNewPage - |> return model - - -urlChanged : Url -> Manager -urlChanged url model = - let - rewrittenUrl = - Page.rewriteUrl { url | query = Nothing } - in - case ( url.query, Page.fromUrl rewrittenUrl ) of - ( Nothing, Just page ) -> - transition page { model | page = page, url = url } - - ( Just _, Just page ) -> - return model (resetUrl model.navKey url page) - - _ -> - return model (resetUrl model.navKey url Page.Index) - - - --- TRANSITIONING - - -transition : Page -> Manager -transition page model = - case page of - ----------------------------------------- - -- Sources.NewThroughRedirect - ----------------------------------------- - Page.Sources (UI.Sources.Page.NewThroughRedirect service args) -> - let - ( form, defaultContext ) = - ( model.sourceForm - , UI.Sources.Form.defaultContext - ) - in - { defaultContext - | data = - case service of - Sources.Dropbox -> - Sources.Services.Dropbox.authorizationSourceData args - - Sources.Google -> - Sources.Services.Google.authorizationSourceData args - - _ -> - defaultContext.data - , service = - service - } - |> (\c -> { form | context = c, step = Sources.By }) - |> (\f -> { model | sourceForm = f }) - |> Return.singleton - - ----------------------------------------- - -- Sources.Edit - ----------------------------------------- - Page.Sources (UI.Sources.Page.Edit sourceId) -> - loadSourceForForm sourceId model - - ----------------------------------------- - -- Sources.Rename - ----------------------------------------- - Page.Sources (UI.Sources.Page.Rename sourceId) -> - loadSourceForForm sourceId model - - ----------------------------------------- - -- 📭 - ----------------------------------------- - _ -> - Return.singleton model - - - --- 🚀 - - -resetUrl : Nav.Key -> Url -> Page.Page -> Cmd UI.Msg -resetUrl key url page = - Nav.replaceUrl key (url.path ++ Page.toString page) - - - --- ㊙️ - - -loadSourceForForm : String -> Manager -loadSourceForForm sourceId model = - let - isLoading = - model.isLoading - - maybeSource = - List.find (.id >> (==) sourceId) model.sources - in - case ( isLoading, maybeSource ) of - ( False, Just source ) -> - model - |> Lens.modify Sources.formLens (\m -> { m | context = source }) - |> Return.singleton - - ( False, Nothing ) -> - Return.singleton model - - ( True, _ ) -> - -- Redirect away from edit-source page - Common.changeUrlUsingPage - (Page.Sources UI.Sources.Page.Index) - model diff --git a/src/Core/UI/Services/State.elm b/src/Core/UI/Services/State.elm deleted file mode 100644 index b0c7cdc04..000000000 --- a/src/Core/UI/Services/State.elm +++ /dev/null @@ -1,75 +0,0 @@ -module UI.Services.State exposing (..) - -import Browser.Navigation as Nav -import Common -import Http -import LastFm -import Notifications -import Return exposing (andThen, return) -import String.Ext as String -import UI.Common.State exposing (showNotification) -import UI.Types exposing (Manager, Msg(..)) -import UI.User.State.Export as User -import Url - - - --- 🔱 - - -connectLastFm : Manager -connectLastFm model = - model.url - |> Common.urlOrigin - |> String.addSuffix "?action=authenticate/lastfm" - |> Url.percentEncode - |> String.append "&cb=" - |> String.append - (String.append - "http://www.last.fm/api/auth/?api_key=" - LastFm.apiKey - ) - |> Nav.load - |> return model - - -disconnectLastFm : Manager -disconnectLastFm model = - User.saveSettings { model | lastFm = LastFm.disconnect model.lastFm } - - -gotLastFmSession : Result Http.Error String -> Manager -gotLastFmSession result model = - case result of - Err _ -> - showNotification - (Notifications.stickyError "Could not connect with Last.fm") - { model | lastFm = LastFm.failedToAuthenticate model.lastFm } - - Ok sessionKey -> - { model | lastFm = LastFm.gotSessionKey sessionKey model.lastFm } - |> showNotification - (Notifications.success "Connected successfully with Last.fm") - |> andThen - User.saveSettings - - -scrobble : { duration : Int, timestamp : Int, trackId : String } -> Manager -scrobble { duration, timestamp, trackId } model = - case Maybe.map (.item >> .identifiedTrack) model.nowPlaying of - Just ( _, track ) -> - if trackId == track.id then - ( model - , LastFm.scrobble model.lastFm - { duration = duration - , msg = Bypass - , timestamp = timestamp - , track = track - } - ) - - else - Return.singleton model - - Nothing -> - Return.singleton model diff --git a/src/Core/UI/Settings/Page.elm b/src/Core/UI/Settings/Page.elm deleted file mode 100644 index b0a94c940..000000000 --- a/src/Core/UI/Settings/Page.elm +++ /dev/null @@ -1,9 +0,0 @@ -module UI.Settings.Page exposing (Page(..)) - --- 🌳 - - -type Page - = Data - | Index - | Sync diff --git a/src/Core/UI/Sources/ContextMenu.elm b/src/Core/UI/Sources/ContextMenu.elm deleted file mode 100644 index 15dd745ec..000000000 --- a/src/Core/UI/Sources/ContextMenu.elm +++ /dev/null @@ -1,84 +0,0 @@ -module UI.Sources.ContextMenu exposing (sourceMenu) - -import Conditional exposing (ifThenElse) -import ContextMenu exposing (..) -import Coordinates exposing (Coordinates) -import Material.Icons.Round as Icons -import Sources exposing (Source) -import UI.Page -import UI.Sources.Page -import UI.Sources.Types as Sources -import UI.Types exposing (Msg(..)) - - - --- 🔱 - - -sourceMenu : Source -> Coordinates -> ContextMenu Msg -sourceMenu source = - ContextMenu - [ Item - { icon = ifThenElse source.directoryPlaylists Icons.folder Icons.folder_open - , label = ifThenElse source.directoryPlaylists "Disable Directory Playlists" "Enable Directory Playlists" - , msg = - { sourceId = source.id } - |> Sources.ToggleDirectoryPlaylists - |> SourcesMsg - - -- - , active = False - } - - -- - , Item - { icon = Icons.edit - , label = "Edit source" - , msg = - source.id - |> UI.Sources.Page.Edit - |> UI.Page.Sources - |> ChangeUrlUsingPage - - -- - , active = False - } - - -- - , Item - { icon = Icons.sync - , label = "Process source" - , msg = - [ source ] - |> Sources.ProcessSpecific - |> SourcesMsg - - -- - , active = False - } - - -- - , Item - { icon = Icons.delete - , label = "Remove source" - , msg = - { sourceId = source.id } - |> Sources.RemoveFromCollection - |> SourcesMsg - , active = False - } - - -- - , Item - { icon = Icons.font_download - , label = "Rename source" - , msg = - source.id - |> UI.Sources.Page.Rename - |> UI.Page.Sources - |> ChangeUrlUsingPage - - -- - , active = False - } - ] diff --git a/src/Core/UI/Sources/Form.elm b/src/Core/UI/Sources/Form.elm deleted file mode 100644 index 9f675e4a9..000000000 --- a/src/Core/UI/Sources/Form.elm +++ /dev/null @@ -1,31 +0,0 @@ -module UI.Sources.Form exposing (..) - -import Sources exposing (..) -import Sources.Services as Services -import UI.Sources.Types exposing (..) - - - --- 🌳 - - -initialModel : Form -initialModel = - { step = Where - , context = defaultContext - } - - -defaultContext : Source -defaultContext = - { id = "CHANGE_ME_PLEASE" - , data = Services.initialData defaultService - , directoryPlaylists = True - , enabled = True - , service = defaultService - } - - -defaultService : Service -defaultService = - Dropbox diff --git a/src/Core/UI/Sources/Page.elm b/src/Core/UI/Sources/Page.elm deleted file mode 100644 index 47c35bf57..000000000 --- a/src/Core/UI/Sources/Page.elm +++ /dev/null @@ -1,16 +0,0 @@ -module UI.Sources.Page exposing (Page(..)) - -import Sources exposing (Service) - - - --- 🌳 - - -type Page - = Index - | Edit String - | New - | NewOnboarding - | NewThroughRedirect Service { codeOrToken : Maybe String, state : Maybe String } - | Rename String diff --git a/src/Core/UI/Sources/Query.elm b/src/Core/UI/Sources/Query.elm deleted file mode 100644 index 00f8993ce..000000000 --- a/src/Core/UI/Sources/Query.elm +++ /dev/null @@ -1,68 +0,0 @@ -module UI.Sources.Query exposing (..) - -import Dict -import Json.Decode as Decode -import Sources exposing (Source) -import Url exposing (Url) -import Url.Parser as Url -import Url.Parser.Query as Query - - -requestedAddition : Url -> Bool -requestedAddition url = - case Url.parse (urlParser identity) url of - Nothing -> - False - - Just [] -> - False - - Just (_ :: _) -> - True - - -sourcesFromUrl : Url -> List Source -sourcesFromUrl url = - url - |> Url.parse (urlParser <| List.filterMap fromUrl) - |> Maybe.withDefault [] - - - --- 🔬 - - -fromUrl : String -> Maybe Source -fromUrl json = - json - |> Decode.decodeString sourceParser - |> Result.toMaybe - - -urlParser individualParser = - individualParser - |> Query.custom "source" - |> Url.query - - -sourceParser : Decode.Decoder Source -sourceParser = - Decode.andThen - (\{ service, data } -> - if Dict.member "name" data then - Decode.succeed - { id = "FILL_IN_LATER" - , data = data - , directoryPlaylists = True - , enabled = True - , service = service - } - - else - Decode.fail "Missing `name` in `data` dictionary" - ) - <| - Decode.map2 - (\s d -> { service = s, data = d }) - (Decode.field "kind" Sources.serviceDecoder) - (Decode.field "data" <| Decode.dict Decode.string) diff --git a/src/Core/UI/Sources/State.elm b/src/Core/UI/Sources/State.elm deleted file mode 100644 index 10c8f592c..000000000 --- a/src/Core/UI/Sources/State.elm +++ /dev/null @@ -1,581 +0,0 @@ -module UI.Sources.State exposing (..) - -import Alien -import Browser.Navigation as Nav -import Common -import Conditional exposing (ifThenElse) -import Coordinates -import Dict -import Dict.Ext as Dict -import Html.Events.Extra.Mouse as Mouse -import Json.Decode as Json -import Json.Encode -import Monocle.Lens as Lens -import Notifications -import Return exposing (andThen, return) -import Sources exposing (..) -import Sources.Encoding as Sources -import Sources.Services as Services -import Sources.Services.Dropbox -import Sources.Services.Google -import Tracks.Collection -import UI.Common.State as Common -import UI.Page as Page -import UI.Ports as Ports -import UI.Sources.ContextMenu as Sources -import UI.Sources.Form as Form -import UI.Sources.Page as Sources -import UI.Sources.Query -import UI.Sources.Types exposing (..) -import UI.Tracks.State as Tracks -import UI.Types as UI exposing (Manager, Model) -import UI.User.State.Export as User - - - --- 🌳 - - -formLens = - { get = .sourceForm - , set = \form m -> { m | sourceForm = form } - } - - -formContextLens = - Lens.compose - formLens - { get = .context - , set = \context m -> { m | context = context } - } - - -formStepLens = - Lens.compose - formLens - { get = .step - , set = \step m -> { m | step = step } - } - - - --- 📣 - - -update : Msg -> Manager -update msg = - case msg of - Bypass -> - Return.singleton - - -- - FinishedProcessingSource a -> - finishedProcessingSource a - - FinishedProcessing -> - finishedProcessing - - Process -> - process - - ProcessSpecific a -> - processSpecific a - - ReportProcessingError a -> - reportProcessingError a - - ReportProcessingProgress a -> - reportProcessingProgress a - - StopProcessing -> - stopProcessing - - ----------------------------------------- - -- Collection - ----------------------------------------- - AddToCollection a -> - addToCollection a - - RemoveFromCollection a -> - removeFromCollection a - - UpdateSourceData a -> - updateSourceData a - - ----------------------------------------- - -- Form - ----------------------------------------- - AddSourceUsingForm -> - addSourceUsingForm - - EditSourceUsingForm -> - editSourceUsingForm - - RenameSourceUsingForm -> - renameSourceUsingForm - - ReturnToIndex -> - returnToIndex - - SelectService a -> - selectService a - - SetFormData a b -> - setFormData a b - - TakeStep -> - takeStep - - TakeStepBackwards -> - takeStepBackwards - - ----------------------------------------- - -- Individual - ----------------------------------------- - SourceContextMenu a b -> - sourceContextMenu a b - - ToggleActivation a -> - toggleActivation a - - ToggleDirectoryPlaylists a -> - toggleDirectoryPlaylists a - - ToggleProcessAutomatically -> - toggleProcessAutomatically - - - --- 🔱 - - -addSourcesFromUrl : Manager -addSourcesFromUrl model = - case UI.Sources.Query.sourcesFromUrl model.url of - [] -> - Return.singleton model - - sources -> - sources - |> List.foldl - (\s -> andThen <| addToCollection s) - (Return.singleton model) - |> Return.command - (Nav.replaceUrl - model.navKey - (model.url.path ++ Page.toString model.page) - ) - - -finishedProcessing : Manager -finishedProcessing model = - (case model.processingNotificationId of - Just id -> - Common.dismissNotification { id = id } - - Nothing -> - Return.singleton - ) - { model | processingContext = [] } - - -finishedProcessingSource : Json.Value -> Manager -finishedProcessingSource json model = - case Json.decodeValue Json.string json of - Ok sourceId -> - model.processingContext - |> List.filter (Tuple.first >> (/=) sourceId) - |> (\newContext -> { model | processingContext = newContext }) - |> Return.singleton - - Err _ -> - Return.singleton model - - -process : Manager -process model = - case sourcesToProcess model of - [] -> - Return.singleton model - - toProcess -> - if model.isOnline then - processSpecific toProcess model - - else - toProcess - |> List.filter Sources.worksOffline - |> (\s -> - case s of - [] -> - Return.singleton model - - _ -> - processSpecific s model - ) - - -processSpecific : List Source -> Manager -processSpecific toProcess model = - let - notification = - Notifications.stickyCasual "Processing sources ..." - - notificationId = - Notifications.id notification - - newNotifications = - List.filter - (\n -> Notifications.kind n /= Notifications.Error) - model.notifications - - processingContext = - toProcess - |> List.sortBy (.data >> Dict.fetch "name" "") - |> List.map (\{ id } -> ( id, 0 )) - - newModel = - { model - | notifications = newNotifications - , processingContext = processingContext - , processingError = Nothing - , processingNotificationId = Just notificationId - } - in - [ ( "origin" - , Json.Encode.string (Common.urlOrigin model.url) - ) - , ( "sources" - , Json.Encode.list Sources.encode toProcess - ) - ] - |> Json.Encode.object - |> Alien.broadcast Alien.ProcessSources - |> Ports.toBrain - |> return newModel - |> andThen (Common.showNotification notification) - - -reportProcessingError : Json.Value -> Manager -reportProcessingError json model = - case Json.decodeValue (Json.dict Json.string) json of - Ok dict -> - let - args = - { error = Dict.fetch "error" "" dict - , sourceId = Dict.fetch "sourceId" "" dict - } - in - [] - |> Notifications.errorWithCode - ("Could not process the _" - ++ Dict.fetch "sourceName" "" dict - ++ "_ source. I got the following response from the source:" - ) - (Dict.fetch "error" "missingError" dict) - |> Common.showNotificationWithModel - { model | processingError = Just args } - - Err _ -> - "Could not decode processing error" - |> Notifications.stickyError - |> Common.showNotificationWithModel model - - -reportProcessingProgress : Json.Value -> Manager -reportProcessingProgress json model = - case - Json.decodeValue - (Json.map2 - (\p s -> - { progress = p - , sourceId = s - } - ) - (Json.field "progress" Json.float) - (Json.field "sourceId" Json.string) - ) - json - of - Ok { progress, sourceId } -> - model.processingContext - |> List.map - (\( sid, pro ) -> - ifThenElse (sid == sourceId) - ( sid, progress ) - ( sid, pro ) - ) - |> (\processingContext -> - { model | processingContext = processingContext } - ) - |> Return.singleton - - Err _ -> - "Could not decode processing progress" - |> Notifications.stickyError - |> Common.showNotificationWithModel model - - -stopProcessing : Manager -stopProcessing model = - case model.processingNotificationId of - Just notificationId -> - Alien.StopProcessing - |> Alien.trigger - |> Ports.toBrain - |> return - { model - | processingContext = [] - , processingNotificationId = Nothing - } - |> andThen (Common.dismissNotification { id = notificationId }) - - Nothing -> - Return.singleton model - - - --- COLLECTION - - -addToCollection : Source -> Manager -addToCollection unsuitableSource model = - let - source = - setProperId - (List.length model.sources + 1) - model.currentTime - unsuitableSource - in - { model | sources = model.sources ++ [ source ] } - |> User.saveSources - |> andThen (processSpecific [ source ]) - - -removeFromCollection : { sourceId : String } -> Manager -removeFromCollection { sourceId } model = - model.sources - |> List.filter (.id >> (/=) sourceId) - |> (\c -> { model | sources = c }) - |> Return.singleton - |> andThen User.saveSources - |> andThen (Tracks.removeBySourceId sourceId) - - -updateSourceData : Json.Value -> Manager -updateSourceData json model = - json - |> Sources.decode - |> Maybe.map - (\source -> - List.map - (\s -> - if s.id == source.id then - source - - else - s - ) - model.sources - ) - |> Maybe.map (\col -> { model | sources = col }) - |> Maybe.withDefault model - |> User.saveSources - - - --- FORM - - -addSourceUsingForm : Manager -addSourceUsingForm model = - let - context = - model.sourceForm.context - - cleanContext = - { context | data = Dict.map (always String.trim) context.data } - in - model - |> formLens.set Form.initialModel - |> addToCollection cleanContext - |> andThen returnToIndex - - -editSourceUsingForm : Manager -editSourceUsingForm model = - model - |> formLens.set Form.initialModel - |> replaceSourceInCollection model.sourceForm.context - |> andThen process - |> andThen returnToIndex - - -renameSourceUsingForm : Manager -renameSourceUsingForm model = - model - |> formLens.set Form.initialModel - |> replaceSourceInCollection model.sourceForm.context - |> andThen returnToIndex - - -returnToIndex : Manager -returnToIndex = - Common.changeUrlUsingPage (Page.Sources Sources.Index) - - -selectService : String -> Manager -selectService serviceKey model = - case Services.keyToType serviceKey of - Just service -> - model - |> Lens.modify - formContextLens - (\c -> - { c - | data = Services.initialData service - , service = service - } - ) - |> Return.singleton - - Nothing -> - Return.singleton model - - -setFormData : String -> String -> Manager -setFormData key value model = - model - |> Lens.modify - formContextLens - (\context -> - context.data - |> Dict.insert key value - |> (\data -> { context | data = data }) - ) - |> Return.singleton - - -takeStep : Manager -takeStep model = - let - form = - formLens.get model - in - case ( form.step, form.context.service ) of - ( How, Dropbox ) -> - form.context.data - |> Sources.Services.Dropbox.authorizationUrl - |> externalAuthorization model - - ( How, Google ) -> - form.context.data - |> Sources.Services.Google.authorizationUrl - |> externalAuthorization model - - _ -> - model - |> Lens.modify formStepLens takeStepForwards - |> Return.singleton - - -takeStepBackwards : Manager -takeStepBackwards = - Lens.modify formStepLens takeStepBackwards_ >> Return.singleton - - - --- INDIVIDUAL - - -sourceContextMenu : Source -> Mouse.Event -> Manager -sourceContextMenu source mouseEvent model = - mouseEvent.clientPos - |> Coordinates.fromTuple - |> Sources.sourceMenu source - |> Common.showContextMenuWithModel model - - -toggleActivation : { sourceId : String } -> Manager -toggleActivation { sourceId } model = - model.sources - |> List.map - (\source -> - if source.id == sourceId then - { source | enabled = not source.enabled } - - else - source - ) - |> (\collection -> { model | sources = collection }) - |> Tracks.reviseCollection Tracks.Collection.identify - |> andThen Common.forceTracksRerender - |> andThen Common.generateDirectoryPlaylists - |> andThen User.saveSources - - -toggleDirectoryPlaylists : { sourceId : String } -> Manager -toggleDirectoryPlaylists { sourceId } model = - model.sources - |> List.map - (\source -> - if source.id == sourceId then - { source | directoryPlaylists = not source.directoryPlaylists } - - else - source - ) - |> (\collection -> { model | sources = collection }) - |> User.saveSources - |> andThen Common.generateDirectoryPlaylists - - -toggleProcessAutomatically : Manager -toggleProcessAutomatically model = - User.saveSettings { model | processAutomatically = not model.processAutomatically } - - - --- ⚗️ - - -externalAuthorization : Model -> (String -> String) -> ( Model, Cmd UI.Msg ) -externalAuthorization model urlBuilder = - model.url - |> Common.urlOrigin - |> urlBuilder - |> Nav.load - |> return model - - -replaceSourceInCollection : Source -> Manager -replaceSourceInCollection source model = - model.sources - |> List.map (\s -> ifThenElse (s.id == source.id) source s) - |> (\s -> { model | sources = s }) - |> User.saveSources - - -sourcesToProcess : Model -> List Source -sourcesToProcess model = - List.filter (.enabled >> (==) True) model.sources - - -takeStepForwards : FormStep -> FormStep -takeStepForwards currentStep = - case currentStep of - Where -> - How - - _ -> - By - - -takeStepBackwards_ : FormStep -> FormStep -takeStepBackwards_ currentStep = - case currentStep of - By -> - How - - _ -> - Where diff --git a/src/Core/UI/Sources/Types.elm b/src/Core/UI/Sources/Types.elm deleted file mode 100644 index f3d6e515b..000000000 --- a/src/Core/UI/Sources/Types.elm +++ /dev/null @@ -1,61 +0,0 @@ -module UI.Sources.Types exposing (..) - -import Html.Events.Extra.Mouse as Mouse -import Json.Decode as Json -import Sources exposing (..) - - - --- 🌳 - - -type alias Form = - { context : Source - , step : FormStep - } - - -type FormStep - = Where - | How - | By - - - --- 📣 - - -type Msg - = Bypass - -- - | FinishedProcessingSource Json.Value - | FinishedProcessing - | Process - | ProcessSpecific (List Source) - | ReportProcessingError Json.Value - | ReportProcessingProgress Json.Value - | StopProcessing - ----------------------------------------- - -- Collection - ----------------------------------------- - | AddToCollection Source - | RemoveFromCollection { sourceId : String } - | UpdateSourceData Json.Value - ----------------------------------------- - -- Form - ----------------------------------------- - | AddSourceUsingForm - | EditSourceUsingForm - | RenameSourceUsingForm - | ReturnToIndex - | SelectService String - | SetFormData String String - | TakeStep - | TakeStepBackwards - ----------------------------------------- - -- Individual - ----------------------------------------- - | SourceContextMenu Source Mouse.Event - | ToggleActivation { sourceId : String } - | ToggleDirectoryPlaylists { sourceId : String } - | ToggleProcessAutomatically diff --git a/src/Core/UI/Svg/Elements.elm b/src/Core/UI/Svg/Elements.elm deleted file mode 100644 index 359ef4799..000000000 --- a/src/Core/UI/Svg/Elements.elm +++ /dev/null @@ -1,118 +0,0 @@ -module UI.Svg.Elements exposing (dropboxLogo, ipfsLogo, loading, loadingWithSize, remoteStorageLogo) - -import Svg exposing (..) -import Svg.Attributes exposing (..) - - - --- LOGOS - - -ipfsLogo : Int -> Svg Never -ipfsLogo size = - svg - [ height (String.fromInt size) - , viewBox "0 0 511.99999 511.99998" - , width (String.fromInt size) - ] - [ -- Group 1 - ---------- - g - [ transform "translate(-50.017 -515.51)" ] - [ Svg.path - [ d "m283.13 546.35-160.74 92.806c0.32126 2.8543 0.32125 5.7352 0 8.5894l160.75 92.806c13.554-10.001 32.043-10.001 45.597 0l160.75-92.807c-0.32126-2.8543-0.32293-5.7338-0.001-8.588l-160.74-92.806c-13.554 10.001-32.044 10.001-45.599 0zm221.79 127.03-160.92 93.84c1.884 16.739-7.3611 32.751-22.799 39.489l0.18062 184.58c2.6325 1.1489 5.1267 2.5886 7.438 4.294l160.75-92.805c-1.884-16.739 7.3611-32.752 22.799-39.49v-185.61c-2.6325-1.1489-5.1281-2.5886-7.4394-4.294zm-397.81 1.0315c-2.3112 1.7054-4.8054 3.1465-7.438 4.2954v185.61c15.438 6.7378 24.683 22.75 22.799 39.489l160.74 92.806c2.3112-1.7054 4.8069-3.1465 7.4394-4.2954v-185.61c-15.438-6.7378-24.683-22.75-22.799-39.489l-160.74-92.81z" - , fill "currentColor" - ] - [] - ] - - -- Group 2 - ---------- - , g - [ fill "currentColor" - , transform "translate(0 -196.66)" - ] - [ Svg.path - [ d "m256 708.66 221.7-128v-256l-221.7 128v256z" - , fillOpacity "1" - ] - [] - , Svg.path - [ d "m256 708.66v-256l-221.7-128v256l221.7 128z" - , fillOpacity ".75" - ] - [] - , Svg.path - [ d "m34.298 324.66 221.7 128 221.7-128-221.7-128-221.7 128z" - , fillOpacity ".5" - ] - [] - ] - ] - - -dropboxLogo : Int -> Svg Never -dropboxLogo size = - svg - [ height (String.fromInt size) - , viewBox "0 0 43 40" - , width (String.fromInt size) - ] - [ Svg.path - [ d "m12.5 0l-12.5 8.1 8.7 7 12.5-7.8-8.7-7.3zm-12.5 21.9l12.5 8.2 8.7-7.3-12.5-7.7-8.7 6.8zm21.2 0.9l8.8 7.3 12.4-8.1-8.6-6.9-12.6 7.7zm21.2-14.7l-12.4-8.1-8.8 7.3 12.6 7.8 8.6-7zm-21.1 16.3l-8.8 7.3-3.7-2.5v2.8l12.5 7.5 12.5-7.5v-2.8l-3.8 2.5-8.7-7.3z" - - -- - , fill "currentColor" - ] - [] - ] - - -remoteStorageLogo : Int -> Svg Never -remoteStorageLogo size = - svg - [ clipRule "evenodd" - , fillRule "evenodd" - , height (String.fromInt size) - , imageRendering "optimizeQuality" - , shapeRendering "geometricPrecision" - , textRendering "geometricPrecision" - , viewBox "0 0 739 853" - , width (String.fromInt size) - ] - [ polygon - [ points "370,754 0,542 0,640 185,747 370,853 554,747 739,640 739,525 739,525 739,476 739,427 739,378 653,427 370,589 86,427 86,427 86,361 185,418 370,524 554,418 653,361 739,311 739,213 739,213 554,107 370,0 185,107 58,180 144,230 228,181 370,100 511,181 652,263 370,425 87,263 87,263 0,213 0,213 0,311 0,378 0,427 0,476 86,525 185,582 370,689 554,582 653,525 653,590 653,592" - , fill "currentColor" - ] - [] - ] - - - --- LOADING ANIMATION - - -loading : Svg Never -loading = - loadingWithSize 29 - - -loadingWithSize : Int -> Svg Never -loadingWithSize size = - svg - [ class "loading-animation" - , height (String.fromInt size) - , viewBox "0 0 30 30" - , width (String.fromInt size) - ] - [ circle - [ class "loading-animation__circle" - , cx "15" - , cy "15" - , fill "none" - , r "14" - , strokeLinecap "round" - , strokeWidth "2" - ] - [] - ] diff --git a/src/Core/UI/Syncing/Common.elm b/src/Core/UI/Syncing/Common.elm deleted file mode 100644 index 6eab02541..000000000 --- a/src/Core/UI/Syncing/Common.elm +++ /dev/null @@ -1,69 +0,0 @@ -module UI.Syncing.Common exposing (..) - -import Chunky exposing (..) -import Html -import Svg -import UI.Svg.Elements -import UI.Syncing.Types exposing (..) -import UI.Types exposing (Msg(..)) -import User.Layer exposing (Method, dropboxMethod, remoteStorageMethod) - - - --- 🚀 - - -startDropbox : Msg -startDropbox = - SyncingMsg (TriggerExternalAuth dropboxMethod "") - - -startIpfs : Msg -startIpfs = - SyncingMsg PingIpfs - - -startRemoteStorage : Msg -startRemoteStorage = - { icon = \size _ -> Svg.map never (UI.Svg.Elements.remoteStorageLogo size) - , placeholder = "example@5apps.com" - , question = - { question = - "What's your user address?" - , info = - [ Html.text "The format is " - , inline - [] - [ Html.text "username@server.domain" ] - ] - } - , value = "" - } - |> AskForInput remoteStorageMethod - |> SyncingMsg - - - --- 🛠 - - -extractMethod : State -> Maybe Method -extractMethod state = - case state of - Synced method -> - Just method - - Syncing { method } -> - Just method - - InputScreen method _ -> - Just method - - NewEncryptionKeyScreen method _ -> - Just method - - UpdateEncryptionKeyScreen method _ -> - Just method - - NotSynced -> - Nothing diff --git a/src/Core/UI/Syncing/ContextMenu.elm b/src/Core/UI/Syncing/ContextMenu.elm deleted file mode 100644 index aa32f896b..000000000 --- a/src/Core/UI/Syncing/ContextMenu.elm +++ /dev/null @@ -1,37 +0,0 @@ -module UI.Syncing.ContextMenu exposing (syncDataMenu) - -import ContextMenu exposing (..) -import Coordinates exposing (Coordinates) -import Svg -import UI.Svg.Elements -import UI.Syncing.Common exposing (startDropbox, startIpfs, startRemoteStorage) -import UI.Types exposing (Msg) -import User.Layer exposing (dropboxMethod, ipfsMethod, methodName, remoteStorageMethod) - - - --- 🔱 - - -syncDataMenu : Coordinates -> ContextMenu Msg -syncDataMenu = - ContextMenu - [ Item - { icon = \_ _ -> Svg.map never (UI.Svg.Elements.dropboxLogo 16) - , label = methodName dropboxMethod - , msg = startDropbox - , active = False - } - , Item - { icon = \_ _ -> Svg.map never (UI.Svg.Elements.remoteStorageLogo 16) - , label = methodName remoteStorageMethod - , msg = startRemoteStorage - , active = False - } - , Item - { icon = \_ _ -> Svg.map never (UI.Svg.Elements.ipfsLogo 16) - , label = methodName ipfsMethod - , msg = startIpfs - , active = False - } - ] diff --git a/src/Core/UI/Syncing/State.elm b/src/Core/UI/Syncing/State.elm deleted file mode 100644 index c8de179ec..000000000 --- a/src/Core/UI/Syncing/State.elm +++ /dev/null @@ -1,642 +0,0 @@ -module UI.Syncing.State exposing (..) - -import Alien -import Base64 -import Binary -import Browser.Navigation as Nav -import Common -import Coordinates -import Dict -import Html -import Html.Attributes -import Html.Events.Extra.Mouse as Mouse -import Http -import Http.Ext as Http -import Json.Decode as Json -import Json.Encode -import Lens.Ext as Lens -import Management -import Maybe.Extra as Maybe -import Monocle.Lens exposing (Lens) -import Notifications -import Return exposing (andThen, return) -import SHA -import String.Ext as String -import Svg -import Time -import UI.Backdrop as Backdrop -import UI.Common.State as Common exposing (showNotification, showNotificationWithModel) -import UI.Ports as Ports -import UI.Sources.State as Sources -import UI.Svg.Elements -import UI.Syncing.ContextMenu as Syncing -import UI.Syncing.Types as Syncing exposing (..) -import UI.Types as UI exposing (..) -import Url exposing (Protocol(..), Url) -import Url.Ext as Url -import UrlBase64 -import User.Layer exposing (..) -import User.Layer.Methods.Dropbox as Dropbox -import User.Layer.Methods.RemoteStorage as RemoteStorage - - - --- ⛩ - - -minimumPassphraseLength : Int -minimumPassphraseLength = - 16 - - -passphraseLengthErrorMessage : String -passphraseLengthErrorMessage = - "Your passphrase should be atleast *16 characters* long." - - - --- 🌳 - - -initialModel : Url -> Syncing.State -initialModel url = - case Url.action url of - [ "authenticate", "remotestorage", encodedUserAddress ] -> - let - dict = - Url.queryDictionary { url | query = url.fragment } - - userAddress = - encodedUserAddress - |> Url.percentDecode - |> Maybe.andThen (UrlBase64.decode Base64.decode >> Result.toMaybe) - |> Maybe.withDefault encodedUserAddress - in - case Dict.get "access_token" dict of - Just t -> - NewEncryptionKeyScreen - (RemoteStorage - { userAddress = userAddress - , token = t - } - ) - Nothing - - Nothing -> - NotSynced - - _ -> - NotSynced - - -initialCommand : Url -> Cmd Syncing.Msg -initialCommand url = - case Url.action url of - [ "authenticate", "dropbox" ] -> - case Dict.get "code" (Url.queryDictionary url) of - Just code -> - Dropbox.exchangeAuthCode - ExchangeDropboxAuthCode - url - code - - _ -> - Cmd.none - - _ -> - Cmd.none - - -lens : Lens UI.Model Syncing.State -lens = - { get = .syncing - , set = \a m -> { m | syncing = a } - } - - - --- 📣 - - -update : Syncing.Msg -> Manager -update msg = - case msg of - Syncing.Bypass -> - Return.singleton - - ActivateSync a -> - activateSync a - - ActivateSyncWithPassphrase a b -> - activateSyncWithPassphrase a b - - BootFailure a -> - bootFailure a - - ExchangeDropboxAuthCode a -> - exchangeDropboxAuthCode a - - GotSyncMethod a -> - gotSyncMethod a - - RemoteStorageWebfinger a b -> - remoteStorageWebfinger a b - - ShowSyncDataMenu a -> - showSyncDataMenu a - - StartedSyncing a -> - startedSyncing a - - StopSync -> - stopSync - - TriggerExternalAuth a b -> - externalAuth a b - - ----------------------------------------- - -- Encryption - ----------------------------------------- - KeepPassphraseInMemory a -> - keepPassphraseInMemory a - - NeedEncryptionKey a -> - needEncryptionKey a - - RemoveEncryptionKey a -> - removeEncryptionKey a - - ShowNewEncryptionKeyScreen a -> - showNewEncryptionKeyScreen a - - ShowUpdateEncryptionKeyScreen a -> - showUpdateEncryptionKeyScreen a - - UpdateEncryptionKey a b -> - updateEncryptionKey a b - - ----------------------------------------- - -- IPFS - ----------------------------------------- - PingIpfs -> - pingIpfs - - PingIpfsCallback a -> - pingIpfsCallback a - - PingOtherIpfs a -> - pingOtherIpfs a - - PingOtherIpfsCallback a b -> - pingOtherIpfsCallback a b - - ----------------------------------------- - -- More Input - ----------------------------------------- - AskForInput a b -> - askForInput a b - - CancelInput -> - cancelInput - - ConfirmInput -> - confirmInput - - Input a -> - input a - - -organize : Organizer Syncing.State -> Manager -organize = - Management.organize lens - - -replaceState : Syncing.State -> Manager -replaceState state = - lens.set state >> Return.singleton - - - --- 🔱 - - -activateSync : Method -> Manager -activateSync method model = - [ ( "method", encodeMethod method ) - , ( "passphrase", Json.Encode.null ) - ] - |> Json.Encode.object - |> Alien.broadcast Alien.SetSyncMethod - |> Ports.toBrain - -- - |> return model - - -activateSyncWithPassphrase : Method -> String -> Manager -activateSyncWithPassphrase method passphrase model = - if String.length passphrase < minimumPassphraseLength then - passphraseLengthErrorMessage - |> Notifications.error - |> Common.showNotificationWithModel model - - else - [ ( "method", encodeMethod method ) - , ( "passphrase", Json.Encode.string <| hashPassphrase passphrase ) - ] - |> Json.Encode.object - |> Alien.broadcast Alien.SetSyncMethod - |> Ports.toBrain - -- - |> return model - - -bootFailure : String -> Manager -bootFailure err model = - model - |> showNotification (Notifications.error err) - |> andThen Backdrop.setDefault - - -externalAuth : Method -> String -> Manager -externalAuth method string model = - case method of - Dropbox _ -> - [ ( "client_id", Dropbox.clientId ) - , ( "redirect_uri", Dropbox.redirectUri model.url ) - , ( "response_type", "code" ) - , ( "token_access_type", "offline" ) - ] - |> Common.queryString - |> String.append "https://www.dropbox.com/oauth2/authorize" - |> Nav.load - |> return model - - RemoteStorage _ -> - string - |> RemoteStorage.parseUserAddress - |> Maybe.map (RemoteStorage.webfingerRequest RemoteStorageWebfinger model.url.protocol) - |> Maybe.map (Cmd.map SyncingMsg) - |> Maybe.unwrap - (RemoteStorage.userAddressError - |> Notifications.error - |> Common.showNotificationWithModel model - ) - (return model) - - _ -> - Return.singleton model - - -exchangeDropboxAuthCode : Result Http.Error Dropbox.Tokens -> Manager -exchangeDropboxAuthCode result model = - case result of - Ok tokens -> - case tokens.refreshToken of - Just refreshToken -> - Nothing - |> NewEncryptionKeyScreen - (Dropbox - { accessToken = tokens.accessToken - , expiresAt = Time.posixToMillis model.currentTime // 1000 + tokens.expiresIn - , refreshToken = refreshToken - } - ) - |> Lens.replace lens model - |> Return.singleton - - Nothing -> - "Missing refresh token in Dropbox code exchange flow." - |> Notifications.stickyError - |> showNotificationWithModel - (Lens.replace lens model NotSynced) - - Err err -> - [] - |> Notifications.errorWithCode - "Failed to authenticate with Dropbox" - (Http.errorToString err) - |> showNotificationWithModel - (Lens.replace lens model NotSynced) - - -gotSyncMethod : Json.Value -> Manager -gotSyncMethod json model = - let - afterwards a = - andThen - (\m -> - if m.processAutomatically then - Sources.process m - - else - Return.singleton m - ) - (case model.syncing of - Syncing { notificationId } -> - Common.dismissNotification { id = notificationId } a - - _ -> - Return.singleton a - ) - in - -- 🧠 told me which auth method we're using, - -- so we can tell the user in the UI. - case decodeMethod json of - Just method -> - model - |> replaceState (Synced method) - |> andThen afterwards - - Nothing -> - afterwards model - - -remoteStorageWebfinger : RemoteStorage.Attributes -> Result Http.Error String -> Manager -remoteStorageWebfinger remoteStorage result model = - case result of - Ok oauthOrigin -> - let - origin = - Common.urlOrigin model.url - in - remoteStorage - |> RemoteStorage.oauthAddress - { oauthOrigin = oauthOrigin - , origin = origin - } - |> Nav.load - |> return model - - Err _ -> - RemoteStorage.webfingerError - |> Notifications.error - |> showNotificationWithModel model - - -showSyncDataMenu : Mouse.Event -> Manager -showSyncDataMenu mouseEvent model = - mouseEvent.clientPos - |> Coordinates.fromTuple - |> Syncing.syncDataMenu - |> Common.showContextMenuWithModel model - - -startedSyncing : Json.Value -> Manager -startedSyncing json = - -- 🧠 started syncing - case decodeMethod json of - Just method -> - Common.showSyncingNotification method - - Nothing -> - Return.singleton - - -stopSync : Manager -stopSync model = - Alien.UnsetSyncMethod - |> Alien.trigger - |> Ports.toBrain - |> return model - |> andThen (replaceState NotSynced) - - - --- ENCRYPTION - - -keepPassphraseInMemory : String -> Manager -keepPassphraseInMemory passphrase model = - (\state -> - case state of - NewEncryptionKeyScreen method _ -> - NewEncryptionKeyScreen method (Just passphrase) - - UpdateEncryptionKeyScreen method _ -> - UpdateEncryptionKeyScreen method (Just passphrase) - - s -> - s - ) - |> Lens.adjust lens model - |> Return.singleton - - -needEncryptionKey : { error : String } -> Manager -needEncryptionKey { error } model = - (case lens.get model of - Syncing { notificationId } -> - Common.dismissNotification { id = notificationId } model - - m -> - replaceState m model - ) - |> andThen - (error - |> Notifications.stickyError - |> Common.showNotification - ) - |> andThen - stopSync - - -removeEncryptionKey : Method -> Manager -removeEncryptionKey method model = - Alien.RemoveEncryptionKey - |> Alien.trigger - |> Ports.toBrain - -- - |> return - (lens.set (Synced method) model) - |> andThen - ("Saving data without encryption ..." - |> Notifications.success - |> Common.showNotification - ) - |> andThen - Common.forceTracksRerender - - -showNewEncryptionKeyScreen : Method -> Manager -showNewEncryptionKeyScreen method = - replaceState (NewEncryptionKeyScreen method Nothing) - - -showUpdateEncryptionKeyScreen : Method -> Manager -showUpdateEncryptionKeyScreen method = - replaceState (UpdateEncryptionKeyScreen method Nothing) - - -updateEncryptionKey : Method -> String -> Manager -updateEncryptionKey method passphrase model = - if String.length passphrase < minimumPassphraseLength then - passphraseLengthErrorMessage - |> Notifications.error - |> Common.showNotificationWithModel model - - else - passphrase - |> hashPassphrase - |> Json.Encode.string - |> Alien.broadcast Alien.UpdateEncryptionKey - |> Ports.toBrain - -- - |> return - (lens.set (Synced method) model) - |> andThen - ("Encrypting data with new passphrase ..." - |> Notifications.success - |> Common.showNotification - ) - |> andThen - Common.forceTracksRerender - - - --- IPFS - - -pingIpfs : Manager -pingIpfs model = - case model.url.protocol of - Https -> - """ - Unfortunately the local IPFS API doesn't work with HTTPS. - Install the [IPFS Companion](https://github.com/ipfs-shipyard/ipfs-companion#release-channel) browser extension to get around this issue - (and make sure it redirects to the local gateway). - """ - |> Notifications.error - |> Common.showNotificationWithModel model - - Http -> - { url = "//localhost:5001/api/v0/id" - , expect = Http.expectWhatever (SyncingMsg << PingIpfsCallback) - , body = Http.emptyBody - } - |> Http.post - |> return model - - -pingIpfsCallback : Result Http.Error () -> Manager -pingIpfsCallback result = - case result of - Ok _ -> - { apiOrigin = "//localhost:5001" } - |> Ipfs - |> showNewEncryptionKeyScreen - - Err _ -> - askForInput - (Ipfs { apiOrigin = "" }) - { icon = \size _ -> Svg.map never (UI.Svg.Elements.ipfsLogo size) - , placeholder = "//localhost:5001" - , question = - { question = - "Where's your IPFS API located?" - , info = - [ Html.text "You can find this address on the IPFS Web UI." - , Html.br [] [] - , Html.text "Most likely you'll also need to setup CORS." - , Html.br [] [] - , Html.text "You can find the instructions for that " - , Html.a - [ Html.Attributes.class "border-b border-current-color font-semibold inline-block leading-tight" - , Html.Attributes.href "about/cors/#CORS__IPFS" - , Html.Attributes.target "_blank" - ] - [ Html.text "here" ] - ] - } - , value = "//localhost:5001" - } - - -pingOtherIpfs : String -> Manager -pingOtherIpfs origin model = - { url = origin ++ "/api/v0/id" - , expect = Http.expectWhatever (SyncingMsg << PingOtherIpfsCallback origin) - , body = Http.emptyBody - } - |> Http.post - |> return model - - -pingOtherIpfsCallback : String -> Result Http.Error () -> Manager -pingOtherIpfsCallback origin result = - case result of - Ok _ -> - { apiOrigin = origin } - |> Ipfs - |> showNewEncryptionKeyScreen - - Err _ -> - "Can't reach this IPFS API, maybe it's offline? Or I don't have access?" - |> Notifications.error - |> Common.showNotification - - - --- MORE INPUT - - -askForInput : Method -> Question -> Manager -askForInput method question = - question - |> InputScreen method - |> replaceState - - -cancelInput : Manager -cancelInput model = - case lens.get model of - InputScreen _ _ -> - replaceState NotSynced model - - NewEncryptionKeyScreen _ _ -> - replaceState NotSynced model - - UpdateEncryptionKeyScreen method _ -> - replaceState (Synced method) model - - m -> - replaceState m model - - -confirmInput : Manager -confirmInput model = - case lens.get model of - InputScreen (Ipfs _) { value } -> - pingOtherIpfs (String.chopEnd "/" value) model - - InputScreen (RemoteStorage r) { value } -> - externalAuth (RemoteStorage r) value model - - _ -> - Return.singleton model - - -input : String -> Manager -input string model = - (\state -> - case state of - InputScreen method opts -> - InputScreen method { opts | value = string } - - s -> - s - ) - |> Lens.adjust lens model - |> Return.singleton - - - --- 🛠 - - -hashPassphrase : String -> String -hashPassphrase phrase = - phrase - |> Binary.fromStringAsUtf8 - |> SHA.sha256 - |> Binary.toHex - |> String.toLower diff --git a/src/Core/UI/Syncing/Types.elm b/src/Core/UI/Syncing/Types.elm deleted file mode 100644 index ba5431af8..000000000 --- a/src/Core/UI/Syncing/Types.elm +++ /dev/null @@ -1,74 +0,0 @@ -module UI.Syncing.Types exposing (Msg(..), Question, State(..)) - -import Html exposing (Html) -import Html.Events.Extra.Mouse as Mouse -import Http -import Json.Decode as Json -import Material.Icons.Types exposing (Coloring) -import Svg exposing (Svg) -import User.Layer exposing (Method) -import User.Layer.Methods.Dropbox as Dropbox -import User.Layer.Methods.RemoteStorage as RemoteStorage - - - --- 🌳 - - -type State - = NotSynced - | Synced Method - | Syncing { method : Method, notificationId : Int } - | InputScreen Method Question - | NewEncryptionKeyScreen Method (Maybe String) - | UpdateEncryptionKeyScreen Method (Maybe String) - - -type alias Question = - { icon : Int -> Coloring -> Svg Msg - , placeholder : String - , question : { question : String, info : List (Html Msg) } - , value : String - } - - - --- 📣 - - -type Msg - = Bypass - -- - | ActivateSync Method - | ActivateSyncWithPassphrase Method String - | BootFailure String - | ExchangeDropboxAuthCode (Result Http.Error Dropbox.Tokens) - | GotSyncMethod Json.Value - | RemoteStorageWebfinger RemoteStorage.Attributes (Result Http.Error String) - | ShowSyncDataMenu Mouse.Event - | StartedSyncing Json.Value - | StopSync - | TriggerExternalAuth Method String - ----------------------------------------- - -- Encryption - ----------------------------------------- - | KeepPassphraseInMemory String - | NeedEncryptionKey { error : String } - | RemoveEncryptionKey Method - | ShowNewEncryptionKeyScreen Method - | ShowUpdateEncryptionKeyScreen Method - | UpdateEncryptionKey Method String - ----------------------------------------- - -- IPFS - ----------------------------------------- - | PingIpfs - | PingIpfsCallback (Result Http.Error ()) - | PingOtherIpfs String - | PingOtherIpfsCallback String (Result Http.Error ()) - ----------------------------------------- - -- More Input - ----------------------------------------- - | AskForInput Method Question - | CancelInput - | ConfirmInput - | Input String diff --git a/src/Core/UI/Theme.elm b/src/Core/UI/Theme.elm deleted file mode 100644 index 7a43d0220..000000000 --- a/src/Core/UI/Theme.elm +++ /dev/null @@ -1,113 +0,0 @@ -module UI.Theme exposing (..) - -import Chunky exposing (..) -import Dict exposing (Dict) -import Html exposing (Html) -import Theme exposing (Theme) -import Themes.Sunrise.Theme as Sunrise -import Themes.Sunrise.Tracks.Scene.Covers -import Themes.Sunrise.Tracks.Scene.List -import Tracks exposing (IdentifiedTrack, Scene) -import UI.Svg.Elements -import UI.Types exposing (Model, Msg) - - - --- 🔮 - - -list : List (Theme Msg Model) -list = - [ Sunrise.theme - ] - - -default : Theme Msg Model -default = - Sunrise.theme - - - --- 🚧 - - -dict : Dict String (Theme Msg Model) -dict = - list - |> List.map - (\theme -> - ( theme.id, theme ) - ) - |> Dict.fromList - - -view : Model -> Html Msg -view model = - if model.isLoading then - loadingAnimation - - else - case model.theme of - Just { id } -> - case Dict.get id dict of - Just theme -> - theme.view model - - Nothing -> - default.view model - - Nothing -> - default.view model - - -loadingAnimation = - chunk - [ "flex" - , "flex-col" - , "items-center" - , "justify-center" - , "screen-height" - , "w-screen" - ] - [ loadingAnimationSvg - , chunk - [ "italic" - , "mt-5" - , "text-white" - , "text-opacity-30" - ] - [ Html.text "Transmitting particles" ] - ] - - -loadingAnimationSvg = - Html.map never UI.Svg.Elements.loading - - - --- TODO - - -scrollTracksToTop : Scene -> Cmd Msg -scrollTracksToTop scene = - case scene of - Tracks.Covers -> - Themes.Sunrise.Tracks.Scene.List.scrollToTop - - Tracks.List -> - Themes.Sunrise.Tracks.Scene.Covers.scrollToTop - - -scrollToNowPlaying : Scene -> IdentifiedTrack -> Model -> Cmd Msg -scrollToNowPlaying scene ( identifiers, track ) model = - case scene of - Tracks.Covers -> - Themes.Sunrise.Tracks.Scene.Covers.scrollToNowPlaying - model.viewport.width - model.covers.harvested - ( identifiers, track ) - - Tracks.List -> - Themes.Sunrise.Tracks.Scene.List.scrollToNowPlaying - model.tracks.harvested - ( identifiers, track ) diff --git a/src/Core/UI/Tracks/ContextMenu.elm b/src/Core/UI/Tracks/ContextMenu.elm deleted file mode 100644 index d9d37d5a6..000000000 --- a/src/Core/UI/Tracks/ContextMenu.elm +++ /dev/null @@ -1,377 +0,0 @@ -module UI.Tracks.ContextMenu exposing (cacheAction, trackMenu, viewMenu) - -import Conditional exposing (ifThenElse) -import ContextMenu exposing (..) -import Coordinates exposing (Coordinates) -import Material.Icons.Round as Icons -import Maybe.Extra as Maybe -import Playlists exposing (Playlist) -import Queue -import Sources exposing (Source) -import Time -import Tracks exposing (Grouping(..), IdentifiedTrack) -import UI.Queue.Types as Queue -import UI.Tracks.Types as Tracks -import UI.Types exposing (Msg(..)) - - - --- TRACK MENU - - -trackMenu : - { cached : List String - , cachingInProgress : List String - , currentTime : Time.Posix - , lastModifiedPlaylistName : Maybe { collection : Bool, name : String } - , selectedPlaylist : Maybe Playlist - , showAlternativeMenu : Bool - , sources : List Source - } - -> List IdentifiedTrack - -> Coordinates - -> ContextMenu Msg -trackMenu { cached, cachingInProgress, currentTime, selectedPlaylist, lastModifiedPlaylistName, showAlternativeMenu, sources } tracks = - if showAlternativeMenu then - [ alternativeMenuActions - currentTime - sources - tracks - ] - |> List.concat - |> ContextMenu - - else - [ queueActions - tracks - - -- - , playlistActions - { selectedPlaylist = selectedPlaylist - , lastModifiedPlaylistName = lastModifiedPlaylistName - } - tracks - - -- - , cacheAction - { cached = cached - , cachingInProgress = cachingInProgress - } - tracks - |> List.singleton - ] - |> List.concat - |> ContextMenu - - -alternativeMenuActions : - Time.Posix - -> List Source - -> List IdentifiedTrack - -> List (ContextMenu.Item Msg) -alternativeMenuActions timestamp sources tracks = - case tracks of - [ ( _, t ) ] -> - [ Item - { icon = Icons.link - , label = "Copy temporary url" - , msg = CopyToClipboard (Queue.makeTrackUrl timestamp sources t) - , active = False - } - - -- - , Item - { icon = Icons.sync - , label = "Sync tags" - , msg = TracksMsg (Tracks.SyncTags [ t ]) - , active = False - } - ] - - _ -> - [] - - -cacheAction : - { cached : List String, cachingInProgress : List String } - -> List IdentifiedTrack - -> ContextMenu.Item Msg -cacheAction { cached, cachingInProgress } tracks = - case tracks of - [ ( _, t ) ] -> - if List.member t.id cached then - Item - { icon = Icons.offline_bolt - , label = "Remove from cache" - , msg = - tracks - |> List.map Tuple.second - |> Tracks.RemoveFromCache - |> TracksMsg - - -- - , active = False - } - - else if List.member t.id cachingInProgress then - Item - { icon = Icons.offline_bolt - , label = "Downloading ..." - , msg = Bypass - , active = True - } - - else - Item - { icon = Icons.offline_bolt - , label = "Store in cache" - , msg = - tracks - |> List.map Tuple.second - |> Tracks.StoreInCache - |> TracksMsg - - -- - , active = False - } - - _ -> - Item - { icon = Icons.offline_bolt - , label = "Store in cache" - , msg = - tracks - |> List.map Tuple.second - |> Tracks.StoreInCache - |> TracksMsg - - -- - , active = False - } - - -playlistActions : - { selectedPlaylist : Maybe Playlist - , lastModifiedPlaylistName : Maybe { collection : Bool, name : String } - } - -> List IdentifiedTrack - -> List (ContextMenu.Item Msg) -playlistActions { selectedPlaylist, lastModifiedPlaylistName } tracks = - let - maybeCustomPlaylist = - Maybe.andThen - (\p -> - case p.autoGenerated of - Just _ -> - Nothing - - Nothing -> - Just p - ) - selectedPlaylist - - maybeAddToLastModifiedPlaylist { collection } = - Maybe.andThen - (\l -> - if Maybe.map .name selectedPlaylist /= Just l.name && l.collection == collection then - justAnItem - { icon = Icons.waves - , label = "Add to \"" ++ l.name ++ "\"" - , msg = - AddTracksToPlaylist - { collection = collection - , playlistName = l.name - , tracks = Tracks.toPlaylistTracks tracks - } - - -- - , active = False - } - - else - Nothing - ) - lastModifiedPlaylistName - in - case maybeCustomPlaylist of - ----------------------------------------- - -- In a custom playlist - ----------------------------------------- - Just playlist -> - Maybe.values - [ maybeAddToLastModifiedPlaylist { collection = True } - , justAnItem - { icon = Icons.waves - , label = - if playlist.collection then - "Remove from collection" - - else - "Remove from playlist" - , msg = RemoveTracksFromPlaylist playlist tracks - - -- - , active = False - } - , justAnItem - { icon = Icons.waves - , label = - if playlist.collection then - "Add to another collection" - - else - "Add to collection" - , msg = AssistWithAddingTracksToCollection tracks - - -- - , active = False - } - , maybeAddToLastModifiedPlaylist { collection = False } - , justAnItem - { icon = Icons.waves - , label = - if playlist.collection then - "Add to playlist" - - else - "Add to another playlist" - , msg = AssistWithAddingTracksToPlaylist tracks - - -- - , active = False - } - ] - - ----------------------------------------- - -- Otherwise - ----------------------------------------- - _ -> - Maybe.values - [ maybeAddToLastModifiedPlaylist { collection = True } - , justAnItem - { icon = Icons.waves - , label = "Add to collection" - , msg = AssistWithAddingTracksToCollection tracks - , active = False - } - , maybeAddToLastModifiedPlaylist { collection = False } - , justAnItem - { icon = Icons.waves - , label = "Add to playlist" - , msg = AssistWithAddingTracksToPlaylist tracks - , active = False - } - ] - - -queueActions : List IdentifiedTrack -> List (ContextMenu.Item Msg) -queueActions identifiedTracks = - [ Item - { icon = Icons.update - , label = "Play next" - , msg = - { inFront = True, tracks = identifiedTracks } - |> Queue.AddTracks - |> QueueMsg - - -- - , active = False - } - , Item - { icon = Icons.update - , label = "Add to queue" - , msg = - { inFront = False, tracks = identifiedTracks } - |> Queue.AddTracks - |> QueueMsg - - -- - , active = False - } - ] - - - --- VIEW MENU - - -viewMenu : Bool -> Maybe Grouping -> Coordinates -> ContextMenu Msg -viewMenu onlyCachedTracks maybeGrouping = - ContextMenu - [ groupByDirectory (maybeGrouping == Just Directory) - , groupByFirstAlphaCharacter (maybeGrouping == Just FirstAlphaCharacter) - , groupByProcessingDate (maybeGrouping == Just AddedOn) - , groupByTrackYear (maybeGrouping == Just TrackYear) - - -- - , Item - { icon = Icons.filter_list - , label = "Cached tracks only" - , active = onlyCachedTracks - , msg = TracksMsg Tracks.ToggleCachedOnly - } - ] - - -groupByDirectory isActive = - Item - { icon = ifThenElse isActive Icons.clear Icons.library_music - , label = "Group by directory" - , active = isActive - - -- - , msg = - if isActive then - TracksMsg Tracks.DisableGrouping - - else - TracksMsg (Tracks.GroupBy Directory) - } - - -groupByFirstAlphaCharacter isActive = - Item - { icon = ifThenElse isActive Icons.clear Icons.library_music - , label = "Group by first letter" - , active = isActive - - -- - , msg = - if isActive then - TracksMsg Tracks.DisableGrouping - - else - TracksMsg (Tracks.GroupBy FirstAlphaCharacter) - } - - -groupByProcessingDate isActive = - Item - { icon = ifThenElse isActive Icons.clear Icons.library_music - , label = "Group by processing date" - , active = isActive - - -- - , msg = - if isActive then - TracksMsg Tracks.DisableGrouping - - else - TracksMsg (Tracks.GroupBy AddedOn) - } - - -groupByTrackYear isActive = - Item - { icon = ifThenElse isActive Icons.clear Icons.library_music - , label = "Group by track year" - , active = isActive - - -- - , msg = - if isActive then - TracksMsg Tracks.DisableGrouping - - else - TracksMsg (Tracks.GroupBy TrackYear) - } diff --git a/src/Core/UI/Tracks/Covers.elm b/src/Core/UI/Tracks/Covers.elm deleted file mode 100644 index 31fec7a83..000000000 --- a/src/Core/UI/Tracks/Covers.elm +++ /dev/null @@ -1,344 +0,0 @@ -module UI.Tracks.Covers exposing (..) - -import Base64 -import Conditional exposing (ifThenElse) -import Dict -import Maybe.Extra as Maybe -import Tracks exposing (..) - - - --- 🔱 - - -generate : - SortBy - -> Tracks.Collection - -> CoverCollection -generate sortBy tracks = - let - groupFn = - coverGroup sortBy - - makeCoverFn = - makeCover sortBy - in - tracks.arranged - |> List.foldr - (\identifiedTrack { covers, gathering } -> - let - group = - groupFn identifiedTrack - - ( identifiers, track ) = - identifiedTrack - - { artist, album } = - track.tags - in - if group /= gathering.previousGroup then - -- New group, make cover for previous group - let - collection = - makeCoverFn gathering covers - in - { gathering = - { acc = [ identifiedTrack ] - , accIds = [ track.id ] - , previousGroup = group - , previousTrack = track - - -- - , currentAlbumSequence = Just ( identifiedTrack, 1 ) - , largestAlbumSequence = Nothing - - -- - , currentAlbumFavsSequence = Just ( identifiedTrack, ifThenElse identifiers.isFavourite 1 0 ) - , largestAlbumFavsSequence = Nothing - - -- - , currentArtistSequence = Just ( identifiedTrack, 1 ) - , largestArtistSequence = Nothing - } - , covers = - collection - } - - else - -- Same group - { gathering = - { acc = identifiedTrack :: gathering.acc - , accIds = track.id :: gathering.accIds - , previousGroup = group - , previousTrack = track - - -- Album sequence - ----------------- - , currentAlbumSequence = - if album /= gathering.previousTrack.tags.album then - Just ( identifiedTrack, 1 ) - - else - increaseSequence gathering.currentAlbumSequence - - -- - , largestAlbumSequence = - if album /= gathering.previousTrack.tags.album then - resolveLargestSequence - gathering.currentAlbumSequence - gathering.largestAlbumSequence - - else - gathering.largestAlbumSequence - - -- Album favourites sequence - ---------------------------- - , currentAlbumFavsSequence = - if album /= gathering.previousTrack.tags.album then - Just ( identifiedTrack, ifThenElse identifiers.isFavourite 1 0 ) - - else if identifiers.isFavourite then - increaseSequence gathering.currentAlbumFavsSequence - - else - gathering.currentAlbumFavsSequence - - -- - , largestAlbumFavsSequence = - if album /= gathering.previousTrack.tags.album then - resolveLargestSequence - gathering.currentAlbumFavsSequence - gathering.largestAlbumFavsSequence - - else - gathering.largestAlbumFavsSequence - - -- Artist sequence - ------------------ - , currentArtistSequence = - if artist /= gathering.previousTrack.tags.artist then - Just ( identifiedTrack, 1 ) - - else - increaseSequence gathering.currentArtistSequence - - -- - , largestArtistSequence = - if artist /= gathering.previousTrack.tags.artist then - resolveLargestSequence - gathering.currentArtistSequence - gathering.largestArtistSequence - - else - gathering.largestArtistSequence - } - , covers = - covers - } - ) - { covers = - [] - , gathering = - { acc = [] - , accIds = [] - , previousGroup = "" - , previousTrack = emptyTrack - - -- - , currentAlbumSequence = Nothing - , largestAlbumSequence = Nothing - , currentAlbumFavsSequence = Nothing - , largestAlbumFavsSequence = Nothing - , currentArtistSequence = Nothing - , largestArtistSequence = Nothing - } - } - |> (\{ covers, gathering } -> - makeCoverFn gathering covers - ) - |> (\collection -> - { arranged = collection, harvested = [] } - ) - - -harvest : - Maybe Cover - -> SortBy - -> Tracks.Collection - -> CoverCollection - -> ( CoverCollection, Maybe Cover ) -harvest previouslySelectedCover sortBy tracks covers = - let - groupFn = - coverGroup sortBy - - ( groups, tracksPerGroup ) = - List.foldr - (\identifiedTrack ( acc, dict ) -> - let - group = - groupFn identifiedTrack - in - ( if Dict.member group dict == False then - group :: acc - - else - acc - -- - , Dict.update group - (Maybe.unwrap [ identifiedTrack ] ((::) identifiedTrack) >> Just) - dict - ) - ) - ( [], Dict.empty ) - tracks.harvested - in - covers.arranged - |> List.foldr - (\cover ( acc, sel ) -> - if List.member cover.group groups then - let - groupTracks = - Maybe.withDefault [] (Dict.get cover.group tracksPerGroup) - - trackIds = - List.map (Tuple.second >> .id) groupTracks - - harvestedCover = - { cover | tracks = groupTracks, trackIds = trackIds } - in - case ( previouslySelectedCover, sel ) of - ( Just pre, Nothing ) -> - ( harvestedCover :: acc - , if pre.key == harvestedCover.key then - Just harvestedCover - - else - Nothing - ) - - ( Just _, Just s ) -> - ( harvestedCover :: acc - , Just s - ) - - ( Nothing, _ ) -> - ( harvestedCover :: acc - , Nothing - ) - - else - ( acc - , sel - ) - ) - ( [] - , Nothing - ) - |> Tuple.mapFirst - (\h -> { covers | harvested = h }) - - - --- ⚗️ - - -makeCover sortBy_ gathering collection = - let - closedGathering = - { gathering - | largestAlbumSequence = - resolveLargestSequence - gathering.currentAlbumSequence - gathering.largestAlbumSequence - - -- - , largestAlbumFavsSequence = - resolveLargestSequence - gathering.currentAlbumFavsSequence - gathering.largestAlbumFavsSequence - - -- - , largestArtistSequence = - resolveLargestSequence - gathering.currentArtistSequence - gathering.largestArtistSequence - } - in - case closedGathering.acc of - [] -> - collection - - fallback :: _ -> - makeCoverWithFallback sortBy_ closedGathering fallback :: collection - - -makeCoverWithFallback _ gathering fallback = - let - amountOfTracks = - List.length gathering.accIds - - group = - gathering.previousGroup - - identifiedTrack : IdentifiedTrack - identifiedTrack = - gathering.largestAlbumFavsSequence - |> Maybe.orElse gathering.largestAlbumSequence - |> Maybe.map Tuple.first - |> Maybe.withDefault fallback - - ( _, track ) = - identifiedTrack - - ( largestAlbumSequence, largestArtistSequence ) = - ( Maybe.unwrap 0 Tuple.second gathering.largestAlbumSequence - , Maybe.unwrap 0 Tuple.second gathering.largestArtistSequence - ) - - ( sameAlbum, sameArtist ) = - ( largestAlbumSequence == amountOfTracks - , largestArtistSequence == amountOfTracks - ) - - isVariousArtists = - False - || (amountOfTracks > 4 && largestArtistSequence < 3) - || (Maybe.map String.toLower track.tags.artist == Just "va") - in - { key = Base64.encode (coverKey isVariousArtists track) - , identifiedTrackCover = identifiedTrack - - -- - , group = group - , sameAlbum = sameAlbum - , sameArtist = sameArtist - - -- - , trackIds = gathering.accIds - , tracks = gathering.acc - , variousArtists = isVariousArtists - } - - - --- ⚗️ ░░ SEQUENCES - - -increaseSequence = - Maybe.map (Tuple.mapSecond ((+) 1)) - - -resolveLargestSequence curr state = - case ( curr, state ) of - ( Just ( _, c ), Just ( _, s ) ) -> - ifThenElse (c > s) curr state - - ( Just _, Nothing ) -> - curr - - ( Nothing, Just _ ) -> - state - - ( Nothing, Nothing ) -> - Nothing diff --git a/src/Core/UI/Tracks/State.elm b/src/Core/UI/Tracks/State.elm deleted file mode 100644 index 4b9a8863a..000000000 --- a/src/Core/UI/Tracks/State.elm +++ /dev/null @@ -1,1122 +0,0 @@ -module UI.Tracks.State exposing (..) - -import Alien -import Base64 -import Common exposing (..) -import ContextMenu -import Coordinates exposing (Coordinates) -import Debouncer.Basic as Debouncer -import Dict -import Html.Events.Extra.Mouse as Mouse -import InfiniteList -import Json.Decode as Json -import Json.Encode -import Keyboard -import List.Ext as List -import List.Extra as List -import Maybe.Extra as Maybe -import Notifications -import Playlists exposing (Playlist) -import Queue -import Return exposing (andThen, return) -import Return.Ext as Return -import Sources -import Task.Extra as Task -import Tracks exposing (..) -import Tracks.Collection as Collection -import Tracks.Encoding as Encoding -import Tracks.Favourites as Favourites -import UI.Common.State as Common exposing (showNotification) -import UI.DnD as DnD -import UI.Page -import UI.Ports as Ports -import UI.Queue.State as Queue -import UI.Theme -import UI.Tracks.ContextMenu as Tracks -import UI.Tracks.Covers as Covers -import UI.Tracks.Types as Tracks exposing (..) -import UI.Types exposing (Manager, Model, Msg(..)) -import UI.User.State.Export as User -import User.Layer exposing (HypaethralData) - - - --- 📣 - - -update : Tracks.Msg -> Manager -update msg = - case msg of - Download a b -> - download a b - - DownloadFinished -> - downloadFinished - - Harvest -> - harvest - - MarkAsSelected a b -> - markAsSelected a b - - ScrollToNowPlaying -> - scrollToNowPlaying - - SyncTags a -> - syncTags a - - ToggleCachedOnly -> - toggleCachedOnly - - ToggleCoverSelectionReducesPool -> - toggleCoverSelectionReducesPool - - ToggleFavouritesOnly -> - toggleFavouritesOnly - - ToggleHideDuplicates -> - toggleHideDuplicates - - ----------------------------------------- - -- Cache - ----------------------------------------- - ClearCache -> - clearCache - - RemoveFromCache a -> - removeFromCache a - - StoreInCache a -> - storeInCache a - - StoredInCache a b -> - storedInCache a b - - --------- - -- Covers - --------- - GotCachedCover a -> - gotCachedCover a - - InsertCoverCache a -> - insertCoverCache a - - ----------------------------------------- - -- Collection - ----------------------------------------- - Add a -> - add a - - AddFavourites a -> - addFavourites a - - Reload a -> - reload a - - RemoveByPaths a -> - removeByPaths a - - RemoveBySourceId a -> - removeBySourceId a - - RemoveFavourites a -> - removeFavourites a - - SortBy a -> - sortBy a - - ToggleFavourite a -> - toggleFavourite a - - ----------------------------------------- - -- Groups - ----------------------------------------- - DisableGrouping -> - disableGrouping - - GroupBy a -> - groupBy a - - ----------------------------------------- - -- Menus - ----------------------------------------- - ShowCoverMenu a b -> - showCoverMenu a b - - ShowCoverMenuWithSmallDelay a b -> - showCoverMenuWithDelay a b - - ShowTracksMenu a b c -> - showTracksMenu a b c - - ShowTracksMenuWithSmallDelay a b c -> - showTracksMenuWithDelay a b c - - ShowViewMenu a b -> - showViewMenu a b - - ----------------------------------------- - -- Scenes - ----------------------------------------- - ChangeScene a -> - changeScene a - - DeselectCover -> - deselectCover - - InfiniteListMsg a -> - infiniteListMsg a - - SelectCover a -> - selectCover a - - ----------------------------------------- - -- Search - ----------------------------------------- - ClearSearch -> - clearSearch - - Search -> - search - - SetSearchResults a -> - setSearchResults a - - SetSearchTerm a -> - setSearchTerm a - - - --- 🔱 - - -add : Json.Value -> Manager -add encodedTracks model = - reviseCollection - (encodedTracks - |> Json.decodeValue (Json.list Encoding.trackDecoder) - |> Result.withDefault [] - |> Collection.add - ) - model - - -addFavourites : List IdentifiedTrack -> Manager -addFavourites = - manageFavourites AddToFavourites - - -afterInitialLoad : Manager -afterInitialLoad model = - Common.toggleLoadingScreen Off model - - -changeScene : Scene -> Manager -changeScene scene model = - (case scene of - Covers -> - Ports.loadAlbumCovers { list = True, coverView = True } - - List -> - Cmd.none - ) - |> return { model | scene = scene, selectedCover = Nothing } - |> andThen - (if model.coverSelectionReducesPool then - Queue.reset - - else - Return.singleton - ) - |> andThen Common.forceTracksRerender - |> andThen User.saveEnclosedUserData - - -clearCache : Manager -clearCache model = - model.cachedTracks - |> Json.Encode.list Json.Encode.string - |> Alien.broadcast Alien.RemoveTracksFromCache - |> Ports.toBrain - |> return { model | cachedTracks = [] } - |> andThen harvest - |> andThen User.saveEnclosedUserData - |> andThen - ("Tracks cache was cleared" - |> Notifications.casual - |> Common.showNotification - ) - - -clearSearch : Manager -clearSearch model = - { model | searchResults = Nothing, searchTerm = Nothing } - |> reviseCollection Collection.harvest - |> andThen User.saveEnclosedUserData - - -deselectCover : Manager -deselectCover model = - (if model.coverSelectionReducesPool then - Queue.reset - - else - Return.singleton - ) - { model | selectedCover = Nothing } - - -download : { prefixTrackNumber : Bool, zipName : String } -> List Track -> Manager -download { prefixTrackNumber, zipName } tracks model = - let - notification = - Notifications.stickyCasual "Downloading tracks ..." - - downloading = - Just { notificationId = Notifications.id notification } - in - [ ( "prefixTrackNumber", Json.Encode.bool prefixTrackNumber ) - , ( "trackIds" - , tracks - |> List.map .id - |> Json.Encode.list Json.Encode.string - ) - , ( "zipName", Json.Encode.string zipName ) - ] - |> Json.Encode.object - |> Alien.broadcast Alien.DownloadTracks - |> Ports.toBrain - |> return { model | downloading = downloading } - |> andThen (Common.showNotification notification) - - -downloadFinished : Manager -downloadFinished model = - case model.downloading of - Just { notificationId } -> - Common.dismissNotification - { id = notificationId } - { model | downloading = Nothing } - - Nothing -> - Return.singleton model - - -disableGrouping : Manager -disableGrouping model = - { model | grouping = Nothing } - |> reviseCollection Collection.arrange - |> andThen User.saveEnclosedUserData - - -failedToStoreInCache : List String -> Manager -failedToStoreInCache trackIds m = - showNotification - (Notifications.error "Failed to store track in cache") - { m | cachingTracksInProgress = List.without trackIds m.cachingTracksInProgress } - - -finishedStoringInCache : List String -> Manager -finishedStoringInCache trackIds model = - { model - | cachedTracks = model.cachedTracks ++ trackIds - , cachingTracksInProgress = List.without trackIds model.cachingTracksInProgress - } - |> (\m -> - -- When a context menu of a track is open, - -- it should be "rerendered" in case - -- the track is no longer being downloaded. - case m.contextMenu of - Just contextMenu -> - let - isTrackContextMenu = - ContextMenu.anyItem - (.label >> (==) "Downloading ...") - contextMenu - - coordinates = - ContextMenu.coordinates contextMenu - in - if isTrackContextMenu then - showTracksMenu Nothing { alt = False } coordinates m - - else - Return.singleton m - - Nothing -> - Return.singleton m - ) - |> andThen harvest - |> andThen User.saveEnclosedUserData - - -generateCovers : Manager -generateCovers model = - model.tracks - |> Covers.generate model.sortBy - |> (\c -> { model | covers = c }) - |> Return.singleton - - -gotCachedCover : Json.Value -> Manager -gotCachedCover json model = - let - cachedCovers = - Maybe.withDefault Dict.empty model.cachedCovers - - decodedValue = - Json.decodeValue - (Json.map3 - (\i k u -> ( i, k, u )) - (Json.field "imageType" Json.string) - (Json.field "key" Json.string) - (Json.field "url" Json.string) - ) - json - in - decodedValue - |> Result.map (\( _, key, url ) -> Dict.insert key url cachedCovers) - |> Result.map (\dict -> { model | cachedCovers = Just dict }) - |> Result.withDefault model - |> (\m -> - case ( m.nowPlaying, decodedValue ) of - ( Just nowPlaying, Ok val ) -> - let - ( imageType, key, url ) = - val - - ( _, track ) = - nowPlaying.item.identifiedTrack - - hasntLoadedYet = - nowPlaying.coverLoaded == False - - ( keyA, keyB ) = - ( Base64.encode (Tracks.coverKey False track) - , Base64.encode (Tracks.coverKey True track) - ) - - keyMatches = - keyA == key || keyB == key - in - if hasntLoadedYet && keyMatches then - ( m, Ports.setMediaSessionArtwork { blobUrl = url, imageType = imageType } ) - - else - Return.singleton m - - _ -> - Return.singleton m - ) - - -groupBy : Tracks.Grouping -> Manager -groupBy grouping model = - { model | grouping = Just grouping } - |> reviseCollection Collection.arrange - |> andThen User.saveEnclosedUserData - - -harvest : Manager -harvest = - reviseCollection Collection.harvest - - -harvestCovers : Manager -harvestCovers model = - model.covers - |> Covers.harvest model.selectedCover model.sortBy model.tracks - |> (\( c, s ) -> { model | covers = c, selectedCover = s }) - |> Return.communicate (Ports.loadAlbumCovers { list = True, coverView = True }) - - -infiniteListMsg : InfiniteList.Model -> Manager -infiniteListMsg infiniteList model = - return - { model | infiniteList = infiniteList } - (Ports.loadAlbumCovers { list = True, coverView = False }) - - -insertCoverCache : Json.Value -> Manager -insertCoverCache json model = - json - |> Json.decodeValue (Json.dict Json.string) - |> Result.map (\dict -> { model | cachedCovers = Just dict }) - |> Result.withDefault model - |> Return.singleton - - -manageFavourites : FavouritesManagementAction -> List IdentifiedTrack -> Manager -manageFavourites action tracks model = - let - newFavourites = - (case action of - AddToFavourites -> - Favourites.completeFavouritesList - - RemoveFromFavourites -> - Favourites.removeFromFavouritesList - ) - tracks - model.favourites - - effect collection = - collection - |> Collection.map - (case action of - AddToFavourites -> - Favourites.completeTracksList tracks - - RemoveFromFavourites -> - Favourites.removeFromTracksList tracks - ) - |> (if model.favouritesOnly then - Collection.harvest - - else - identity - ) - - selectedCover = - Maybe.map - (\cover -> - cover.tracks - |> (case action of - AddToFavourites -> - Favourites.completeTracksList tracks - - RemoveFromFavourites -> - Favourites.removeFromTracksList tracks - ) - |> (\a -> { cover | tracks = a }) - ) - model.selectedCover - in - { model | favourites = newFavourites, selectedCover = selectedCover } - |> reviseCollection effect - |> andThen User.saveFavourites - |> (if model.scene == Covers then - andThen generateCovers >> andThen harvestCovers - - else - identity - ) - - -markAsSelected : Int -> { shiftKey : Bool } -> Manager -markAsSelected indexInList { shiftKey } model = - let - selection = - if shiftKey then - model.selectedTrackIndexes - |> List.head - |> Maybe.map - (\n -> - if n > indexInList then - List.range indexInList n - - else - List.range n indexInList - ) - |> Maybe.withDefault [ indexInList ] - - else - [ indexInList ] - in - Return.singleton { model | selectedTrackIndexes = selection } - - -reload : Json.Value -> Manager -reload encodedTracks model = - reviseCollection - (encodedTracks - |> Json.decodeValue (Json.list Encoding.trackDecoder) - |> Result.withDefault model.tracks.untouched - |> Collection.replace - ) - model - - -removeByPaths : Json.Value -> Manager -removeByPaths encodedParams model = - let - decoder = - Json.map2 - Tuple.pair - (Json.field "filePaths" <| Json.list Json.string) - (Json.field "sourceId" Json.string) - - ( paths, sourceId ) = - encodedParams - |> Json.decodeValue decoder - |> Result.withDefault ( [], missingId ) - - { kept, removed } = - Tracks.removeByPaths - { sourceId = sourceId, paths = paths } - model.tracks.untouched - - newCollection = - { emptyCollection | untouched = kept } - in - { model | tracks = newCollection } - |> reviseCollection Collection.identify - |> andThen (removeFromCache removed) - - -removeBySourceId : String -> Manager -removeBySourceId sourceId model = - let - { kept, removed } = - Tracks.removeBySourceId sourceId model.tracks.untouched - - newCollection = - { emptyCollection | untouched = kept } - in - sourceId - |> Json.Encode.string - |> Alien.broadcast Alien.RemoveTracksBySourceId - |> Ports.toBrain - |> return { model | tracks = newCollection } - |> andThen (reviseCollection Collection.identify) - |> andThen (removeFromCache removed) - - -removeFavourites : List IdentifiedTrack -> Manager -removeFavourites = - manageFavourites RemoveFromFavourites - - -removeFromCache : List Track -> Manager -removeFromCache tracks model = - let - trackIds = - List.map .id tracks - in - trackIds - |> Json.Encode.list Json.Encode.string - |> Alien.broadcast Alien.RemoveTracksFromCache - |> Ports.toBrain - |> return { model | cachedTracks = List.without trackIds model.cachedTracks } - |> andThen harvest - |> andThen User.saveEnclosedUserData - - -reviseCollection : (Parcel -> Parcel) -> Manager -reviseCollection collector model = - resolveParcel - (model - |> makeParcel - |> collector - ) - model - - -search : Manager -search model = - case ( model.searchTerm, model.searchResults ) of - ( Just term, _ ) -> - term - |> String.trim - |> Json.Encode.string - |> Ports.giveBrain Alien.SearchTracks - |> return model - - ( Nothing, Just _ ) -> - reviseCollection Collection.harvest { model | searchResults = Nothing } - - ( Nothing, Nothing ) -> - Return.singleton model - - -selectCover : Cover -> Manager -selectCover cover model = - { model | selectedCover = Just cover } - |> (if model.coverSelectionReducesPool then - Queue.reset - - else - Return.singleton - ) - |> Return.command (Ports.loadAlbumCovers { list = False, coverView = True }) - - -setSearchResults : Json.Value -> Manager -setSearchResults json model = - case model.searchTerm of - Just _ -> - json - |> Json.decodeValue (Json.list Json.string) - |> Result.withDefault [] - |> (\results -> { model | searchResults = Just results }) - |> reviseCollection Collection.harvest - |> andThen afterInitialLoad - - Nothing -> - Return.singleton model - - -setSearchTerm : String -> Manager -setSearchTerm term model = - (case String.trim term of - "" -> - { model | searchTerm = Nothing } - - _ -> - { model | searchTerm = Just term } - ) - |> Return.communicate - (Search - |> TracksMsg - |> Debouncer.provideInput - |> SearchDebounce - |> Task.do - ) - |> Return.andThen User.saveEnclosedUserData - - -showCoverMenu : Cover -> Coordinates -> Manager -showCoverMenu cover coordinates model = - let - menuDependencies = - { cached = model.cachedTracks - , cachingInProgress = model.cachingTracksInProgress - , currentTime = model.currentTime - , selectedPlaylist = model.selectedPlaylist - , lastModifiedPlaylistName = model.lastModifiedPlaylist - , showAlternativeMenu = False - , sources = model.sources - } - in - coordinates - |> Tracks.trackMenu menuDependencies cover.tracks - |> Common.showContextMenuWithModel model - - -showCoverMenuWithDelay : Cover -> Coordinates -> Manager -showCoverMenuWithDelay a b model = - Tracks.ShowCoverMenu a b - |> TracksMsg - |> Task.doDelayed 250 - |> return model - - -showTracksMenu : Maybe Int -> { alt : Bool } -> Coordinates -> Manager -showTracksMenu maybeTrackIndex { alt } coordinates model = - let - selection = - case maybeTrackIndex of - Just trackIndex -> - if List.isEmpty model.selectedTrackIndexes then - [ trackIndex ] - - else if List.member trackIndex model.selectedTrackIndexes == False then - [ trackIndex ] - - else - model.selectedTrackIndexes - - Nothing -> - model.selectedTrackIndexes - - menuDependencies = - { cached = model.cachedTracks - , cachingInProgress = model.cachingTracksInProgress - , currentTime = model.currentTime - , selectedPlaylist = model.selectedPlaylist - , lastModifiedPlaylistName = model.lastModifiedPlaylist - , showAlternativeMenu = alt - , sources = model.sources - } - - tracks = - List.pickIndexes selection model.tracks.harvested - in - coordinates - |> Tracks.trackMenu menuDependencies tracks - |> Common.showContextMenuWithModel - { model - | dnd = DnD.initialModel - , selectedTrackIndexes = selection - } - - -showTracksMenuWithDelay : Maybe Int -> { alt : Bool } -> Coordinates -> Manager -showTracksMenuWithDelay a b c model = - Tracks.ShowTracksMenu a b c - |> TracksMsg - |> Task.doDelayed 250 - |> return model - - -showViewMenu : Maybe Grouping -> Mouse.Event -> Manager -showViewMenu maybeGrouping mouseEvent model = - mouseEvent.clientPos - |> Coordinates.fromTuple - |> Tracks.viewMenu model.cachedTracksOnly maybeGrouping - |> Common.showContextMenuWithModel model - - -scrollToNowPlaying : Manager -scrollToNowPlaying model = - model.nowPlaying - |> Maybe.map - (.item >> .identifiedTrack >> Tuple.second >> .id) - |> Maybe.andThen - (\id -> - List.find - (Tuple.second >> .id >> (==) id) - model.tracks.harvested - ) - |> Maybe.map - (\( identifiers, track ) -> - case model.scene of - Covers -> - if List.member Keyboard.Shift model.pressedKeys then - return - { model | selectedCover = Nothing } - (UI.Theme.scrollToNowPlaying Covers ( identifiers, track ) model) - - else - model.covers.harvested - |> List.find (\cover -> List.member track.id cover.trackIds) - |> Maybe.unwrap model (\cover -> { model | selectedCover = Just cover }) - |> Return.communicate (Ports.loadAlbumCovers { list = True, coverView = True }) - - List -> - return - { model | selectedCover = Nothing } - (UI.Theme.scrollToNowPlaying List ( identifiers, track ) model) - ) - |> Maybe.map - (UI.Page.Index - |> Common.changeUrlUsingPage - |> andThen - ) - |> Maybe.withDefault - (Return.singleton model) - - -sortBy : SortBy -> Manager -sortBy property model = - let - sortDir = - if model.sortBy /= property then - Asc - - else if model.sortDirection == Asc then - Desc - - else - Asc - in - { model | sortBy = property, sortDirection = sortDir } - |> reviseCollection Collection.arrange - |> andThen User.saveEnclosedUserData - - -storeInCache : List Track -> Manager -storeInCache tracks model = - let - trackIds = - List.map .id tracks - - notification = - case tracks of - [ t ] -> - ("__" ++ t.tags.title ++ "__ will be stored in the cache") - |> Notifications.casual - - list -> - list - |> List.length - |> String.fromInt - |> (\s -> "__" ++ s ++ " tracks__ will be stored in the cache") - |> Notifications.casual - in - tracks - |> Json.Encode.list - (\track -> - Json.Encode.object - [ ( "trackId" - , Json.Encode.string track.id - ) - , ( "url" - , track - |> Queue.makeTrackUrl - model.currentTime - model.sources - |> Json.Encode.string - ) - ] - ) - |> Alien.broadcast Alien.StoreTracksInCache - |> Ports.toBrain - |> return { model | cachingTracksInProgress = model.cachingTracksInProgress ++ trackIds } - |> andThen (Common.showNotification notification) - - -storedInCache : Json.Value -> Maybe String -> Manager -storedInCache json maybeError = - case - ( maybeError - , Json.decodeValue (Json.list Json.string) json - ) - of - ( Nothing, Ok list ) -> - finishedStoringInCache list - - ( Nothing, Err err ) -> - err - |> Json.errorToString - |> Notifications.error - |> Common.showNotification - - ( Just _, Ok trackIds ) -> - failedToStoreInCache trackIds - - ( Just err, Err _ ) -> - err - |> Notifications.error - |> Common.showNotification - - -syncTags : List Track -> Manager -syncTags tracks = - tracks - |> Json.Encode.list - (\track -> - Json.Encode.object - [ ( "path", Json.Encode.string track.path ) - , ( "sourceId", Json.Encode.string track.sourceId ) - , ( "trackId", Json.Encode.string track.id ) - ] - ) - |> Alien.broadcast Alien.SyncTrackTags - |> Ports.toBrain - |> Return.communicate - - -toggleCachedOnly : Manager -toggleCachedOnly model = - { model | cachedTracksOnly = not model.cachedTracksOnly } - |> reviseCollection Collection.harvest - |> andThen User.saveEnclosedUserData - |> andThen Common.forceTracksRerender - - -toggleCoverSelectionReducesPool : Manager -toggleCoverSelectionReducesPool model = - { model | coverSelectionReducesPool = not model.coverSelectionReducesPool } - |> Queue.reset - |> andThen User.saveSettings - - -toggleFavourite : Int -> Manager -toggleFavourite index model = - case List.getAt index model.tracks.harvested of - Just ( i, t ) -> - let - newFavourites = - Favourites.toggleInFavouritesList ( i, t ) model.favourites - - effect collection = - collection - |> Collection.map (Favourites.toggleInTracksList t) - |> (if model.favouritesOnly then - Collection.harvest - - else - identity - ) - - selectedCover = - Maybe.map - (\cover -> - cover.tracks - |> Favourites.toggleInTracksList t - |> (\a -> { cover | tracks = a }) - ) - model.selectedCover - in - { model | favourites = newFavourites, selectedCover = selectedCover } - |> reviseCollection effect - |> andThen User.saveFavourites - |> (if model.scene == Covers then - andThen generateCovers >> andThen harvestCovers - - else - identity - ) - - Nothing -> - Return.singleton model - - -toggleFavouritesOnly : Manager -toggleFavouritesOnly model = - { model | favouritesOnly = not model.favouritesOnly } - |> reviseCollection Collection.harvest - |> andThen User.saveEnclosedUserData - - -toggleHideDuplicates : Manager -toggleHideDuplicates model = - { model | hideDuplicates = not model.hideDuplicates } - |> reviseCollection Collection.arrange - |> andThen User.saveSettings - - - --- 📣 ░░ PARCEL - - -makeParcel : Model -> Parcel -makeParcel model = - ( { cached = model.cachedTracks - , cachedOnly = model.cachedTracksOnly - , enabledSourceIds = Sources.enabledSourceIds model.sources - , favourites = model.favourites - , favouritesOnly = model.favouritesOnly - , grouping = model.grouping - , hideDuplicates = model.hideDuplicates - , searchResults = model.searchResults - , selectedPlaylist = model.selectedPlaylist - , sortBy = model.sortBy - , sortDirection = model.sortDirection - } - , model.tracks - ) - - -resolveParcel : Parcel -> Manager -resolveParcel ( deps, newCollection ) model = - let - scrollObj = - Json.Encode.object - [ ( "scrollTop", Json.Encode.int 0 ) ] - - scrollEvent = - Json.Encode.object - [ ( "target", scrollObj ) ] - - newScrollContext = - scrollContext model - - collectionChanged = - Collection.tracksChanged - model.tracks.untouched - newCollection.untouched - - arrangementChanged = - if collectionChanged then - True - - else - Collection.identifiedTracksChanged - model.tracks.arranged - newCollection.arranged - - harvestChanged = - if arrangementChanged then - True - - else - Collection.identifiedTracksChanged - model.tracks.harvested - newCollection.harvested - - scrollContextChanged = - newScrollContext /= model.tracks.scrollContext - - modelWithNewCollection = - (if model.scene == List && scrollContextChanged then - \m -> { m | infiniteList = InfiniteList.updateScroll scrollEvent m.infiniteList } - - else - identity - ) - { model - | tracks = - { newCollection | scrollContext = newScrollContext } - , selectedTrackIndexes = - if collectionChanged || harvestChanged then - [] - - else - model.selectedTrackIndexes - } - in - (if collectionChanged then - whenCollectionChanges - - else if arrangementChanged then - whenArrangementChanges - - else if harvestChanged then - whenHarvestChanges - - else - identity - ) - ( modelWithNewCollection - ----------------------------------------- - -- Command - ----------------------------------------- - , if scrollContextChanged then - UI.Theme.scrollTracksToTop model.scene - - else - Cmd.none - ) - - -whenHarvestChanges = - andThen harvestCovers >> andThen Queue.reset - - -whenArrangementChanges = - andThen generateCovers >> whenHarvestChanges - - -whenCollectionChanges = - andThen search >> andThen Common.generateDirectoryPlaylists >> whenArrangementChanges - - -scrollContext : Model -> String -scrollContext model = - String.concat - [ Maybe.withDefault "" <| model.searchTerm - , Maybe.withDefault "" <| Maybe.map .name model.selectedPlaylist - ] - - - --- 📣 ░░ USER DATA - - -importHypaethral : HypaethralData -> Maybe Playlist -> Manager -importHypaethral data selectedPlaylist model = - { model - | favourites = data.favourites - , selectedPlaylist = selectedPlaylist - , tracks = { emptyCollection | untouched = data.tracks } - } - |> reviseCollection Collection.identify - |> andThen search - |> (case model.searchTerm of - Just _ -> - identity - - Nothing -> - andThen afterInitialLoad - ) - - - --- ㊙️ - - -type FavouritesManagementAction - = AddToFavourites - | RemoveFromFavourites diff --git a/src/Core/UI/Tracks/Types.elm b/src/Core/UI/Tracks/Types.elm deleted file mode 100644 index eecaf6e86..000000000 --- a/src/Core/UI/Tracks/Types.elm +++ /dev/null @@ -1,74 +0,0 @@ -module UI.Tracks.Types exposing (..) - -import Coordinates exposing (Coordinates) -import Html.Events.Extra.Mouse as Mouse -import InfiniteList -import Json.Decode as Json -import Tracks exposing (..) - - - --- 📣 - - -type Msg - = Download { prefixTrackNumber : Bool, zipName : String } (List Track) - | DownloadFinished - | Harvest - | MarkAsSelected Int { shiftKey : Bool } - | ScrollToNowPlaying - | SyncTags (List Track) - | ToggleCachedOnly - | ToggleFavouritesOnly - | ToggleHideDuplicates - ----------------------------------------- - -- Cache - ----------------------------------------- - | ClearCache - | RemoveFromCache (List Track) - | StoreInCache (List Track) - | StoredInCache Json.Value (Maybe String) - --------- - -- Covers - --------- - | GotCachedCover Json.Value - | InsertCoverCache Json.Value - ----------------------------------------- - -- Collection - ----------------------------------------- - | Add Json.Value - | AddFavourites (List IdentifiedTrack) - | Reload Json.Value - | RemoveByPaths Json.Value - | RemoveBySourceId String - | RemoveFavourites (List IdentifiedTrack) - | SortBy SortBy - | ToggleFavourite Int - | ToggleCoverSelectionReducesPool - ----------------------------------------- - -- Groups - ----------------------------------------- - | DisableGrouping - | GroupBy Grouping - ----------------------------------------- - -- Menus - ----------------------------------------- - | ShowCoverMenu Cover Coordinates - | ShowCoverMenuWithSmallDelay Cover Coordinates - | ShowTracksMenu (Maybe Int) { alt : Bool } Coordinates - | ShowTracksMenuWithSmallDelay (Maybe Int) { alt : Bool } Coordinates - | ShowViewMenu (Maybe Grouping) Mouse.Event - ----------------------------------------- - -- Scenes - ----------------------------------------- - | ChangeScene Scene - | DeselectCover - | InfiniteListMsg InfiniteList.Model - | SelectCover Cover - ----------------------------------------- - -- Search - ----------------------------------------- - | ClearSearch - | Search - | SetSearchResults Json.Value - | SetSearchTerm String diff --git a/src/Core/UI/Types.elm b/src/Core/UI/Types.elm deleted file mode 100644 index a48bfe630..000000000 --- a/src/Core/UI/Types.elm +++ /dev/null @@ -1,332 +0,0 @@ -module UI.Types exposing (..) - -import Alfred exposing (Alfred) -import Alien -import Browser -import Browser.Navigation as Nav -import Color exposing (Color) -import Common exposing (ServiceWorkerStatus, Switch) -import ContextMenu exposing (ContextMenu) -import Coordinates exposing (Viewport) -import Debouncer.Basic as Debouncer exposing (Debouncer) -import Dict exposing (Dict) -import Equalizer -import File exposing (File) -import Html.Events.Extra.Mouse as Mouse -import Http -import InfiniteList -import Json.Decode -import Keyboard -import LastFm -import Management -import Notifications exposing (Notification) -import Playlists exposing (Playlist, PlaylistTrackWithoutMetadata) -import Queue -import Random -import Sources exposing (Source) -import Theme -import Time -import Tracks exposing (..) -import UI.Audio.Types exposing (DurationChangeEvent, ErrorAudioEvent, GenericAudioEvent, NowPlaying, PlaybackStateEvent, TimeUpdatedEvent) -import UI.DnD as DnD -import UI.Page exposing (Page) -import UI.Queue.Types as Queue -import UI.Sources.Types as Sources -import UI.Syncing.Types as Syncing -import UI.Tracks.Types as Tracks -import Url exposing (Url) - - - --- ⛩ - - -type alias Flags = - { buildTimestamp : Int - , darkMode : Bool - , initialTime : Int - , isInstallingServiceWorker : Bool -- ie. Installing SW for the first time - , isOnline : Bool - , isTauri : Bool - , version : String - , viewport : Viewport - } - - - --- 🌳 - - -type alias Model = - { buildTimestamp : Int - , confirmation : Maybe String - , currentTime : Time.Posix - , currentTimeZone : Time.Zone - , darkMode : Bool - , downloading : Maybe { notificationId : Int } - , dnd : DnD.Model Int - , focusedOnInput : Bool - , isDragging : Bool - , isLoading : Bool - , isOnline : Bool - , isTauri : Bool - , isTouchDevice : Bool - , lastFm : LastFm.Model - , navKey : Nav.Key - , page : Page - , pressedKeys : List Keyboard.Key - , processAutomatically : Bool - , serviceWorkerStatus : ServiceWorkerStatus - , theme : Maybe Theme.Id - , uuidSeed : Random.Seed - , url : Url - , version : String - , viewport : Viewport - - ----------------------------------------- - -- Audio - ----------------------------------------- - , audioElements : List Queue.EngineItem - , nowPlaying : Maybe NowPlaying - , progress : Dict String Float - , rememberProgress : Bool - - ----------------------------------------- - -- Backdrop - ----------------------------------------- - , chosenBackdrop : Maybe String - , extractedBackdropColor : Maybe Color - , fadeInBackdrop : Bool - , loadedBackdrops : List String - - ----------------------------------------- - -- Debouncing - ----------------------------------------- - , preloadDebouncer : Debouncer Msg Msg - , progressDebouncer : Debouncer Msg Msg - , resizeDebouncer : Debouncer Msg Msg - , searchDebouncer : Debouncer Msg Msg - - ----------------------------------------- - -- Equalizer - ----------------------------------------- - , eqSettings : Equalizer.Settings - , showVolumeSlider : Bool - - ----------------------------------------- - -- Instances - ----------------------------------------- - , alfred : Maybe (Alfred Msg) - , contextMenu : Maybe (ContextMenu Msg) - , notifications : List (Notification Msg) - - ----------------------------------------- - -- Playlists - ----------------------------------------- - , editPlaylistContext : Maybe { oldName : String, newName : String } - , lastModifiedPlaylist : Maybe { collection : Bool, name : String } - , newPlaylistContext : Maybe String - , playlists : List Playlist - , playlistToActivate : Maybe String - , selectedPlaylist : Maybe Playlist - - ----------------------------------------- - -- Queue - ----------------------------------------- - , dontPlay : List Queue.Item - , playedPreviously : List Queue.Item - , playingNext : List Queue.Item - , selectedQueueItem : Maybe Queue.Item - - -- Settings - ----------- - , repeat : Bool - , shuffle : Bool - - ----------------------------------------- - -- Sources - ----------------------------------------- - , processingContext : List ( String, Float ) - , processingError : Maybe { error : String, sourceId : String } - , processingNotificationId : Maybe Int - , sourceForm : Sources.Form - , sources : List Source - - ----------------------------------------- - -- Tracks - ----------------------------------------- - , cachedCovers : Maybe (Dict String String) - , cachedTracks : List String - , cachedTracksOnly : Bool - , cachingTracksInProgress : List String - , covers : { arranged : List Tracks.Cover, harvested : List Tracks.Cover } - , coverSelectionReducesPool : Bool - , favourites : List Favourite - , favouritesOnly : Bool - , grouping : Maybe Grouping - , hideDuplicates : Bool - , scene : Scene - , searchResults : Maybe (List String) - , searchTerm : Maybe String - , selectedCover : Maybe Cover - , selectedTrackIndexes : List Int - , sortBy : SortBy - , sortDirection : SortDirection - , tracks : Tracks.Collection - - -- List scene - ------------- - , infiniteList : InfiniteList.Model - - ----------------------------------------- - -- 🦉 Nested - ----------------------------------------- - , syncing : Syncing.State - } - - - --- 📣 - - -type Msg - = Bypass - ----------------------------------------- - -- Alfred - ----------------------------------------- - | AssignAlfred (Alfred Msg) - | GotAlfredInput String - | SelectAlfredItem Int - ----------------------------------------- - -- Audio - ----------------------------------------- - | AudioDurationChange DurationChangeEvent - | AudioError ErrorAudioEvent - | AudioEnded GenericAudioEvent - | AudioHasLoaded GenericAudioEvent - | AudioIsLoading GenericAudioEvent - | AudioPlaybackStateChanged PlaybackStateEvent - | AudioPreloadDebounce (Debouncer.Msg Msg) - | AudioTimeUpdated TimeUpdatedEvent - | NoteProgress { trackId : String, progress : Float } - | NoteProgressDebounce (Debouncer.Msg Msg) - | Pause - | Play - | Seek { trackId : String, progress : Float } - | Stop - | TogglePlay - | ToggleRememberProgress - ----------------------------------------- - -- Backdrop - ----------------------------------------- - | ExtractedBackdropColor { r : Int, g : Int, b : Int } - | ChooseBackdrop String - | LoadBackdrop String - ----------------------------------------- - -- Equalizer - ----------------------------------------- - | AdjustVolume Float - | ToggleVolumeSlider Switch - ----------------------------------------- - -- Interface - ----------------------------------------- - | AssistWithChangingTheme - | Blur - | ChangeTheme Theme.Id - | ContextMenuConfirmation String Msg - | CopyToClipboard String - | DismissNotification { id : Int } - | DnD (DnD.Msg Int) - | FocusedOnInput - | HideOverlay - | LostWindowFocus - | MsgViaContextMenu Msg - | PreferredColorSchemaChanged { dark : Bool } - | RemoveNotification { id : Int } - | RemoveQueueSelection - | RemoveTrackSelection - | ResizeDebounce (Debouncer.Msg Msg) - | ResizedWindow ( Int, Int ) - | SearchDebounce (Debouncer.Msg Msg) - | ShowNotification (Notification Msg) - | SetIsTouchDevice Bool - | StoppedDragging - | ToggleLoadingScreen Switch - ----------------------------------------- - -- Playlists - ----------------------------------------- - | ActivatePlaylist Playlist - | AddTracksToPlaylist { collection : Bool, playlistName : String, tracks : List PlaylistTrackWithoutMetadata } - | AssistWithAddingTracksToCollection (List IdentifiedTrack) - | AssistWithAddingTracksToPlaylist (List IdentifiedTrack) - | AssistWithSelectingPlaylist - | ConvertCollectionToPlaylist { name : String } - | ConvertPlaylistToCollection { name : String } - | CreateCollection - | CreatePlaylist - | DeactivatePlaylist - | DeletePlaylist { playlistName : String } - | DeselectPlaylist - | ModifyPlaylist - | MoveTrackInSelectedPlaylist { to : Int } - | RemoveTracksFromPlaylist Playlist (List IdentifiedTrack) - | SelectPlaylist Playlist - | SetPlaylistCreationContext String - | SetPlaylistModificationContext String String - | ShowPlaylistListMenu Playlist Mouse.Event - | TogglePlaylistVisibility Playlist - ----------------------------------------- - -- Routing - ----------------------------------------- - | ChangeUrlUsingPage Page - | LinkClicked Browser.UrlRequest - | OpenUrlOnNewPage String - | PageChanged Page - | UrlChanged Url - ----------------------------------------- - -- Services - ----------------------------------------- - | ConnectLastFm - | DisconnectLastFm - | GotLastFmSession (Result Http.Error String) - | Scrobble { duration : Int, timestamp : Int, trackId : String } - ----------------------------------------- - -- User - ----------------------------------------- - | Export - | ImportFile File - | ImportJson String - | InsertDemo - | LoadEnclosedUserData Json.Decode.Value - | LoadHypaethralUserData Json.Decode.Value - | RequestImport - | SaveEnclosedUserData - ----------------------------------------- - -- ⚗️ Adjunct - ----------------------------------------- - | KeyboardMsg Keyboard.Msg - ----------------------------------------- - -- 🦉 Nested - ----------------------------------------- - | SyncingMsg Syncing.Msg - | QueueMsg Queue.Msg - | SourcesMsg Sources.Msg - | TracksMsg Tracks.Msg - ----------------------------------------- - -- 📭 Other - ----------------------------------------- - | InstalledServiceWorker - | InstallingServiceWorker - | RedirectToBrain Alien.Event - | ReloadApp - | SetCurrentTime Time.Posix - | SetCurrentTimeZone Time.Zone - | SetIsOnline Bool - - -type alias Organizer model = - Management.Manager Msg model - - -type alias Manager = - Organizer Model diff --git a/src/Core/UI/User/State/Export.elm b/src/Core/UI/User/State/Export.elm deleted file mode 100644 index 5154319d5..000000000 --- a/src/Core/UI/User/State/Export.elm +++ /dev/null @@ -1,136 +0,0 @@ -module UI.User.State.Export exposing (..) - -import Alien -import File.Download -import Json.Encode -import List.Extra as List -import Maybe.Extra as Maybe -import Playlists.Encoding as Playlists -import Return exposing (return) -import Settings exposing (Settings) -import Sources.Encoding as Sources -import Tracks.Encoding as Tracks -import UI.Ports as Ports -import UI.Types exposing (..) -import User.Layer exposing (..) - - - --- 🔱 - - -export model = - { favourites = model.favourites - , playlists = List.filterNot (.autoGenerated >> Maybe.isJust) model.playlists - , progress = model.progress - , settings = Just (gatherSettings model) - , sources = model.sources - , tracks = model.tracks.untouched - - -- - , modifiedAt = Just model.currentTime - } - |> encodeHypaethralData - |> Json.Encode.encode 2 - |> (if model.isTauri then - \json -> Ports.downloadJsonUsingTauri { filename = "diffuse.json", json = json } - - else - File.Download.string "diffuse.json" "application/json" - ) - |> return model - - -gatherSettings : Model -> Settings -gatherSettings { chosenBackdrop, coverSelectionReducesPool, hideDuplicates, lastFm, processAutomatically, rememberProgress } = - { backgroundImage = chosenBackdrop - , coverSelectionReducesPool = coverSelectionReducesPool - , hideDuplicates = hideDuplicates - , lastFm = lastFm.sessionKey - , processAutomatically = processAutomatically - , rememberProgress = rememberProgress - } - - -saveEnclosedUserData : Manager -saveEnclosedUserData model = - { cachedTracks = model.cachedTracks - , equalizerSettings = model.eqSettings - , grouping = model.grouping - , onlyShowCachedTracks = model.cachedTracksOnly - , onlyShowFavourites = model.favouritesOnly - , repeat = model.repeat - , scene = model.scene - , searchTerm = model.searchTerm - , selectedPlaylist = Maybe.map .name model.selectedPlaylist - , shuffle = model.shuffle - , sortBy = model.sortBy - , sortDirection = model.sortDirection - , theme = model.theme - } - |> encodeEnclosedData - |> Alien.broadcast Alien.SaveEnclosedUserData - |> Ports.toBrain - |> Return.return model - - -saveFavourites : Manager -saveFavourites model = - model.favourites - |> Json.Encode.list Tracks.encodeFavourite - |> Alien.broadcast Alien.SaveFavourites - |> Ports.toBrain - |> return model - - -saveModifiedAt : Manager -saveModifiedAt = - -- Handled by 🧠 - Return.singleton - - -savePlaylists : Manager -savePlaylists model = - model.playlists - |> List.filterNot (.autoGenerated >> Maybe.isJust) - |> Json.Encode.list Playlists.encode - |> Alien.broadcast Alien.SavePlaylists - |> Ports.toBrain - |> return model - - -saveProgress : Manager -saveProgress model = - model.progress - |> Json.Encode.dict identity Json.Encode.float - |> Alien.broadcast Alien.SaveProgress - |> Ports.toBrain - |> return model - - -saveSettings : Manager -saveSettings model = - model - |> gatherSettings - |> Settings.encode - |> Alien.broadcast Alien.SaveSettings - |> Ports.toBrain - |> return model - - -saveSources : Manager -saveSources model = - model.sources - |> Json.Encode.list Sources.encode - |> Alien.broadcast Alien.SaveSources - |> Ports.toBrain - |> Return.return model - - -saveTracks : Manager -saveTracks model = - model.tracks.untouched - |> Json.Encode.list Tracks.encodeTrack - |> Alien.broadcast Alien.SaveTracks - |> Ports.toBrain - |> return model diff --git a/src/Core/UI/User/State/Import.elm b/src/Core/UI/User/State/Import.elm deleted file mode 100644 index 5905d71a8..000000000 --- a/src/Core/UI/User/State/Import.elm +++ /dev/null @@ -1,232 +0,0 @@ -module UI.User.State.Import exposing (..) - -import File exposing (File) -import File.Select -import Json.Decode -import Json.Encode -import LastFm -import List.Extra as List -import Maybe.Extra as Maybe -import Notifications -import Process -import Return exposing (andThen, return) -import Return.Ext exposing (communicate) -import Task -import UI.Backdrop as Backdrop -import UI.Common.State as Common exposing (showNotification) -import UI.Demo as Demo -import UI.Equalizer.State as Equalizer -import UI.Page as Page -import UI.Playlists.Directory -import UI.Sources.State as Sources -import UI.Tracks.State as Tracks -import UI.Types as UI exposing (..) -import UI.User.State.Export as User -import Url.Ext as Url -import User.Layer exposing (..) - - - --- 🔱 - - -importFile : File -> Manager -importFile file model = - 250 - |> Process.sleep - |> Task.andThen (\_ -> File.toString file) - |> Task.perform UI.ImportJson - |> return { model | isLoading = True } - - -importJson : String -> Manager -importJson json model = - json - -- Load data on main thread (this app) - |> Json.Decode.decodeString Json.Decode.value - |> Result.withDefault Json.Encode.null - |> (\j -> importHypaethral j model) - -- Show notification - |> andThen - ("Imported data successfully!" - |> Notifications.success - |> showNotification - ) - -- Clear tracks cache - |> andThen Tracks.clearCache - -- Redirect to index page - |> andThen Common.forceTracksRerender - |> andThen (Common.changeUrlUsingPage Page.Index) - ----------------------------- - -- Save all the imported data - ----------------------------- - |> saveAllHypaethralData - - -insertDemo : Manager -insertDemo model = - model - |> loadHypaethralUserData (Demo.tape model.currentTime) - |> saveAllHypaethralData - - -loadEnclosedUserData : Json.Decode.Value -> Manager -loadEnclosedUserData = - importEnclosed - - -loadHypaethralUserData : Json.Decode.Value -> Manager -loadHypaethralUserData json model = - model - |> importHypaethral json - |> andThen - (\m -> - case Url.action m.url of - [ "authenticate", "lastfm" ] -> - { authenticating = True - , sessionKey = Nothing - } - |> (\n -> { m | lastFm = n }) - |> communicate (LastFm.authenticationCommand GotLastFmSession m.url) - - _ -> - Return.singleton m - ) - |> andThen - Sources.addSourcesFromUrl - - -requestImport : Manager -requestImport model = - ImportFile - |> File.Select.file [ "application/json" ] - |> return model - - - --- ⚗️ ░░ HYPAETHRAL DATA - - -importHypaethral : Json.Decode.Value -> Manager -importHypaethral value model = - case decodeHypaethralData value of - Ok data -> - let - chosenBackdrop = - data.settings - |> Maybe.andThen .backgroundImage - |> Maybe.withDefault Backdrop.default - |> Just - - newPlaylistsCollection = - List.append - data.playlists - (UI.Playlists.Directory.generate data.sources data.tracks) - - selectedPlaylist = - Maybe.andThen - (\n -> List.find (.name >> (==) n) newPlaylistsCollection) - model.playlistToActivate - - lastFmModel = - model.lastFm - in - Tracks.importHypaethral - data - selectedPlaylist - { model - | chosenBackdrop = chosenBackdrop - , coverSelectionReducesPool = Maybe.unwrap True .coverSelectionReducesPool data.settings - , hideDuplicates = Maybe.unwrap False .hideDuplicates data.settings - , lastFm = { lastFmModel | sessionKey = Maybe.andThen .lastFm data.settings } - , playlists = newPlaylistsCollection - , playlistToActivate = Nothing - , processAutomatically = Maybe.unwrap True .processAutomatically data.settings - , progress = data.progress - , rememberProgress = Maybe.unwrap True .rememberProgress data.settings - , sources = data.sources - } - - Err err -> - err - |> Json.Decode.errorToString - |> Notifications.error - |> Common.showNotificationWithModel model - - -saveAllHypaethralData : ( Model, Cmd Msg ) -> ( Model, Cmd Msg ) -saveAllHypaethralData return = - List.foldl - (\( _, bit ) -> - case bit of - Favourites -> - andThen User.saveFavourites - - ModifiedAt -> - andThen User.saveModifiedAt - - Playlists -> - andThen User.savePlaylists - - Progress -> - andThen User.saveProgress - - Settings -> - andThen User.saveSettings - - Sources -> - andThen User.saveSources - - Tracks -> - andThen User.saveTracks - ) - return - hypaethralBit.list - - - --- ⚗️ ░░ ENCLOSED DATA - - -importEnclosed : Json.Decode.Value -> Manager -importEnclosed value model = - let - equalizerSettings = - model.eqSettings - in - case decodeEnclosedData value of - Ok data -> - let - newEqualizerSettings = - { equalizerSettings - | low = data.equalizerSettings.low - , mid = data.equalizerSettings.mid - , high = data.equalizerSettings.high - , volume = data.equalizerSettings.volume - } - in - ( { model - | eqSettings = newEqualizerSettings - , playlistToActivate = data.selectedPlaylist - , repeat = data.repeat - , shuffle = data.shuffle - - -- Tracks - , cachedTracks = data.cachedTracks - , cachedTracksOnly = data.onlyShowCachedTracks - , favouritesOnly = data.onlyShowFavourites - , grouping = data.grouping - , scene = data.scene - , searchTerm = data.searchTerm - , sortBy = data.sortBy - , sortDirection = data.sortDirection - , theme = data.theme - } - -- - , Equalizer.adjustAllKnobs newEqualizerSettings - ) - - Err err -> - ("Failed to decode enclosed data: " ++ Json.Decode.errorToString err) - |> Notifications.error - |> Common.showNotificationWithModel model diff --git a/src/Core/UI/View.elm b/src/Core/UI/View.elm deleted file mode 100644 index 1f3cefd3c..000000000 --- a/src/Core/UI/View.elm +++ /dev/null @@ -1,16 +0,0 @@ -module UI.View exposing (view) - -import Browser -import UI.Theme -import UI.Types exposing (Model, Msg) - - - --- 🗺 - - -view : Model -> Browser.Document Msg -view model = - { title = "Diffuse" - , body = [ UI.Theme.view model ] - } diff --git a/src/Css/About.css b/src/Css/About.css deleted file mode 100644 index 066df2cf9..000000000 --- a/src/Css/About.css +++ /dev/null @@ -1,146 +0,0 @@ -@charset "UTF-8"; - -/* Imports - ------- */ - -@import "tailwindcss/base"; -@import "tailwindcss/components"; -@import "tailwindcss/utilities"; - -@import "Fonts.css"; -@import "Logo.css"; - -/* Basic - ----- */ - -p a, -ul a, -ol a { - @apply underline; - text-underline-offset: 2px; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - @apply font-bold leading-tight antialiased; - font-family: "Playfair Display", serif; - letter-spacing: -0.005em; -} - -h1 { - display: none; -} - -h2 { - @apply mb-6 mt-12 text-2xl; -} - -h3 { - @apply mb-5 mt-8 text-xl; -} - -h4 { - @apply mb-4 mt-6 text-lg; -} - -h5 { - @apply mb-5 mt-6 text-base; -} - -p { - @apply my-3 leading-relaxed; -} - -strong { - @apply font-semibold; -} - -/* Blockquote - ---------- */ - -blockquote { - @apply font-display mb-12 ml-0 mt-12 max-w-xl pl-0 font-bold tracking-tight; -} - -blockquote p { - @apply text-5xl leading-tight; - background: url(images/Background/21.jpg); - background-position: center 33%; - background-size: cover; - color: #b7c2d4; - - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -/* Code - ---- */ - -pre { - @apply overflow-x-auto overflow-y-hidden rounded border-2 border-gray-400 p-6 text-sm leading-relaxed; -} - -code { - @apply font-mono; -} - -@screen dark { - pre { - @apply border-base01; - } -} - -.hljs-comment, -.language-shell .hljs-meta, -.language-shell .hljs-meta + .bash { - opacity: 0.5; -} - -.hljs-string { - @apply text-base0b; -} - -/* Links - ----- */ - -h5 + p a { - @apply mb-2 inline-block; -} - -/* Lists - ----- */ - -ul, -ol { - @apply ml-3 leading-relaxed; -} - -ul li, -ol li { - @apply mb-1; -} - -ol { - counter-reset: ol; -} - -ol li::before { - @apply text-base06 inline-block pr-3; - counter-increment: ol; - content: counters(ol, ".") "."; -} - -ul li::before { - @apply text-base06 inline-block pr-3; - content: "•"; -} - -ul p, -ol p { - @apply my-0 inline; -} diff --git a/src/Css/Application.css b/src/Css/Application.css deleted file mode 100644 index cb70cee67..000000000 --- a/src/Css/Application.css +++ /dev/null @@ -1,257 +0,0 @@ -@charset "UTF-8"; - -/* Imports - ------- */ - -@import "tailwindcss/base"; -@import "tailwindcss/components"; -@import "tailwindcss/utilities"; - -@import "Fonts.css"; -@import "Logo.css"; -@import "Notifications.css"; - -/* 🛠 - ----- */ - -@layer utilities { - .scrolling-touch { - -webkit-overflow-scrolling: touch; - } - - .scrolling-auto { - -webkit-overflow-scrolling: auto; - } -} - -/* Base - ---- */ - -:root { - color-scheme: light dark; -} - -html, -body { - overflow: hidden; - overscroll-behavior: none; -} - -html, -body, -.screen-height { - height: 100dvh; -} - -body { - @apply text-base01; - - background-color: rgb(29, 29, 29); - background-image: url(images/ocean.jpg); - font-feature-settings: kern, liga; - font-weight: 400; - min-width: 300px; - text-rendering: optimizeLegibility; -} - -*:active { - outline: none; -} - -.align-sub { - vertical-align: sub; -} - -.all-small-caps { - font-variant-caps: all-small-caps; -} - -.flex-basis-0 { - flex-basis: 0; -} - -.smooth-scrolling { - scroll-behavior: smooth; -} - -@screen dark { - body { - @apply text-gray-600; - } -} - -/* Buttons - ------- */ - -button { - color: inherit; - font-family: inherit; -} - -/* Dragging - -------- */ - -.dragging-something { - @apply select-none; - cursor: grabbing; -} - -.dragging-something * { - cursor: grabbing !important; -} - -/* Forms - ----- */ - -input::placeholder, -textarea::placeholder { - color: rgba(0, 0, 0, 0.275); - opacity: 1; -} - -input:user-invalid, -textarea:user-invalid { - @apply border-base08; - box-shadow: none; - outline: none; -} - -select:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 rgb(63, 63, 63); -} - -@screen dark { - input::placeholder, - textarea::placeholder { - @apply text-base03; - opacity: 1; - } - - select:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 rgb(232, 232, 232); - } -} - -/* Loading - ------- */ - -.loading-animation { - animation: loading-rotator 2s linear infinite; -} - -.loading-animation__circle { - animation: - loading-dash 1.5s ease-in-out infinite, - loading-colors 6s ease-in-out infinite; - stroke-dasharray: 1, 86.25; - stroke-dashoffset: 0; - transform-origin: center; -} - -@keyframes loading-rotator { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -@keyframes loading-colors { - 0% { - stroke: rgb(248, 164, 167); - } - - 25% { - stroke: rgb(254, 196, 24); - } - - 50% { - stroke: rgb(72, 182, 133); - } - - 75% { - stroke: rgb(6, 182, 239); - } - - 100% { - stroke: rgb(248, 164, 167); - } -} - -@keyframes loading-dash { - 0% { - stroke-dasharray: 1, 86.25; - stroke-dashoffset: 0; - } - - 50% { - stroke-dasharray: 64.6875, 86.25; - stroke-dashoffset: -21.5625; - } - - 100% { - stroke-dasharray: 64.6875, 86.25; - stroke-dashoffset: -86.25; - } -} - -/* Overrides - --------- */ - -.no-tracks-view > div { - /* CSS fix for a scrolling issue: - Element would appear scrolled even though it's not (virtual-dom issue probably) - */ - overflow: visible !important; -} - -/* Range slider - ------------ */ - -.range-slider { - @apply w-24 appearance-none rounded bg-gray-400; - height: 4px; -} - -.range-slider::-webkit-slider-thumb { - @apply bg-base02 appearance-none; - border-radius: 100%; - height: 10px; - width: 10px; -} - -.range-slider::-moz-range-thumb { - @apply bg-darkest-hour appearance-none; - border-radius: 100%; - height: 10px; - width: 10px; -} - -.range-slider::-ms-thumb { - @apply bg-darkest-hour appearance-none; - border-radius: 100%; - height: 10px; - width: 10px; -} - -@screen dark { - .range-slider { - @apply bg-base01; - } - - .range-slider::-webkit-slider-thumb { - @apply bg-base07; - } - - .range-slider::-moz-range-thumb { - @apply bg-base07; - } - - .range-slider::-ms-thumb { - @apply bg-base07; - } -} diff --git a/src/Css/Fonts.css b/src/Css/Fonts.css deleted file mode 100644 index d19748e2a..000000000 --- a/src/Css/Fonts.css +++ /dev/null @@ -1,222 +0,0 @@ -/* Hack */ - - -@font-face { - font-family: "Hack"; - src: url("fonts/Hack/regular-subset.woff2") format("woff2"); - font-weight: 400; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Hack"; - src: url("fonts/Hack/bold-subset.woff2") format("woff2"); - font-weight: 700; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - - -/* Source Sans Pro */ - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/roman.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/light.woff2") format("woff2"); - font-weight: 300; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/italic.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/light-italic.woff2") format("woff2"); - font-weight: 300; - font-stretch: normal; - font-style: italic; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/roman.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/regular.woff2") format("woff2"); - font-weight: 400; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/italic.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/italic.woff2") format("woff2"); - font-weight: 400; - font-stretch: normal; - font-style: italic; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/roman.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/semibold.woff2") format("woff2"); - font-weight: 600; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/italic.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/semibold-italic.woff2") format("woff2"); - font-weight: 600; - font-stretch: normal; - font-style: italic; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/roman.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/bold.woff2") format("woff2"); - font-weight: 700; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/italic.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/bold-italic.woff2") format("woff2"); - font-weight: 700; - font-stretch: normal; - font-style: italic; - font-display: swap; -} - - -@font-face { - font-family: "Source Sans Pro"; - src: url("fonts/Source Sans Pro/Variable/roman.woff2") format("woff2-variations"), - url("fonts/Source Sans Pro/Old/black.woff2") format("woff2"); - font-weight: 900; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - - -/* Montserrat */ - - -@font-face { - font-family: "Montserrat"; - src: url("fonts/Montserrat/Variable/variable.woff2") format("woff2-variations"), - url("fonts/Montserrat/Old/light.woff2") format("woff2"); - font-weight: 300; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Montserrat"; - src: url("fonts/Montserrat/Variable/variable.woff2") format("woff2-variations"), - url("fonts/Montserrat/Old/regular.woff2") format("woff2"); - font-weight: 400; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Montserrat"; - src: url("fonts/Montserrat/Variable/variable.woff2") format("woff2-variations"), - url("fonts/Montserrat/Old/medium.woff2") format("woff2"); - font-weight: 500; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Montserrat"; - src: url("fonts/Montserrat/Variable/variable.woff2") format("woff2-variations"), - url("fonts/Montserrat/Old/semibold.woff2") format("woff2"); - font-weight: 600; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Montserrat"; - src: url("fonts/Montserrat/Variable/variable.woff2") format("woff2-variations"), - url("fonts/Montserrat/Old/bold.woff2") format("woff2"); - font-weight: 700; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: "Montserrat"; - src: url("fonts/Montserrat/Variable/variable.woff2") format("woff2-variations"), - url("fonts/Montserrat/Old/extrabold.woff2") format("woff2"); - font-weight: 800; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - - -/* Playfair Display */ - - -@font-face { - font-family: "Playfair Display"; - src: url("fonts/Playfair Display/bold.woff2") format("woff2"); - font-weight: 700; - font-stretch: normal; - font-style: normal; - font-display: swap; -} - - - -/* Icons */ - - -@font-face { - font-family: "or-favourites"; - src: - url(data:application/x-font-woff;charset=utf-8;base64,d09GMgABAAAAAALMAA0AAAAABtgAAAJ3AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4GYACCZhEICoIkgiALEAABNgIkAxoEIAWDYwc/G90FyK4GbGMY+iEaIl0ZXnqKj/KnMxo7rFjbeJ5aK9/v7pldlgeU6EhAReQQhOPyqSgCF2HRqQMjoqPX6vfTvRLxyy8hvfxwgdgCG18WlkhGZ3wxVyCZK1Ax/aiqC+B/4mbcWICdj4mBZGkigQYYTgsk7LKxKMD9f/5g/fla58se9MEfLNSfcvcO9O3SWls3IbDt4mQLsPO506mPDqZYAkVELxoKan2ddJQfZfAFrM4/QkypmmLtKeRVvX0rsyIZCEj3I9S5bSDwhL5gg4FzYGqoaGI/qeHF70Bi9gvts99VMQVAVNKTCqSggHQPjarzL3rg45FF1mAA5uhxD7wHNEAQUaTWurrhQYB4+hIlu2a3I8xOXQvzO3CizrFj67snzG8Dsray+jlA9h5ezAsG025eelIV85r3AwDU71dzp4caxei26MzamixWl2hW+WUHJJl1NZnROUYue2qoNFe4OdrTX+dfx9KE/vahFdXFL1cVr2ofSZppTAzr8pckcBbsbPn493jRlIeqiT2AI78PQBDeHHyt7bBO/T80b+bKaag/KzBUQBjfHpqKuPLt0DdAbgJQ0LilHmHgCD1QAAAw91dAzGwgUAycRKCaeYRAM/NWQGfSDeiR3c1ja3GjL20k1iCiirkDVLUmG2iIiuos2aV6a87fMnaYFn0GRZLqMqrPiEFyw7ghePoi8puJy+TDfAi4hhONUOkyCO+o1YPOhTW69BZu0BC5Pho+mCgxcD9rjEXFDHTaBX00hvkQcRrcoC7llF4+uk3wUaRHnzJ9wImCp+nYcd4p6un78cMlgEKfGJCipKJHXNbQi6qJfhkzHtHIY2JiGAA=) format("woff2"), url(data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAWQAAsAAAAABUQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgDxEM82NtYXAAAAFoAAAAXAAAAFwAsAEYZ2FzcAAAAcQAAAAIAAAACAAAABBnbHlmAAABzAAAATAAAAEwWmanp2hlYWQAAAL8AAAANgAAADYOgV4yaGhlYQAAAzQAAAAkAAAAJAdsA8dobXR4AAADWAAAABgAAAAYDgAArGxvY2EAAANwAAAADgAAAA4AwACAbWF4cAAAA4AAAAAgAAAAIAAJADduYW1lAAADoAAAAc4AAAHOtR+CC3Bvc3QAAAVwAAAAIAAAACAAAwAAAAMDVQGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAAAAHQDwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEAEAAAAAMAAgAAgAEAAEAIABmAHT//f//AAAAAAAgAGYAdP/9//8AAf/j/57/kQADAAEAAAAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAACAFYAHQOqAysAGAA0AAAlPgM1NCYjIgYHIy4BIyIGFRQeAh8BEzIeAhUUDgIPAScuAzU0PgIzMhYXPgECBEx9WTBVQTFWEVAQVjJAVjBZfE0EwDFWPyQ0X4ZTPj5Sh180JD9VMjdmIyJmk0V0aF4vQFQ5Kys5VEAvXmh0RQQCnCQ/VjE9cXR/Szg2S392cT0xVj8kMigoMgAAAAABAFYAHQOqAysAGwAAJScuAzU0PgIzMhYXPgEzMh4CFRQOAgcCAD5Sh180JD9VMjdmIyJmODFWPyQ0X4ZTHThLf3RxPTFWPyQyKCgyJD9WMT1xdn9LAAAAAQAAAAEAALY04pNfDzz1AAsEAAAAAADV3I0FAAAAANXcjQUAAAAAA6oDKwAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADqgABAAAAAAAAAAAAAAAAAAAABgQAAAAAAAAAAAAAAAIAAAAEAABWBAAAVgAAAAAACgAUAB4AbACYAAAAAQAAAAYANQACAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAA0AAAABAAAAAAACAAcAlgABAAAAAAADAA0ASAABAAAAAAAEAA0AqwABAAAAAAAFAAsAJwABAAAAAAAGAA0AbwABAAAAAAAKABoA0gADAAEECQABABoADQADAAEECQACAA4AnQADAAEECQADABoAVQADAAEECQAEABoAuAADAAEECQAFABYAMgADAAEECQAGABoAfAADAAEECQAKADQA7G9yLWZhdm91cml0ZXMAbwByAC0AZgBhAHYAbwB1AHIAaQB0AGUAc1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMG9yLWZhdm91cml0ZXMAbwByAC0AZgBhAHYAbwB1AHIAaQB0AGUAc29yLWZhdm91cml0ZXMAbwByAC0AZgBhAHYAbwB1AHIAaQB0AGUAc1JlZ3VsYXIAUgBlAGcAdQBsAGEAcm9yLWZhdm91cml0ZXMAbwByAC0AZgBhAHYAbwB1AHIAaQB0AGUAc0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format("woff"); - font-weight: normal; - font-stretch: normal; - font-style: normal; -} diff --git a/src/Css/Logo.css b/src/Css/Logo.css deleted file mode 100644 index 075cb26f7..000000000 --- a/src/Css/Logo.css +++ /dev/null @@ -1,15 +0,0 @@ -.logo { - opacity: 0.85; -} - -.logo img { - width: 154px; -} - -.logo-backdrop { - background-image: url("images/diffuse__icon-dark.svg"); - background-position: -43.5% 98px; - transform-origin: left top; - transform: rotate(90deg); - width: 105vh; -} diff --git a/src/Css/Notifications.css b/src/Css/Notifications.css deleted file mode 100644 index 303fdc96b..000000000 --- a/src/Css/Notifications.css +++ /dev/null @@ -1,29 +0,0 @@ -.notifications { - font-size: 13px; - line-height: 1.35; -} - -.notifications a, -.notifications em { - border-bottom: 1px solid rgba(255, 255, 255, 0.45); - display: inline-block; -} - -.notifications a { - color: inherit; - font-weight: 600; - text-decoration: none; -} - -.notifications p { - margin: 0; - padding: 0; -} - -.notifications em { - font-weight: inherit; -} - -.notifications strong { - font-weight: 600; -} diff --git a/src/Javascript/Brain/application.ts b/src/Javascript/Brain/application.ts deleted file mode 100644 index eeb2156dc..000000000 --- a/src/Javascript/Brain/application.ts +++ /dev/null @@ -1,24 +0,0 @@ -import "./index.d" - -// @ts-ignore -import { Elm } from "brain.elm.js" - - -// 🚀 - - -const flags: Record = location - .hash - .substring(1) - .split("&") - .reduce((acc, flag) => { - const [k, v] = flag.split("=") - return { ...acc, [k]: v } - }, {}) - - -export const load = () => Elm.Brain.init({ - flags: { - initialUrl: decodeURIComponent(flags.appHref) || "" - } -}) diff --git a/src/Javascript/Brain/artwork.ts b/src/Javascript/Brain/artwork.ts deleted file mode 100644 index fd8e9c896..000000000 --- a/src/Javascript/Brain/artwork.ts +++ /dev/null @@ -1,218 +0,0 @@ -// -// Album Covers -// (◕‿◕✿) - -import * as Uint8arrays from "uint8arrays" - -import * as processing from "./processing" -import { type App } from "./elm/types" -import { transformUrl } from "../urls" -import { toCache } from "./common" -import { type CoverPrep } from "../common" - - -// 🌳 - - -type CoverPrepWithUrls = CoverPrep & { - trackGetUrl: string - trackHeadUrl: string -} - - - -// 🏔️ - - -let artworkQueue: CoverPrep[] = [] -let app: App - - - -// 🚀 - - -export function init(a: App) { - app = a - - app.ports.provideArtworkTrackUrls.subscribe(provideArtworkTrackUrls) -} - - - -// PORTS - - -function provideArtworkTrackUrls(prep: CoverPrepWithUrls) { - find(prep).then(blob => { - return toCache(`coverCache.${prep.cacheKey}`, blob).then(_ => blob) - }) - .then((blob: Blob) => { - const url = URL.createObjectURL(blob) - - self.postMessage({ - tag: "GOT_CACHED_COVER", - data: { imageType: blob.type, key: prep.cacheKey, url: url }, - error: null - }) - }) - .catch(err => { - if (err === "No artwork found") { - // Indicate that we've tried to find artwork, - // so that we don't try to find it each time we launch the app. - return toCache(`coverCache.${prep.cacheKey}`, "TRIED") - - } else { - // Something went wrong - console.error(err) - return toCache(`coverCache.${prep.cacheKey}`, "TRIED") - - } - }) - .catch(() => { - console.warn("Failed to download artwork for ", prep) - }) - .finally(shiftQueue) -} - - - -// 🛠️ - - -export function download(list: CoverPrep[]) { - const exe = !artworkQueue[0] - artworkQueue = artworkQueue.concat(list) - if (exe) shiftQueue() -} - - -function shiftQueue() { - const next = artworkQueue.shift() - - if (next) { - app.ports.makeArtworkTrackUrls.send(next) - } else { - self.postMessage({ - action: "FINISHED_DOWNLOADING_ARTWORK", - data: null - }) - } -} - - - -// ㊙️ - - -const REJECT = () => Promise.reject("No artwork found") - - -function decodeCacheKey(cacheKey: string) { - return Uint8arrays.toString( - Uint8arrays.fromString(cacheKey, "base64"), - "utf8" - ) -} - - -function find(prep: CoverPrepWithUrls) { - return findUsingTags(prep) - .then(a => a ? a : findUsingMusicBrainz(prep)) - .then(a => a ? a : findUsingLastFm(prep)) - .then(a => a ? a : REJECT()) - .then(a => a.type.startsWith("image/") ? a : REJECT()) -} - - - -// 1. TAGS - - -async function findUsingTags(prep: CoverPrepWithUrls) { - return Promise.all( - [ - transformUrl(prep.trackHeadUrl, app), - transformUrl(prep.trackGetUrl, app) - ] - - ).then(([ headUrl, getUrl ]) => processing.getTags( - headUrl, - getUrl, - prep.trackFilename, - { covers: true } - - )).then(tags => { - return tags?.picture - ? new Blob([ tags.picture.data ], { type: tags.picture.format }) - : null - - }) -} - - - -// 2. MUSIC BRAINZ - - -function findUsingMusicBrainz(prep: CoverPrepWithUrls) { - if (!navigator.onLine) return null - - const parts = decodeCacheKey(prep.cacheKey).split(" --- ") - const artist = parts[ 0 ] - const album = parts[ 1 ] || parts[ 0 ] - - const query = `release:"${album}"` + (prep.variousArtists === "t" ? `` : ` AND artist:"${artist}"`) - const encodedQuery = encodeURIComponent(query) - - return fetch(`https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`) - .then(r => r.json()) - .then(r => musicBrainzCover(r.releases)) -} - - -function musicBrainzCover(remainingReleases) { - const release = remainingReleases[ 0 ] - if (!release) return null - - return fetch( - `https://coverartarchive.org/release/${release.id}/front-500` - ).then( - r => r.blob() - ).then( - r => r && r.type.startsWith("image/") - ? r - : musicBrainzCover(remainingReleases.slice(1)) - ).catch( - () => musicBrainzCover(remainingReleases.slice(1)) - ) -} - - - -// 3. LAST FM - - -function findUsingLastFm(prep: CoverPrepWithUrls) { - if (!navigator.onLine) return null - - const query = encodeURIComponent( - decodeCacheKey(prep.cacheKey).replace(" --- ", " ") - ) - - return fetch(`https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`) - .then(r => r.json()) - .then(r => lastFmCover(r.results.albummatches.album)) -} - - -function lastFmCover(remainingMatches) { - const album = remainingMatches[ 0 ] - const url = album ? album.image[ album.image.length - 1 ][ "#text" ] : null - - return url && url !== "" - ? fetch(url) - .then(r => r.blob()) - .catch(_ => lastFmCover(remainingMatches.slice(1))) - : album && lastFmCover(remainingMatches.slice(1)) -} diff --git a/src/Javascript/Brain/common.ts b/src/Javascript/Brain/common.ts deleted file mode 100644 index f61bedd52..000000000 --- a/src/Javascript/Brain/common.ts +++ /dev/null @@ -1,117 +0,0 @@ -// -// Common stuff -// ʕ•ᴥ•ʔ - - -import * as crypto from "../crypto" -import { db } from "../common" - - -export const SECRET_KEY_LOCATION = "SECRET_KEY" - - -// 🔱 - - -export function isLocalHost(url: string) { - return ( - url.startsWith("localhost") || - url.startsWith("localhost") || - url.startsWith("127.0.0.1") || - url.startsWith("127.0.0.1") - ) -} - - -export function parseJsonIfNeeded(a: unknown) { - if (typeof a === "string") return JSON.parse(a) - return a -} - - -export function reportError(app, event) { - return e => { - const err = e ? e.message || e : null - if (err) { - console.error(err, e.stack) - app.ports.fromAlien.send({ tag: event.tag, data: null, error: err }) - } - } -} - - -export function sendData(app, event, opts: any = {}) { - return data => { - app.ports.fromAlien.send({ - tag: event.tag, - data: (opts && opts.parseJSON && typeof data === "string") - ? JSON.parse(data) - : (data || null), - error: null - }) - } -} - - - -// Cache -// ----- - -export function removeCache(key: string): Promise { - return db().removeItem(key) -} - - -export function fromCache(key: string): Promise { - return db().getItem(key) -} - - -export function toCache(key: string, data: unknown): Promise { - return db().setItem(key, data) -} - - - -// Crypto -// ------ - -export function decryptIfNeeded(data: unknown): Promise { - if (typeof data !== "string") { - return Promise.resolve(data) - - } else if (typeof data === "string" && (data.startsWith("{") || data.startsWith("["))) { - return Promise.resolve(data) - - } else if (data.length < 15 && Number.isInteger(parseInt(data, 10))) { - return Promise.resolve(data) - - } else { - return data - ? getSecretKey().then(secretKey => { - if (!secretKey) throw new Error("There seems to be existing data that's encrypted, I will need the passphrase (ie. encryption key) to continue.") - return crypto.decrypt(secretKey, data) - }) - : Promise.resolve(null) - - } -} - - -export async function encryptIfPossible(unencryptedData: string): Promise { - return unencryptedData - ? getSecretKey().then(secretKey => - secretKey - ? crypto.encrypt(secretKey, unencryptedData) - : unencryptedData - ) - : unencryptedData -} - - -export { encryptIfPossible as encryptWithSecretKey } - - -export function getSecretKey(): Promise { - return db().getItem(SECRET_KEY_LOCATION) -} diff --git a/src/Javascript/Brain/elm/types.ts b/src/Javascript/Brain/elm/types.ts deleted file mode 100644 index e5a05984a..000000000 --- a/src/Javascript/Brain/elm/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type App = any // TODO: ElmApp - - -export type ElmPorts = { - // ← Elm - // ... - - // → Elm - fromAlien: PortToElm -} diff --git a/src/Javascript/Brain/index.d.ts b/src/Javascript/Brain/index.d.ts deleted file mode 100644 index d1860d30d..000000000 --- a/src/Javascript/Brain/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ElmPorts } from "./elm/types" - - -export { } - - -declare const Elm: { Brain: ElmMain } -declare const BUILD_TIMESTAMP: string - - -declare module "elm-taskport" { - const install: () => void - const register: (a: string, b: (arg: any) => any) => void -} diff --git a/src/Javascript/Brain/index.ts b/src/Javascript/Brain/index.ts deleted file mode 100644 index 4444ef914..000000000 --- a/src/Javascript/Brain/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -// -// Brain -// 🧠 -// -// This worker is responsible for everything non-UI. - -import * as Application from "./application" -import * as Artwork from "./artwork" -import * as Processing from "./processing" -import * as Search from "./search" -import * as User from "./user" -import * as TaskPorts from "./task-ports" -import * as Tracks from "./tracks" -import * as UI from "./ui" - - -// 🚀 - -TaskPorts.register() -User.TaskPorts.register() - -const app = Application.load() -const brain = self as unknown as Worker - -// 🖼️ - -UI.link(brain, app) - -// ⚡ -Artwork.init(app) -Processing.init(app) -Search.init(app) -Tracks.init(app) - -User.Ports.register(app) - -// 🛫 - -brain.postMessage({ action: "READY" }) diff --git a/src/Javascript/Brain/processing.ts b/src/Javascript/Brain/processing.ts deleted file mode 100644 index 4c625e658..000000000 --- a/src/Javascript/Brain/processing.ts +++ /dev/null @@ -1,303 +0,0 @@ -// -// Processing -// ♪(´ε` ) -// -// Audio processing, getting metadata, etc. - -import type { IAudioMetadata } from "music-metadata" -import type { GeneralTrack, MediaInfoResult } from "mediainfo.js" -import type { ITokenizer } from "strtok3" - -import * as Uint8arrays from "uint8arrays" -import { type App } from "./elm/types" -import { transformUrl } from "../urls" - - -// 🏔️ - - -const ENCODING_ISSUE_REPLACE_CHAR = '▩'; - -let app: App - - - -// 🚀 - - -export function init(a: App) { - app = a - - app.ports.requestTags.subscribe(requestTags) - app.ports.syncTags.subscribe(syncTags) -} - - - -// Ports -// ----- - - -function requestTags(context) { - processContext(context, app).then(newContext => { - app.ports.receiveTags.send(newContext) - }) -} - - -function syncTags(context) { - processContext(context, app).then(newContext => { - app.ports.replaceTags.send(newContext) - }) -} - - - -// Contexts -// -------- - - -export async function processContext(context, app) { - const initialPromise = Promise.resolve([]); - - return context.urlsForTags - .reduce((accumulator, urls, idx) => { - return accumulator.then((col) => { - const filename = context.receivedFilePaths[idx].split("/").reverse()[0]; - - return Promise.all([transformUrl(urls.headUrl, app), transformUrl(urls.getUrl, app)]) - .then(([headUrl, getUrl]) => { - return getTags(headUrl, getUrl, filename, { covers: false }); - }) - .then((r) => { - return col.concat(r); - }) - .catch((e) => { - console.warn(e); - return col.concat(null); - }); - }); - }, initialPromise) - .then((col) => { - context.receivedTags = col; - return context; - }); -} - - - -// Tags - General -// -------------- - - -type Tags = { - disc: number; - nr: number; - album: string | null; - artist: string | null; - title: string; - genre: string | null; - year: number | null; - picture: { data: Uint8Array; format: string } | null; -}; - -export async function getTags( - headUrl: string, - getUrl: string, - filename: string, - { covers }: { covers: boolean }, -) { - const musicMetadata = await import("music-metadata"); - const httpTokenizer = await import("@tokenizer/http"); - const rangeTokenizer = await import("@tokenizer/range"); - - let tokenizer: ITokenizer; - let mmResult; - - try { - const httpClient = new httpTokenizer.HttpClient(headUrl, { resolveUrl: false }); - httpClient.resolvedUrl = getUrl - - tokenizer = await rangeTokenizer.tokenizer(httpClient); - - mmResult = await musicMetadata - .parseFromTokenizer(tokenizer, { skipCovers: !covers }) - .catch((err) => { - console.warn(err); - return null; - }); - } catch (err) { - console.warn(err); - } - - const mmTags = mmResult && pickTagsFromMusicMetadata(filename, mmResult); - if (mmTags) return mmTags; - - const miResult = await (await mediaInfoClient(covers)) - .analyzeData(getSize(headUrl), readChunk(getUrl)) - .catch((err) => { - console.warn(err); - return null; - }); - - const miTags = miResult && pickTagsFromMediaInfo(filename, miResult); - if (miTags) return miTags; - - return fallbackTags(filename); -} - -function fallbackTags(filename: string): Tags { - const filenameWithoutExt = filename.replace(/\.\w+$/, ""); - - return { - disc: 1, - nr: 1, - album: null, - artist: null, - title: filenameWithoutExt, - genre: null, - year: null, - picture: null, - }; -} - -// Tags - Media Info -// ----------------- - -const getSize = (headUrl: string) => async (): Promise => { - const response = await fetch(headUrl, { method: "HEAD" }); - - if (!response.ok) { - throw new Error(`HTTP error status=${response.status}: ${response.statusText}`); - } - - const l = response.headers.get("Content-Length"); - - if (l) { - return parseInt(l, 10); - } else { - throw new Error("HTTP response doesn't have a Content-Length"); - } -}; - -const readChunk = - (getUrl: string) => - async (chunkSize: number, offset: number): Promise => { - if (chunkSize === 0) return new Uint8Array(); - - const from = offset; - const to = offset + chunkSize; - - const start = to < from ? to : from; - const end = to < from ? from : to; - - const response = await fetch(getUrl, { - method: "GET", - headers: { - Range: `bytes=${start}-${end}`, - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error status=${response.status}: ${response.statusText}`); - } - - return new Uint8Array(await response.arrayBuffer()); - }; - -function pickTagsFromMediaInfo(filename: string, result: MediaInfoResult): Tags | null { - const tagsRaw = result?.media?.track?.filter((t) => t["@type"] === "General")[0]; - const tags = tagsRaw === undefined ? undefined : tagsRaw as GeneralTrack; - if (tags === undefined) return null; - - let artist = typeof tags.Performer == "string" ? tags.Performer : null; - let album = typeof tags.Album == "string" ? tags.Album : null; - - let title = - typeof tags.Track == "string" ? tags.Track : typeof tags.Title == "string" ? tags.Title : null; - - if (!artist && !title) return null; - - // TODO: Encoding issues with mediainfo.js - // https://github.com/buzz/mediainfo.js/issues/150 - if (artist?.includes("�")) artist = artist.replace("�", ENCODING_ISSUE_REPLACE_CHAR) - if (album?.includes("�")) album = album.replace("�", ENCODING_ISSUE_REPLACE_CHAR) - if (title?.includes("�")) title = title.replace("�", ENCODING_ISSUE_REPLACE_CHAR) - - if (artist && artist.includes(" / ")) { - artist = artist - .split(" / ") - .filter((a) => a.trim() !== "") - .join(", "); - } - - const year = tags.Recorded_Date ? new Date(Date.parse(tags.Recorded_Date)).getFullYear() : null; - - return { - disc: tags.Part_Position || 1, - nr: tags.Track_Position || 1, - album: album, - artist: artist, - title: title || filename.replace(/\.\w+$/, ""), - genre: tags.Genre || null, - year: year !== null && isNaN(year) ? null : year, - picture: tags.Cover_Data - ? { - data: Uint8arrays.fromString(tags.Cover_Data.split(" / ")[0], "base64pad"), - format: tags.Cover_Mime || "image/jpeg", - } - : null, - }; -} - - -// Tags - Music Metadata -// --------------------- - - -function pickTagsFromMusicMetadata(filename: string, result: IAudioMetadata): Tags | null { - const tags = result && result.common; - if (!tags) return null; - - const artist = tags.artist && tags.artist.length ? tags.artist : null; - const title = tags.title && tags.title.length ? tags.title : null; - - if (!artist && !title) return null; - - return { - disc: tags.disk.no || 1, - nr: tags.track.no || 1, - album: tags.album && tags.album.length ? tags.album : null, - artist: artist, - title: title || filename.replace(/\.\w+$/, ""), - genre: (tags.genre && tags.genre[0]) || null, - year: tags.year || null, - picture: - tags.picture && tags.picture[0] - ? { data: tags.picture[0].data, format: tags.picture[0].format } - : null, - }; -} - - - -// 🛠️ - - -let client - - -async function mediaInfoClient(covers: boolean) { - const MediaInfoFactory = await import("mediainfo.js").then((a) => a.default); - - if (client) return client - - client = await MediaInfoFactory({ - coverData: covers, - locateFile: () => { - return "../../wasm/media-info.wasm"; - }, - }); - - return client -} diff --git a/src/Javascript/Brain/search.ts b/src/Javascript/Brain/search.ts deleted file mode 100644 index b2b978b5a..000000000 --- a/src/Javascript/Brain/search.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { App } from "./elm/types" - - -// 🏔️ - - -let app: App - - - -// 🚀 - - -export function init(a: App) { - app = a - - app.ports.requestSearch.subscribe(requestSearch) - app.ports.updateSearchIndex.subscribe(updateSearchIndex) -} - - -const search = new Worker( - "../../search.js", - { type: "module" } -) - - -search.onmessage = event => { - switch (event.data.action) { - case "PERFORM_SEARCH": - app.ports.receiveSearchResults.send(event.data.data) - break - } -} - - - -// PORTS - - -function requestSearch(searchTerm: string) { - search.postMessage({ - action: "PERFORM_SEARCH", - data: searchTerm - }) -} - - -function updateSearchIndex(tracksJson: string) { - search.postMessage({ - action: "UPDATE_SEARCH_INDEX", - data: tracksJson - }) -} diff --git a/src/Javascript/Brain/task-ports.ts b/src/Javascript/Brain/task-ports.ts deleted file mode 100644 index ea7d6bf9e..000000000 --- a/src/Javascript/Brain/task-ports.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-ignore -import * as TaskPort from "elm-taskport" - -import { fromCache, removeCache, toCache } from "./common" - - -export function register() { - TaskPort.install() - - TaskPort.register("fromCache", fromCache) - TaskPort.register("removeCache", removeCache) - TaskPort.register("toCache", ({ key, value }) => toCache(key, value)) -} diff --git a/src/Javascript/Brain/tracks.ts b/src/Javascript/Brain/tracks.ts deleted file mode 100644 index 530eaa916..000000000 --- a/src/Javascript/Brain/tracks.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { App } from "./elm/types" -import { db } from "../common" -import { reportError } from "./common" -import { transformUrl } from "../urls" - - -// 🏔️ - - -let app: App - - - -// 🚀 - - -export function init(a: App) { - app = a - - app.ports.downloadTracks.subscribe(downloadTracks) - app.ports.removeTracksFromCache.subscribe(removeTracksFromCache) - app.ports.storeTracksInCache.subscribe(storeTracksInCache) -} - - - -// PORTS - - -function downloadTracks(group) { - self.postMessage({ - action: "DOWNLOAD_TRACKS", - data: group - }) -} - - -function removeTracksFromCache(trackIds) { - trackIds.reduce( - (acc, id) => acc.then(_ => db("tracks").removeItem(id)), - Promise.resolve() - - ).catch( - _ => reportError - (app, { tag: "REMOVE_TRACKS_FROM_CACHE" }) - ("Failed to remove tracks from cache") - - ) -} - - -function storeTracksInCache(list) { - list.reduce( - (acc, item) => { - return acc - .then(_ => transformUrl(item.url, app)) - .then(fetch) - .then(r => r.blob()) - .then(b => db("tracks").setItem(item.trackId, b)) - }, - Promise.resolve() - - ).then( - _ => self.postMessage({ - tag: "STORE_TRACKS_IN_CACHE", - data: list.map(l => l.trackId), - error: null - }) - - ).catch( - err => { - console.error(err) - self.postMessage({ - tag: "STORE_TRACKS_IN_CACHE", - data: list.map(l => l.trackId), - error: err.message || err - }) - } - - ) -} diff --git a/src/Javascript/Brain/ui.ts b/src/Javascript/Brain/ui.ts deleted file mode 100644 index 46c104f44..000000000 --- a/src/Javascript/Brain/ui.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { App } from "./elm/types" -import * as Artwork from "./artwork" - - -export function link(worker: Worker, app: App) { - app.ports.toUI.subscribe(event => { - worker.postMessage(event) - }) - - worker.onmessage = event => { - if (event.data.action) return handleAction(event.data.action, event.data.data) - if (event.data.tag) return app.ports.fromAlien.send(event.data) - } - - - function handleAction(action: string, data: unknown) { - switch (action) { - case "DOWNLOAD_ARTWORK": return Artwork.download(data) - } - } -} diff --git a/src/Javascript/Brain/user.ts b/src/Javascript/Brain/user.ts deleted file mode 100644 index ae6ffaf37..000000000 --- a/src/Javascript/Brain/user.ts +++ /dev/null @@ -1,185 +0,0 @@ -// -// User -// (づ。◕‿‿◕。)づ -// -// Related to the user layer. - - -// @ts-ignore -import * as TaskPort from "elm-taskport" - -import type { App } from "./elm/types" - -import * as crypto from "../crypto" - -import { decryptIfNeeded, encryptIfPossible, SECRET_KEY_LOCATION } from "./common" -import { parseJsonIfNeeded, toCache } from "./common" - - -const ports: Record = {} -const taskPorts: Record = {} - - -// Crypto -// ====== - -taskPorts.fabricateSecretKey = async passphrase => { - const data = await crypto.keyFromPassphrase(passphrase) - return toCache(SECRET_KEY_LOCATION, data) -} - - - -// Dropbox -// ------- - -taskPorts.fromDropbox = ({ fileName, token }) => { - return fetch("https://content.dropboxapi.com/2/files/download", { - method: "POST", - headers: { - "Authorization": "Bearer " + token, - "Dropbox-API-Arg": JSON.stringify({ path: "/" + fileName }) - } - }) - .then(r => r.ok ? r.text() : r.json()) - .then(r => r.error ? null : r) - .then(decryptIfNeeded) - .then(parseJsonIfNeeded) -} - - -taskPorts.toDropbox = async ({ fileName, data, token }) => { - const json = JSON.stringify(data) - const params = { - path: "/" + fileName, - mode: "overwrite", - mute: true - } - - return fetch("https://content.dropboxapi.com/2/files/upload", { - method: "POST", - headers: { - "Authorization": "Bearer " + token, - "Content-Type": "application/octet-stream", - "Dropbox-API-Arg": JSON.stringify(params) - }, - body: await encryptIfPossible(json) - }) -} - - - -// IPFS -// ---- - -const IPFS_ROOT = "/Applications/Diffuse/" - - -taskPorts.fromIpfs = ({ apiOrigin, fileName }) => { - const path = IPFS_ROOT + fileName - - return fetch(apiOrigin + "/api/v0/files/read?arg=" + encodeURIComponent(path), { method: "POST" }) - .then(r => r.ok ? r.text() : r.json()) - .then(r => r.Code === 0 ? null : r) - .then(decryptIfNeeded) - .then(parseJsonIfNeeded) -} - - -taskPorts.toIpfs = ({ apiOrigin, fileName, data }) => { - const json = JSON.stringify(data) - const params = new URLSearchParams({ - arg: IPFS_ROOT + fileName, - create: "true", - offset: "0", - parents: "true", - truncate: "true" - }).toString() - - return encryptIfPossible(json).then(possiblyEncryptedData => { - const formData = new FormData() - - formData.append("data", possiblyEncryptedData) - - return fetch( - apiOrigin + "/api/v0/files/write?" + params, - { method: "POST", body: formData } - ) - }) -} - - - -// Remote Storage -// -------------- - -let rs -let rsClient - - -async function remoteStorage(userAddress: string, token: string) { - if (!rs) { - const { default: RemoteStorage } = await import("remotestoragejs") - - rs = new RemoteStorage({ cache: false }) - rs.access.claim("diffuse", "rw") - - rsClient = rs.scope("/diffuse/") - - return new Promise(resolve => { - rs.on("connected", resolve) - rs.connect(userAddress, token) - }) - - } else { - return Promise.resolve() - - } -} - - -ports.deconstructRemoteStorage = _app => _ => { - rs = null - rsClient = null -} - - -taskPorts.fromRemoteStorage = ({ fileName, userAddress, token }) => { - return remoteStorage(userAddress, token) - .then(_ => rsClient.getFile(fileName)) - .then(r => r.data) - .then(decryptIfNeeded) - .then(parseJsonIfNeeded) -} - - -taskPorts.toRemoteStorage = ({ data, fileName, userAddress, token }) => { - const json = JSON.stringify(data) - - return remoteStorage(userAddress, token) - .then(_ => encryptIfPossible(json)) - .then(data => rsClient.storeFile("application/json", fileName, data)) -} - - - -// EXPORT -// ====== - -function registerPorts(app: App) { - Object.keys(ports).forEach(name => { - const fn = ports[ name ](app) - app.ports[ name ].subscribe(fn) - }) -} - -function registerTaskPorts() { - Object.keys(taskPorts).forEach(name => { - const fn = taskPorts[ name ] - TaskPort.register(name, fn) - }) -} - - -export const TaskPorts = { register: registerTaskPorts } -export const Ports = { register: registerPorts } diff --git a/src/Javascript/UI/application.ts b/src/Javascript/UI/application.ts deleted file mode 100644 index 58acdd26d..000000000 --- a/src/Javascript/UI/application.ts +++ /dev/null @@ -1,106 +0,0 @@ -import "./index.d" -import type { App } from "./elm/types" -import { version } from "../../../package.json" - - -// 🏔️ - - -let app: App -let channel: BroadcastChannel - - - -// 🚀 - - -export const load = ({ isNativeWrapper, reg }: { isNativeWrapper: boolean, reg: ServiceWorkerRegistration }) => Elm.UI.init({ - node: document.getElementById("elm") || undefined, - flags: { - buildTimestamp: BUILD_TIMESTAMP, - darkMode: preferredColorScheme().matches, - initialTime: Date.now(), - isInstallingServiceWorker: !!reg.installing, - isOnline: navigator.onLine, - isTauri: isNativeWrapper, - version, - viewport: { - height: window.innerHeight, - width: window.innerWidth - } - } -}) - - -export function init(a: App, c: BroadcastChannel) { - app = a - channel = c - - app.ports.downloadJsonUsingTauri.subscribe(downloadJsonUsingTauri) - app.ports.openUrlOnNewPage.subscribe(openUrlOnNewPage) - app.ports.reloadApp.subscribe(reloadApp) -} - - - -// 🌗 - - -function preferredColorScheme() { - const m = - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)") - - m?.addEventListener("change", e => { - app.ports.preferredColorSchemaChanged.send({ dark: e.matches }) - }) - - return m -} - - - -// PORTS - - -async function downloadJsonUsingTauri( - { filename, json }: { filename: string, json: string } -) { - const { save } = await import("@tauri-apps/plugin-dialog") - const { writeTextFile } = await import("@tauri-apps/plugin-fs") - const { BaseDirectory } = await import("@tauri-apps/api/path") - - const filePath = await save({ defaultPath: filename }) - await writeTextFile(filePath || filename, json, { baseDir: BaseDirectory.Download }) -} - - -function openUrlOnNewPage(url: string) { - if (globalThis.__TAURI__) { - globalThis.__TAURI__.shell.open( - url.includes("://") ? url : `${location.origin}/${url.replace(/^\.\//, "")}` - ) - - } else { - window.open(url, "_blank") - - } -} - - -function reloadApp() { - const timeout = setTimeout(async () => { - const reg = await navigator.serviceWorker.getRegistration() - if (reg?.waiting) reg.waiting.postMessage("skipWaiting") - window.location.reload() - }, 250) - - channel.addEventListener("message", event => { - if (event.data === "PONG") { - clearTimeout(timeout) - alert("⚠️ You can only update the app when you have no more than one instance open.") - } - }) - - channel.postMessage("PING") -} diff --git a/src/Javascript/UI/artwork.ts b/src/Javascript/UI/artwork.ts deleted file mode 100644 index 0c5b35127..000000000 --- a/src/Javascript/UI/artwork.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { debounce } from "throttle-debounce" - -import type { App } from "./elm/types" -import { type CoverPrep, db } from "../common" - - - -// 🏔️ - - -let app: App -let brain: Worker - - - -// 🚀 - - -export function init(a: App, b: Worker) { - app = a - brain = b - - app.ports.loadAlbumCovers.subscribe( - debounce(500, loadAlbumCoversFromDom) - ) - - db().keys().then(cachedCovers) -} - - - -// 🛠️ - - -export function albumCover(coverKey: string): Promise { - return db().getItem(`coverCache.${coverKey}`) -} - - -async function loadAlbumCoversFromDom({ coverView, list }: { coverView: boolean, list: boolean }): Promise { - let nodes: HTMLElement[] = [] - - if (list) nodes = nodes.concat(Array.from( - document.querySelectorAll("#diffuse__track-covers [data-key]") - )) - - if (coverView) nodes = nodes.concat(Array.from( - document.querySelectorAll("#diffuse__track-covers + div [data-key]") - )) - - if (!nodes.length) return; - - const coverPrepList = nodes.reduce((acc: CoverPrep[], node: HTMLElement) => { - const a = { - cacheKey: node.getAttribute("data-key"), - trackFilename: node.getAttribute("data-filename"), - trackPath: node.getAttribute("data-path"), - trackSourceId: node.getAttribute("data-source-id"), - variousArtists: node.getAttribute("data-various-artists") - } - - if (a.cacheKey && a.trackFilename && a.trackPath && a.trackSourceId && a.variousArtists) { - return [...acc, a as CoverPrep] - } else { - return acc - } - }, [] as CoverPrep[]) - - return loadAlbumCovers(coverPrepList) -} - - -export async function loadAlbumCovers(coverPrepList: CoverPrep[]): Promise { - const withoutEarlierAttempts = await coverPrepList.reduce(async ( - acc: Promise, - prep: CoverPrep - ): Promise => { - const arr = await acc - const a = await albumCover(prep.cacheKey) - if (!a) return [...arr, prep] - return arr - }, Promise.resolve([])) - - brain.postMessage({ - action: "DOWNLOAD_ARTWORK", - data: withoutEarlierAttempts - }) -} - - -// Send a dictionary of the cached covers to the app. -async function cachedCovers(keys: string[]) { - const cacheKeys = keys.filter( - k => k.startsWith("coverCache.") - ) - - const cache = await cacheKeys.reduce(async (acc, key) => { - const c = await acc - const blob = await db().getItem(key) - const cacheKey = key.slice(11) - - if (blob && typeof blob !== "string" && blob instanceof Blob) { - c[cacheKey] = URL.createObjectURL(blob) - } - - return c - }, Promise.resolve({})) - - app.ports.insertCoverCache.send(cache) - setTimeout(() => loadAlbumCoversFromDom({ list: true, coverView: true }), 500) -} diff --git a/src/Javascript/UI/audio.ts b/src/Javascript/UI/audio.ts deleted file mode 100644 index 8576176a8..000000000 --- a/src/Javascript/UI/audio.ts +++ /dev/null @@ -1,533 +0,0 @@ -// -// Audio engine -// ♪(´ε` ) - -import type { App } from "./elm/types" - -import Timer from "timer.js" -import { debounce } from "throttle-debounce" -import { CoverPrep, db, mimeType } from "../common" -import { albumCover, loadAlbumCovers } from "./artwork" -import { transformUrl } from "../urls" - - -// 🏔️ - - -const silentMp3File = - "data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV" - - -let app: App -let container: Element | null = null -let scrobbleTimer: Timer | null = null - - - -// 🚀 - - -export function init(a: App) { - app = a - - app.ports.adjustEqualizerSetting.subscribe(adjustEqualizerSetting) - app.ports.pause.subscribe(pause) - app.ports.pauseScrobbleTimer.subscribe(pauseScrobbleTimer) - app.ports.play.subscribe(play) - app.ports.reloadAudioNodeIfNeeded.subscribe(reloadAudioNodeIfNeeded) - app.ports.renderAudioElements.subscribe(renderAudioElements) - app.ports.resetScrobbleTimer.subscribe(resetScrobbleTimer) - app.ports.seek.subscribe(seek) - app.ports.setMediaSessionArtwork.subscribe(setMediaSessionArtwork) - app.ports.setMediaSessionMetadata.subscribe(setMediaSessionMetadata) - app.ports.setMediaSessionPlaybackState.subscribe(setMediaSessionPlaybackState) - app.ports.setMediaSessionPositionState.subscribe(setMediaSessionPositionState) - app.ports.startScrobbleTimer.subscribe(startScrobbleTimer) -} - - - -// 🌳 - - -/** - * Javascript representation of `Queue.EngineItem` in Elm. - */ -type EngineItem = { - isCached: boolean - isPreload: boolean - progress: number | null - sourceId: string - trackId: string - trackTags: TrackTags - trackPath: string - url: string -} - - -/**/ -type TrackTags = { - disc: number - nr: number - - // Main - album: string | null - artist: string | null - title: string - - // Extra - genre: string | null - picture: string | null - year: number | null -} - - - -// Ports -// ----- - - -function adjustEqualizerSetting({ knob, value }: { knob: string, value: number }): void { - switch (knob) { - case "VOLUME": - Array.from( - document.body.querySelectorAll('#audio-elements audio[data-is-preload="false"]'), - ).forEach((audio) => ((audio as HTMLAudioElement).volume = value)) - break - } -} - - -function pause({ trackId }: { trackId: string }) { - withAudioNode(trackId, (audio) => audio.pause()) -} - - -function pauseScrobbleTimer() { - if (scrobbleTimer) scrobbleTimer.pause() -} - - -function play({ trackId, volume }: { trackId: string, volume: number }) { - withAudioNode(trackId, (audio) => { - audio.volume = volume - audio.muted = false - - if (audio.readyState === 0) audio.load() - if (!audio.isConnected) return - - const promise = audio.play() || Promise.resolve() - const didPreload = audio.getAttribute("data-did-preload") === "true" - const isPreload = audio.getAttribute("data-is-preload") === "true" - - if (didPreload && !isPreload) { - audio.removeAttribute("data-did-preload") - app.ports.audioDurationChange.send({ - trackId: audio.id, - duration: audio.duration, - }) - } - - promise.catch((e) => { - if (!audio.isConnected) return /* The node was removed from the DOM, we can ignore this error */ - const err = "Couldn't play audio automatically. Please resume playback manually." - console.error(err, e) - if (app) app.ports.fromAlien.send({ tag: "", data: null, error: err }) - }) - }) -} - - -async function reloadAudioNodeIfNeeded(args: { play: boolean, progress: number | null, trackId: string }) { - withAudioNode(args.trackId, (audio) => { - if (audio.readyState === 0 || audio.error?.code === 2) { - audio.load() - - if (args.progress) { - audio.setAttribute("data-initial-progress", JSON.stringify(args.progress)) - } - - if (args.play) { - play({ trackId: args.trackId, volume: audio.volume }) - } - } - }) -} - - -async function renderAudioElements(args: { - items: Array - play: string | null - volume: number -}) { - await render(args.items) - if (args.play) play({ trackId: args.play, volume: args.volume }) -} - - -function resetScrobbleTimer({ duration, trackId }: { duration: number, trackId: string }) { - const timestamp = Math.round(Date.now() / 1000) - const scrobbleTimeoutDuration = Math.min(240 + 0.5, duration / 1.95) - - if (scrobbleTimer) scrobbleTimer.stop() - - scrobbleTimer = new Timer({ - onend: () => { - scrobbleTimer = undefined - app.ports.scrobble.send({ - duration: Math.round(duration), - timestamp, - trackId, - }) - } - }) - - scrobbleTimer.start(scrobbleTimeoutDuration) -} - - -function seek({ percentage, trackId }: { percentage: number, trackId: string }) { - withAudioNode(trackId, (audio) => { - if (!isNaN(audio.duration)) { - audio.currentTime = audio.duration * percentage - } - }) -} - - -async function setMediaSessionArtwork({ blobUrl, imageType }: { blobUrl: string, imageType: string }) { - const artwork: MediaImage[] = [{ - src: blobUrl, - type: imageType - }] - - navigator.mediaSession.metadata = new MediaMetadata({ - title: navigator.mediaSession.metadata?.title, - artist: navigator.mediaSession.metadata?.artist, - album: navigator.mediaSession.metadata?.album, - artwork: artwork, - }) -} - - -async function setMediaSessionMetadata({ - album, - artist, - title, - - coverPrep, -}: { - album: string | null - artist: string | null - title: string - - coverPrep: CoverPrep | null -}) { - let artwork: MediaImage[] = [] - - if (coverPrep) { - const blob = await albumCover(coverPrep.cacheKey) - - artwork = blob && typeof blob !== "string" - ? [{ - src: URL.createObjectURL(blob), - type: blob.type - }] - : [] - - if (!blob) { - // Download artwork and set it later - loadAlbumCovers([coverPrep]) - } - } - - navigator.mediaSession.metadata = new MediaMetadata({ - title, - artist: artist || undefined, - album: album || undefined, - artwork: artwork, - }) -} - - -function setMediaSessionPlaybackState(state: MediaSessionPlaybackState) { - if (navigator.mediaSession) navigator.mediaSession.playbackState = state -} - - -function setMediaSessionPositionState({ - currentTime, - duration, -}: { - currentTime: number - duration: number -}) { - try { - navigator?.mediaSession?.setPositionState({ - duration: duration, - position: currentTime, - }) - } catch (_err) { - // - } -} - - -function startScrobbleTimer() { - if (scrobbleTimer) scrobbleTimer.start() -} - - - -// Media Keys -// ---------- - - -if ("mediaSession" in navigator) { - navigator.mediaSession.setActionHandler("play", () => { - app.ports.requestPlay.send(null) - }) - - navigator.mediaSession.setActionHandler("pause", () => { - app.ports.requestPause.send(null) - }) - - navigator.mediaSession.setActionHandler("previoustrack", () => { - app.ports.requestPrevious.send(null) - }) - - navigator.mediaSession.setActionHandler("nexttrack", () => { - app.ports.requestNext.send(null) - }) - - navigator.mediaSession.setActionHandler("seekbackward", (event) => { - const seekOffset = event.seekOffset || 10 - withActiveAudioNode( - (audio) => (audio.currentTime = Math.max(audio.currentTime - seekOffset, 0)), - ) - }) - - navigator.mediaSession.setActionHandler("seekforward", (event) => { - const seekOffset = event.seekOffset || 10 - withActiveAudioNode( - (audio) => (audio.currentTime = Math.min(audio.currentTime + seekOffset, audio.duration)), - ) - }) - - navigator.mediaSession.setActionHandler("seekto", (event) => { - withActiveAudioNode((audio) => (audio.currentTime = event.seekTime || audio.currentTime)) - }) -} - - - -// 🖼️ - - -async function render(items: Array) { - if (!container) { - container = document.createElement("div") - container.id = "audio-elements" - container.className = "absolute h-0 invisible left-0 pointer-events-none top-0 w-0" - - document.body.appendChild(container) - } - - const trackIds = items.map((e) => e.trackId) - const existingNodes = {} - - // Manage existing nodes - Array.from(container.querySelectorAll("audio")).map((node: HTMLAudioElement) => { - if (trackIds.includes(node.id)) { - existingNodes[node.id] = node - } else { - node.src = silentMp3File - container?.removeChild(node) - } - }) - - // Adjust existing and add new - await items.reduce(async (acc: Promise, item: EngineItem) => { - await acc - - const existingNode = existingNodes[item.trackId] - - if (existingNode) { - const isPreload = existingNode.getAttribute("data-is-preload") - if (isPreload === "true") existingNode.setAttribute("data-did-preload", "true") - - existingNode.setAttribute( - "data-is-preload", - item.isPreload ? "true" : "false", - ) - } else { - await createElement(item) - } - }, Promise.resolve()) -} - - -export async function createElement(item: EngineItem) { - const url = item.isCached - ? await db("tracks") - .getItem(item.trackId) - .then((blob) => (blob ? URL.createObjectURL(blob as Blob) : item.url)) - : await transformUrl(item.url, app) - - // Mime + SRC - const fileName = item.trackPath.split("/").reverse()[0] - const fileExtMatch = fileName.match(/\.(\w+)$/) - const fileExt = fileExtMatch && fileExtMatch[1] - const mime = fileExt ? mimeType(fileExt) : null - - const source = document.createElement("source") - if (mime) source.setAttribute("type", mime) - source.setAttribute("src", url) - - // Audio node - const audio = new Audio() - audio.setAttribute("id", item.trackId) - audio.setAttribute("crossorigin", "anonymous") - audio.setAttribute("data-initial-progress", JSON.stringify(item.progress)) - audio.setAttribute("data-is-preload", item.isPreload ? "true" : "false") - audio.setAttribute("muted", "true") - audio.setAttribute("preload", "auto") - audio.appendChild(source) - - audio.addEventListener("canplay", canplayEvent) - audio.addEventListener("durationchange", durationchangeEvent) - audio.addEventListener("ended", endedEvent) - audio.addEventListener("error", errorEvent) - audio.addEventListener("pause", pauseEvent) - audio.addEventListener("play", playEvent) - audio.addEventListener("suspend", suspendEvent) - audio.addEventListener("timeupdate", timeupdateEvent) - audio.addEventListener("waiting", debounce(1500, waitingEvent)) - - container?.appendChild(audio) -} - - - -// 🖼 ░░ EVENTS - - -function canplayEvent(event: Event) { - const target = event.target as HTMLAudioElement - - if (target.hasAttribute("data-initial-progress") && target.duration && !isNaN(target.duration)) { - const progress = JSON.parse(target.getAttribute("data-initial-progress") as string) - target.currentTime = target.duration * progress - target.removeAttribute("data-initial-progress") - } - - finishedLoading(event) -} - - -function durationchangeEvent(event: Event) { - const target = event.target as HTMLAudioElement - - if (!isNaN(target.duration)) { - app.ports.audioDurationChange.send({ - trackId: target.id, - duration: target.duration, - }) - } -} - -function endedEvent(event: Event) { - app.ports.audioEnded.send({ - trackId: (event.target as HTMLAudioElement).id, - }) -} - -function errorEvent(event: Event) { - const audio = event.target as HTMLAudioElement - - app.ports.audioError.send({ - trackId: audio.id, - code: audio.error?.code || 0 - }) -} - - -function pauseEvent(event: Event) { - app.ports.audioPlaybackStateChanged.send({ - trackId: (event.target as HTMLAudioElement).id, - isPlaying: false, - }) -} - - -function playEvent(event: Event) { - const audio = event.target as HTMLAudioElement - - app.ports.audioPlaybackStateChanged.send({ - trackId: audio.id, - isPlaying: true, - }) - - // In case audio was preloaded: - if (audio.readyState === 4) finishedLoading(event) -} - - -function suspendEvent(event: Event) { - finishedLoading(event) -} - - -function timeupdateEvent(event: Event) { - const target = event.target as HTMLAudioElement - - app.ports.audioTimeUpdated.send({ - trackId: target.id, - currentTime: target.currentTime, - duration: isNaN(target.duration) ? null : target.duration, - }) -} - - -function waitingEvent(event: Event) { - initiateLoading(event) -} - - - -// 🛠️ - - -function finishedLoading(event: Event) { - app.ports.audioHasLoaded.send({ - trackId: (event.target as HTMLAudioElement).id, - }) -} - - -function initiateLoading(event: Event) { - const audio = event.target as HTMLAudioElement - - if (audio.readyState < 4) - app.ports.audioIsLoading.send({ - trackId: audio.id, - }) -} - - -function withActiveAudioNode(fn: (node: HTMLAudioElement) => void): void { - const nonPreloadNodes: HTMLAudioElement[] = Array.from( - document.body.querySelectorAll(`#audio-elements audio[data-is-preload="false"]`), - ) - const playingNodes = nonPreloadNodes.filter((n) => n.paused === false) - const node = playingNodes.length ? playingNodes[0] : nonPreloadNodes[0] - if (node) fn(node) -} - - -function withAudioNode(trackId: string, fn: (node: HTMLAudioElement) => void): void { - const node = document.body.querySelector( - `#audio-elements audio[id="${trackId}"][data-is-preload="false"]`, - ) - if (node) fn(node as HTMLAudioElement) -} diff --git a/src/Javascript/UI/backdrop.ts b/src/Javascript/UI/backdrop.ts deleted file mode 100644 index ee5daff70..000000000 --- a/src/Javascript/UI/backdrop.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 🚀 - - -export function init(app) { - app.ports.pickAverageBackgroundColor.subscribe((src: string) => { - const avgColor = pickAverageBackgroundColor(src) - if (avgColor) app.ports.setAverageBackgroundColor.send(avgColor) - }) -} - - - -// 🛠️ - - -function averageColorOfImage(img: HTMLImageElement): { r: number, g: number, b: number } | null { - const canvas = document.createElement("canvas") - const ctx = canvas.getContext("2d") - canvas.width = img.naturalWidth - canvas.height = img.naturalHeight - - if (!ctx) return null - - ctx.drawImage(img, 0, 0) - - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) - const color = { r: 0, g: 0, b: 0 } - - for (let i = 0, l = imageData.data.length; i < l; i += 4) { - color.r += imageData.data[i] - color.g += imageData.data[i + 1] - color.b += imageData.data[i + 2] - } - - color.r = Math.floor(color.r / (imageData.data.length / 4)) - color.g = Math.floor(color.g / (imageData.data.length / 4)) - color.b = Math.floor(color.b / (imageData.data.length / 4)) - - return color -} - - -function pickAverageBackgroundColor(src: string): { r: number, g: number, b: number } | null { - const img = document.querySelector(`img[src$="${src}"]`) - return img ? averageColorOfImage(img as HTMLImageElement) : null -} diff --git a/src/Javascript/UI/brain.ts b/src/Javascript/UI/brain.ts deleted file mode 100644 index bfb6efa2e..000000000 --- a/src/Javascript/UI/brain.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { App } from "./elm/types" -import * as Tracks from "./tracks" - - -export async function load(): Promise { - const brain = new Worker( - "./js/brain/index.js#appHref=" + encodeURIComponent(window.location.href), - { type: "module" } - ) - - await new Promise((resolve, reject) => { - brain.onmessage = event => { - if (event.data.action === "READY") resolve(null) - } - - brain.addEventListener("error", () => { - reject("Failed to load web worker.
If you're using Firefox, you might need to upgrade your browser (version 113 and up) and set `dom.workers.modules.enabled` to `true` in `about:config`") - }) - }) - - // Fin - return brain -} - - -export function link({ app, brain }: { app: App, brain: Worker }) { - function handleAction(action, data, _ports) { - switch (action) { - case "DOWNLOAD_TRACKS": return Tracks.download(data) - } - } - - brain.onmessage = event => { - if (event.data.action) return handleAction(event.data.action, event.data.data, event.ports) - if (event.data.tag) app.ports.fromAlien.send(event.data) - } - - app.ports.toBrain.subscribe(a => brain.postMessage(a)) -} diff --git a/src/Javascript/UI/broadcast.ts b/src/Javascript/UI/broadcast.ts deleted file mode 100644 index 5e4be2118..000000000 --- a/src/Javascript/UI/broadcast.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function channel() { - const bc = new BroadcastChannel(`diffuse-${location.hostname}`) - - bc.addEventListener("message", event => { - switch (event.data) { - case "PING": return bc.postMessage("PONG") - } - }) - - return bc -} diff --git a/src/Javascript/UI/elm/types.ts b/src/Javascript/UI/elm/types.ts deleted file mode 100644 index d4ab9a503..000000000 --- a/src/Javascript/UI/elm/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type App = any // TODO: ElmApp - - -export type ElmPorts = { - // ← Elm - openUrlOnNewPage: PortFromElm - - // → Elm - fromAlien: PortToElm - indicateTouchDevice: PortToElm -} diff --git a/src/Javascript/UI/errors.ts b/src/Javascript/UI/errors.ts deleted file mode 100644 index b15472cd4..000000000 --- a/src/Javascript/UI/errors.ts +++ /dev/null @@ -1,20 +0,0 @@ -export function failure(text: string): void { - const note = document.createElement("div") - - note.className = "flex flex-col font-body items-center h-screen italic justify-center leading-relaxed px-4 text-center text-base text-white" - note.innerHTML = ` - - -

- ${text} -

- ` - - document.body.appendChild(note) - - // Remove loader - const elm = document.querySelector("#elm") - elm?.parentNode?.removeChild(elm) -} diff --git a/src/Javascript/UI/index.d.ts b/src/Javascript/UI/index.d.ts deleted file mode 100644 index 6e505a797..000000000 --- a/src/Javascript/UI/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ElmPorts } from "./elm/types" - -export { } - -declare global { - const BUILD_TIMESTAMP: string - - const Elm: { UI: ElmMain } - const tocca: any -} diff --git a/src/Javascript/UI/index.ts b/src/Javascript/UI/index.ts deleted file mode 100644 index a725f3d09..000000000 --- a/src/Javascript/UI/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -// -// | (• ◡•)| (❍ᴥ❍ʋ) -// -// The bit where we launch the Elm apps & workers, -// and connect the other bits to it. - -import "./pointer-events" - -import * as Application from "./application" -import * as Artwork from "./artwork" -import * as Audio from "./audio" -import * as Backdrop from "./backdrop" -import * as Brain from "./brain" -import * as Broadcast from "./broadcast" -import * as Errors from "./errors" -import * as Misc from "./misc" -import * as ServiceWorker from "./service-worker" -import * as Tracks from "./tracks" - - - -// 🌸 - - -const isNativeWrapper = !!globalThis.__TAURI__ - - - -// 🚀 - - -ServiceWorker - .load({ isNativeWrapper }) - .then(async (reg: ServiceWorkerRegistration) => { - const brain = await Brain.load() - const app = Application.load({ isNativeWrapper, reg }) - const channel = Broadcast.channel() - - // 🧑‍🏭 - ServiceWorker.link({ - app, isNativeWrapper, reg - }) - - // 🧠 - Brain.link({ - app, brain - }) - - // ⚡ - Application.init(app, channel) - Artwork.init(app, brain) - Audio.init(app) - Backdrop.init(app) - Misc.init(app) - Tracks.init(app) - }) - .catch( - Errors.failure - ) diff --git a/src/Javascript/UI/misc.ts b/src/Javascript/UI/misc.ts deleted file mode 100644 index 6da919e64..000000000 --- a/src/Javascript/UI/misc.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { App } from "./elm/types" - - -// 🏔️ - - -let app: App - - - -// 🚀 - - -export function init(a: App) { - app = a - - app.ports.copyToClipboard.subscribe(copyToClipboard) -} - - - -// Clipboard -// --------- - - -async function copyToClipboard(text: string) { - navigator.clipboard.writeText(text) -} - - - -// Focus -// ----- - -window.addEventListener("blur", event => { - if (app && event.target === window) app.ports.lostWindowFocus.send(null) -}) - - - -// Internet Connection -// ------------------- - -window.addEventListener("online", onlineStatusChanged) -window.addEventListener("offline", onlineStatusChanged) - - -function onlineStatusChanged() { - if (app) app.ports.setIsOnline.send(navigator.onLine) -} - - - -// Touch Device -// ------------ - -window.addEventListener("touchstart", function onFirstTouch() { - if (!app) return - app.ports.indicateTouchDevice.send() - window.removeEventListener("touchstart", onFirstTouch, false) -}, false) diff --git a/src/Javascript/UI/pointer-events.ts b/src/Javascript/UI/pointer-events.ts deleted file mode 100644 index 0a1522117..000000000 --- a/src/Javascript/UI/pointer-events.ts +++ /dev/null @@ -1,116 +0,0 @@ -import "./index.d" -import "tocca" - -// Pointer Events -// -------------- -// Thanks to https://github.com/mpizenberg/elm-pep/ - -let enteredElement - - -tocca({ - dbltapThreshold: 400, - tapThreshold: 250 -}) - - -function mousePointerEvent(eventType, mouseEvent) { - const pointerEvent: any = new MouseEvent(eventType, mouseEvent) - pointerEvent.pointerId = 1 - pointerEvent.isPrimary = true - pointerEvent.pointerType = "mouse" - pointerEvent.width = 1 - pointerEvent.height = 1 - pointerEvent.tiltX = 0 - pointerEvent.tiltY = 0 - - "buttons" in mouseEvent && mouseEvent.buttons !== 0 - ? (pointerEvent.pressure = 0.5) - : (pointerEvent.pressure = 0) - - return pointerEvent -} - - -function touchPointerEvent(eventType, touchEvent, touch) { - const pointerEvent: any = new CustomEvent(eventType, { - bubbles: true, - cancelable: true - }) - - pointerEvent.ctrlKey = touchEvent.ctrlKey - pointerEvent.shiftKey = touchEvent.shiftKey - pointerEvent.altKey = touchEvent.altKey - pointerEvent.metaKey = touchEvent.metaKey - - pointerEvent.clientX = touch.clientX - pointerEvent.clientY = touch.clientY - pointerEvent.screenX = touch.screenX - pointerEvent.screenY = touch.screenY - pointerEvent.pageX = touch.pageX - pointerEvent.pageY = touch.pageY - - const rect = touch.target.getBoundingClientRect() - pointerEvent.offsetX = touch.clientX - rect.left - pointerEvent.offsetY = touch.clientY - rect.top - pointerEvent.pointerId = 1 + touch.identifier - - pointerEvent.button = 0 - pointerEvent.buttons = 1 - pointerEvent.movementX = 0 - pointerEvent.movementY = 0 - pointerEvent.region = null - pointerEvent.relatedTarget = null - pointerEvent.x = pointerEvent.clientX - pointerEvent.y = pointerEvent.clientY - - pointerEvent.pointerType = "touch" - pointerEvent.width = 1 - pointerEvent.height = 1 - pointerEvent.tiltX = 0 - pointerEvent.tiltY = 0 - pointerEvent.pressure = 1 - pointerEvent.isPrimary = true - - return pointerEvent -} - - -// Simulate `pointerenter` and `pointerleave` event for non-touch devices -if (!self.PointerEvent) { - document.addEventListener("mouseover", event => { - const section = document.body.querySelector("section") - const isDragging = section && section.classList.contains("dragging-something") - const node = isDragging && document.elementFromPoint(event.clientX, event.clientY) - - if (node && node != enteredElement) { - enteredElement && enteredElement.dispatchEvent(mousePointerEvent("pointerleave", event)) - node.dispatchEvent(mousePointerEvent("pointerenter", event)) - enteredElement = node - } - }) -} - - -// Simulate `pointerenter` and `pointerleave` event for touch devices -document.body.addEventListener("touchmove", event => { - const section = document.body.querySelector("section") - const isDragging = section && section.classList.contains("dragging-something") - const touch = event.touches[0] - - let node - - if (isDragging && touch) { - node = document.elementFromPoint(touch.clientX, touch.clientY) - } - - if (node && node != enteredElement) { - enteredElement && enteredElement.dispatchEvent(touchPointerEvent("pointerleave", event, touch)) - node.dispatchEvent(touchPointerEvent("pointerenter", event, touch)) - enteredElement = node - } - - if (isDragging) { - event.stopPropagation() - } -}) diff --git a/src/Javascript/UI/service-worker.ts b/src/Javascript/UI/service-worker.ts deleted file mode 100644 index 77f3ce0a7..000000000 --- a/src/Javascript/UI/service-worker.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { App } from "./elm/types" - - -/** - * Load: - * - * 1. Redirect to HTTPS if using the `diffuse.sh` domain (subdomains included). - * 2. Fail if not a secure context. - * 3. Set up service worker, ensure it's ready and then continue initialisation. - */ -export async function load({ isNativeWrapper } : { isNativeWrapper: boolean }): Promise { - return new Promise((resolve, reject) => { - if (location.hostname.endsWith("diffuse.sh") && location.protocol === "http:") { - location.href = location.href.replace("http://", "https://") - reject("Just a moment, redirecting to HTTPS.") - - } else if (!self.isSecureContext) { - reject(` - This app only works on a secure context, HTTPS & localhost, and modern browsers. - `) - - } else if ("serviceWorker" in navigator) { - // Service worker - window.addEventListener("load", () => { - navigator.serviceWorker - .getRegistrations() - .then(async registrations => { - const serverIsOnline = navigator.onLine && await fetch(`${location.origin}?ping=1`) - .then(r => r.text()) - .then(a => a === "false" ? false : true) - - if (isNativeWrapper) await Promise.all( - registrations.map(r => r.unregister()) - ) - - if (serverIsOnline) return navigator.serviceWorker.register( - "service-worker.js", - // { type: "module" } - ) - - if (registrations[0]) return registrations - - throw new Error("Web server is offline") - }) - .then(() => navigator.serviceWorker.ready) - .then(resolve) - .catch(err => { - const isFirefox = navigator.userAgent.toLowerCase().includes("firefox") - - console.error(err) - return reject( - location.protocol === "https:" || location.hostname === "localhost" - ? "Failed to start the service worker." + (isFirefox ? " Make sure the setting Delete cookies and site data when Firefox is closed is off, or Diffuse's domain is added as an exception." : "") - : "Failed to start the service worker, try using HTTPS." - ) - }) - }) - - } - }) -} - - -/** - * Link. - */ -export function link( - { app, isNativeWrapper, reg } : { app: App, isNativeWrapper: boolean, reg: ServiceWorkerRegistration } -) { - if (reg.installing) console.log("🧑‍✈️ Service worker is installing") - const initialInstall = reg.installing - - initialInstall?.addEventListener("statechange", function() { - if (this.state === "activated") { - console.log("🧑‍✈️ Service worker is activated") - app.ports.installedNewServiceWorker.send(null) - } - }) - - if (reg.waiting) { - console.log("🧑‍✈️ A new version of Diffuse is available") - app.ports.installingNewServiceWorker.send(null) - app.ports.installedNewServiceWorker.send(null) - } - - if (initialInstall?.state === "activated") { - console.log("🧑‍✈️ Service worker is activated") - app.ports.installedNewServiceWorker.send(null) - } - - reg.addEventListener("updatefound", () => { - const newWorker = reg.installing - if (!newWorker) return - - // No worker was installed yet, so we'll only want to track the state changes - if (newWorker !== initialInstall) { - console.log("🧑‍✈️ A new version of Diffuse is available") - app.ports.installingNewServiceWorker.send(null) - } - - newWorker.addEventListener("statechange", (e: any) => { - console.log("🧑‍✈️ Service worker is", e.target.state) - if (e.target.state === "installed") app.ports.installedNewServiceWorker.send(null) - }) - }) - - // Check for service worker updates and every hour after that - if (!isNativeWrapper && navigator.onLine) { - reg.update() - setInterval(() => reg.update(), 1 * 1000 * 60 * 60) - } -} diff --git a/src/Javascript/UI/tracks.ts b/src/Javascript/UI/tracks.ts deleted file mode 100644 index 644f630e4..000000000 --- a/src/Javascript/UI/tracks.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { App } from "./elm/types" -import { fileExtension } from "../common" -import { transformUrl } from "../urls" - - -// 🏔️ - - -let app: App - - - -// 🚀 - - -export function init(a: App) { - app = a -} - - - -// 🛠️ - - -export async function download(group) { - const { saveAs } = await import("file-saver").then(a => a.default) - const JSZip = await import("jszip").then(a => a.default) - - const zip = new JSZip() - const folder = zip.folder("Diffuse - " + group.name) - if (!folder) throw new Error("Failed to create ZIP file") - - return group.tracks - .reduce((acc, track) => { - return acc - .then(() => transformUrl(track.url, app)) - .then(fetch) - .then((r: Response) => { - const mimeType = r.headers.get("content-type") - const fileExt = (mimeType ? fileExtension(mimeType) : null) || track.path.match(/\.(\w+)$/)[1] || "unknown-ext" - - return r.blob().then((b: Blob) => folder.file(track.filename + "." + fileExt, b)) - }) - }, Promise.resolve()) - .then(() => zip.generateAsync({ type: "blob" })) - .then((zipFile: Blob) => { - saveAs(zipFile, "Diffuse - " + group.name + ".zip") - app.ports.downloadTracksFinished.send(null) - }) -} diff --git a/src/Javascript/Workers/search.ts b/src/Javascript/Workers/search.ts deleted file mode 100644 index cdbc70850..000000000 --- a/src/Javascript/Workers/search.ts +++ /dev/null @@ -1,157 +0,0 @@ -// -// Search worker -// (◡ ‿ ◡ ✿) -// -// This worker is responsible for searching through a `Track` collection. - - -import lunr from "lunr" - - -const FIELDS = ["album", "artist", "title"] - - -lunr.Pipeline.registerFunction( - removeParenthesesFromToken, - "Remove parentheses from token" -) - - -let index: lunr.Index - - - -// Incoming messages -// ----------------- - -self.onmessage = (event: MessageEvent) => { - switch (event.data.action) { - case "PERFORM_SEARCH": - performSearch(event.data.data) - break - - case "UPDATE_SEARCH_INDEX": - updateSearchIndex(event.data.data) - break - } -} - - - -// Mapper -// ------ - -const mapTrack = track => ({ - id: track.id, - album: track.tags.album, - artist: track.tags.artist, - title: track.tags.title, -}) - - - -// Actions -// ------- - -function performSearch(rawSearchTerm: string) { - let results: string[] = - [] - - const searchTerm = rawSearchTerm - .replace(/-\s+/g, "-") - .replace(/\+\s+/g, "+") - .split(/ +/) - .reduce( - ([ acc, previousOperator, previousPrefix ]: [ string[], string, string ], chunk: string): [ string[], string, string ] => { - const operator = (a => a && a[0])( chunk.match(/^(\+|-)/) ) - - let chunkWithoutOperator = chunk.replace(/^(\+|-)/, "").replace(/\*$/, "").trim() - let prefix = (a => a && a[1])( chunkWithoutOperator.match(/^([^:]+:)/) ) - let chunkWithoutPrefix = chunkWithoutOperator.replace(/^([^:]+:)/, "") - - if (prefix && !FIELDS.includes(prefix.slice(0, -1))) { - prefix = null - chunkWithoutPrefix = chunkWithoutOperator.replace(":", "\\:") - chunkWithoutOperator = chunkWithoutPrefix - - } else if (prefix && chunkWithoutPrefix.includes(":")) { - chunkWithoutPrefix = chunkWithoutPrefix.replace(":", "\\:") - chunkWithoutOperator = prefix + chunkWithoutPrefix - - } - - const op = operator || previousOperator - const pr = prefix ? "" : (operator ? "" : previousPrefix) - - return chunkWithoutPrefix.trim().length > 0 - ? [ [ ...acc - , op + pr + chunkWithoutOperator - ] - , op - , prefix || pr - ] - : [ acc, previousOperator, previousPrefix ] - }, - [ [], "+", "" ] - )[0] - .join(" ") - - const searchTermWithAsteriks = - searchTerm - .split(" ") - .map(s => { - if (s.startsWith("-")) return s - return s + "*" - }) - .join(" ") - - if (index) { - results = index - .search(searchTerm) - .map(s => s.ref) - .concat( - index - .search(searchTermWithAsteriks) - .map(s => s.ref) - ) - } - - self.postMessage({ - action: "PERFORM_SEARCH", - data: results - }) -} - - -function updateSearchIndex(input: string | object[]) { - const tracks = (typeof input == "string") - ? JSON.parse(input) - : input - - index = customLunr((builder: lunr.Builder) => { - FIELDS.forEach( - field => builder.field(field) - ) - - ;(tracks || []) - .map(mapTrack) - .forEach(t => builder.add(t)) - }) -} - - - -function customLunr(fn: (b: lunr.Builder) => void) { - const builder = new lunr.Builder - - builder.pipeline.add(removeParenthesesFromToken, lunr.stemmer) - builder.searchPipeline.add(removeParenthesesFromToken, lunr.stemmer) - - fn(builder) - return builder.build() -} - - -function removeParenthesesFromToken(token: lunr.Token): lunr.Token { - return token.update(s => s.replace(/\(|\)/, "")) -} diff --git a/src/Javascript/Workers/service.ts b/src/Javascript/Workers/service.ts deleted file mode 100644 index b467b062d..000000000 --- a/src/Javascript/Workers/service.ts +++ /dev/null @@ -1,182 +0,0 @@ -// -// Service worker -// (◡ ‿ ◡ ✿) -// -// This worker is responsible for caching the application -// so it can be used offline and acts as a proxy that -// allows for example, authentication through headers -// when using audio elements. -// -/// - - -const KEY = - /* eslint-disable no-undef *//* @ts-ignore */ - `diffuse-${BUILD_TIMESTAMP}` - - -const EXCLUDE = - [ "_headers" - , "_redirects" - , "CORS" - ] - - -const GOOGLE_DRIVE = "https://www.googleapis.com/drive/" - - - -// 🙈 - - -const isNativeWrapper = location.host === "localhost:44999" || location.host === "127.0.0.1:44999" -let googleDriveToken - - - -// 📣 - - -self.addEventListener("activate", () => { - // Remove all caches except the one with the currently used `KEY` - caches.keys().then(keys => { - keys.forEach(k => { - if (k !== KEY) caches.delete(k) - }) - }) -}) - - -self.addEventListener("install", event => { - if (isNativeWrapper) { - return globalThis.skipWaiting() - } - - const href = self.location.href.replace("service-worker.js", "") - const promise = fetch("tree.json") - .then(response => response.json()) - .then(tree => { - const filteredTree = tree - .filter(t => !EXCLUDE.find(u => u === t)) - .filter(u => u.endsWith(".jpg")) - const whatToCache = [ href, `${href.replace(/\/+$/, "")}/about/` ].concat(filteredTree) - return caches.open(KEY).then(c => Promise.all(whatToCache.map(x => c.add(x)))) - }) - - event.waitUntil(promise) -}) - - -self.addEventListener("fetch", fetchEvent => { - const event = fetchEvent as FetchEvent - - const isInternal = - !!event.request.url.match(new RegExp("^" + self.location.origin)) - - // Ping - if (event.request.url.includes("?ping=1")) { - event.respondWith( - (async () => { - const serverIsOnline = await network(event).then(_ => true).catch(_ => false) - return new Response(JSON.stringify(serverIsOnline), { - headers: { "Content-Type": "application/json" } - }) - })() - ) - - // When doing a request with basic authentication in the url, put it in the headers instead - } else if (event.request.url.includes("basic_auth=")) { - const url = new URL(event.request.url) - const token = url.searchParams.get("basic_auth") - - event.respondWith(newRequestWithAuth( - event.request, - url.toString(), - "Basic " + token - )) - - // When doing a request with access token in the url, put it in the headers instead - } else if (event.request.url.includes("bearer_token=")) { - const url = new URL(event.request.url) - const token = url.searchParams.get("bearer_token") - - if (url.href.startsWith(GOOGLE_DRIVE)) googleDriveToken = token - - url.searchParams.delete("bearer_token") - url.search = "?" + url.searchParams.toString() - - event.respondWith(newRequestWithAuth( - event.request, - url.toString(), - "Bearer " + token - )) - - // Use cache if internal request and not using native app - } else if (isInternal) { - event.respondWith( - isNativeWrapper - ? network(event) - : cacheThenNetwork(event) - ) - - } else if (event.request.url && event.request.url.startsWith(GOOGLE_DRIVE) && event.request.url.includes("alt=media")) { - // For some reason Safari starts using the non bearer-token URL while playing audio - event.respondWith( - googleDriveToken - ? newRequestWithAuth( - event.request, - event.request.url.toString(), - "Bearer " + googleDriveToken - ) - : network(event) - ) - - } -}) - - -function cacheThenNetwork(event) { - const url = new URL(event.request.url) - url.search = "" - - return caches - .open(KEY) - .then(cache => cache.match(url)) - .then(match => match || fetch(url)) -} - - -function network(event) { - return fetch(event.request.url) -} - - -addEventListener("message", event => { - if (event.data === "skipWaiting") { - globalThis.skipWaiting() - } -}) - - - -// ⚗️ - - -function newRequestWithAuth(request: Request, newUrl: string, authToken: string): Promise { - const newHeaders = new Headers(request.headers) - newHeaders.append("authorization", authToken) - - const newRequest = new Request(request, { headers: newHeaders }) - - const makeFetch = () => fetch(newRequest).then(async r => { - if (r.ok) { - return r - } else { - return r.text().then(text => { - throw new Error(text) - }) - } - }) - - return makeFetch() -} diff --git a/src/Javascript/common.ts b/src/Javascript/common.ts deleted file mode 100644 index e4081a427..000000000 --- a/src/Javascript/common.ts +++ /dev/null @@ -1,67 +0,0 @@ -// -// Common stuff -// ʕ•ᴥ•ʔ - - -import * as localforage from "localforage" - - -// 🌳 - - -export type CoverPrep = { - cacheKey: string - trackFilename: string - trackPath: string - trackSourceId: string - variousArtists: string -} - - - -// FUNCTIONS - - -export function db(storeName: string = "main"): LocalForage { - return localforage.createInstance({ - name: "diffuse", - storeName - }) -} - - -export function fileExtension(mimeType: string): string | undefined { - const audioId = mimeType.toLowerCase().split("/")[ 1 ] - - switch (audioId) { - case "mp3": return "mp3"; - case "mpeg": return "mp3"; - - case "mp4a-latm": return "m4a"; - case "mp4": return "m4a"; - case "x-m4a": return "m4a"; - - case "flac": return "flac"; - case "x-flac": return "flac"; - case "ogg": return "ogg"; - case "opus": return "opus"; - - case "wav": return "wav"; - case "wave": return "wav"; - - case "webm": return "webm"; - } -} - - -export function mimeType(fileExt: string): string | undefined { - switch (fileExt) { - case "mp3": return "audio/mpeg"; - case "mp4": return "audio/mp4"; - case "m4a": return "audio/mp4"; - case "flac": return "audio/flac"; - case "ogg": return "audio/ogg"; - case "wav": return "audio/wave"; - case "webm": return "audio/webm"; - } -} diff --git a/src/Javascript/crypto.ts b/src/Javascript/crypto.ts deleted file mode 100644 index a00e63914..000000000 --- a/src/Javascript/crypto.ts +++ /dev/null @@ -1,83 +0,0 @@ -// -// Cryptography -// \ (•◡•) / -// -// Data encryption & decryption. - - -import * as Uint8arrays from "uint8arrays" - - -const extractable = false - - -export async function keyFromPassphrase(passphrase: string): Promise { - const baseKey = await crypto.subtle.importKey( - "raw", - Uint8arrays.fromString(passphrase, "utf8"), - { - name: "PBKDF2" - }, - false, - [ "deriveKey" ] - ) - - return await crypto.subtle.deriveKey( - { - name: "PBKDF2", - salt: Uint8arrays.fromString("diffuse", "utf8"), - iterations: 10000, - hash: "SHA-512" - }, - baseKey, - { - name: "AES-GCM", - length: 256 - }, - extractable, - [ "encrypt", "decrypt" ] - ) -} - - -export async function encrypt(key: CryptoKey, string: string): Promise { - const iv = crypto.getRandomValues(new Uint8Array(12)) - - const buf = await crypto.subtle.encrypt( - { - name: "AES-GCM", - iv: iv, - tagLength: 128 - }, - key, - Uint8arrays.fromString(string, "utf8") - ) - - const iv_b64 = Uint8arrays.toString(iv, "base64pad") - const buf_b64 = Uint8arrays.toString(new Uint8Array(buf), "base64pad") - return iv_b64 + buf_b64 -} - - -export async function decrypt(key: CryptoKey, string: string): Promise { - const iv_b64 = string.substring(0, 16) - const buf_b64 = string.substring(16) - - const iv = Uint8arrays.fromString(iv_b64, "base64pad") - const buf = Uint8arrays.fromString(buf_b64, "base64pad") - - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv: iv, - tagLength: 128 - }, - key, - buf - ) - - return Uint8arrays.toString( - new Uint8Array(decrypted), - "utf8" - ) -} diff --git a/src/Javascript/urls.ts b/src/Javascript/urls.ts deleted file mode 100644 index 5f9fd85f6..000000000 --- a/src/Javascript/urls.ts +++ /dev/null @@ -1,118 +0,0 @@ -// -// URLs -// \ (•◡•) / -// -// Some URLs are special you know. - - -const EXPIRED_ACCESS_TOKENS = { - GOOGLE: {} -} - - -export async function transformUrl(url, app) { - const parts = url.split("://") - - switch (parts[ 0 ]) { - - case "dropbox": { - const dropboxBits = parts[ 1 ].split("@") - const accessToken = dropboxBits[ 0 ] - const filePath = dropboxBits[ 1 ] - - return fetch( - "https://api.dropboxapi.com/2/files/get_temporary_link", - { - method: "POST" - , body: JSON.stringify({ path: filePath }) - , headers: new Headers({ - "Authorization": "Bearer " + accessToken, - "Content-Type": "application/json" - }) - } - ).then( - response => response.json() - ).then( - response => response.link - ) - } - - case "google": { - let finalAccessToken - - const googleBits = parts[ 1 ].split("@") - const [ accessToken, expiresAtString, refreshToken, clientId, clientSecret, srcId ] = googleBits[ 0 ].split(":") - const fileId = googleBits[ 1 ] - - // Unix timestamp in milliseconds - const inXminutes = Date.now() + 5 * 60 * 1000 // 5 minutes - const expiresAt = parseInt(expiresAtString, 10) - const isAlmostExpired = expiresAt <= inXminutes - - if (EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ]) { - const replacement = EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ] - - if (replacement.newExpiresAt <= inXminutes) { - finalAccessToken = await refreshGoogleAccessToken({ - app, clientId, clientSecret, refreshToken, srcId, oldToken: accessToken - }) - } else { - finalAccessToken = replacement.newToken - } - - } else if (isAlmostExpired) { - finalAccessToken = await refreshGoogleAccessToken({ - app, clientId, clientSecret, refreshToken, srcId, oldToken: accessToken - }) - - } else { - finalAccessToken = accessToken - - } - - return Promise.resolve( - `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media&bearer_token=${encodeURIComponent(finalAccessToken)}` - ) - } - - default: - return Promise.resolve(url) - - } -} - - - -// GOOGLE - - -async function refreshGoogleAccessToken({ app, clientId, clientSecret, oldToken, refreshToken, srcId }) { - console.log("🔐 Refreshing Google Drive access token") - - const url = new URL("https://www.googleapis.com/oauth2/v4/token") - - url.searchParams.set("client_id", clientId) - url.searchParams.set("client_secret", clientSecret) - url.searchParams.set("refresh_token", refreshToken) - url.searchParams.set("grant_type", "refresh_token") - - const serverResponse = await fetch(url, { method: "POST" }).then(r => r.json()) - const newToken = serverResponse.access_token - const newExpiresAt = Date.now() + (serverResponse.expires_in * 1000) - - EXPIRED_ACCESS_TOKENS.GOOGLE[ oldToken ] = { - oldToken, - newToken, - newExpiresAt, - refreshToken - } - - app.ports.refreshedAccessToken.send({ - service: "Google", - sourceId: srcId, - accessToken: newToken, - expiresAt: newExpiresAt - }) - - return serverResponse.access_token -} diff --git a/src/Library/Alfred.elm b/src/Library/Alfred.elm deleted file mode 100644 index 625e93839..000000000 --- a/src/Library/Alfred.elm +++ /dev/null @@ -1,110 +0,0 @@ -module Alfred exposing (..) - -import List.Extra as List -import Material.Icons.Types exposing (Coloring) -import Svg exposing (Svg) - - - --- 🌳 - - -type alias Alfred msg = - { action : Action msg - , focus : Int - , index : List (Group msg) - , indexFlattened : List (Item msg) - , message : String - , operation : Operation - , results : List (Group msg) - , searchTerm : Maybe String - } - - -type alias Action msg = - { result : Maybe (Item msg), searchTerm : Maybe String } -> List msg - - -type alias Group msg = - { name : Maybe String, items : List (Item msg) } - - -type alias Item msg = - { icon : Maybe (Coloring -> Svg msg) - , title : String - , value : ItemValue msg - } - - -type ItemValue msg - = Command msg - | StringValue String - - -type Operation - = Query - | QueryOrMutation - | Mutation - - - --- 🛳 - - -create : - { action : Action msg - , index : List (Group msg) - , message : String - , operation : Operation - } - -> Alfred msg -create { action, index, message, operation } = - { action = action - , focus = 0 - , index = index - , indexFlattened = List.concatMap .items index - , message = message - , operation = operation - , results = index - , searchTerm = Nothing - } - - - --- 🛠 - - -command : ItemValue msg -> Maybe msg -command val = - case val of - Command cmd -> - Just cmd - - StringValue _ -> - Nothing - - -stringValue : ItemValue msg -> Maybe String -stringValue val = - case val of - Command _ -> - Nothing - - StringValue string -> - Just string - - - --- 🛠 - - -getAt : Int -> Alfred msg -> Maybe (Item msg) -getAt index alfred = - alfred.results - |> List.concatMap .items - |> List.getAt index - - -length : Alfred msg -> Int -length { indexFlattened } = - List.length indexFlattened diff --git a/src/Library/Alien.elm b/src/Library/Alien.elm deleted file mode 100644 index ab08b6bfb..000000000 --- a/src/Library/Alien.elm +++ /dev/null @@ -1,200 +0,0 @@ -module Alien exposing (Event, Tag(..), broadcast, hostDecoder, report, tagDecoder, tagFromJson, tagFromString, tagToJson, tagToString, trigger) - -{-| 👽 Aliens. - -This involves the incoming and outgoing data. -Including the communication between the different Elm apps/workers. - --} - -import Enum exposing (Enum) -import Json.Decode -import Json.Encode - - - --- 🌳 - - -type alias Event = - { tag : String - , data : Json.Encode.Value - , error : Maybe String - } - - -type Tag - = EnclosedData - | SearchTracks - | SecretKey - | SyncLocal - | SyncMethod - ----------------------------------------- - -- from UI - ----------------------------------------- - | DownloadTracks - | ProcessSources - | RefreshedAccessToken - | RemoveEncryptionKey - | RemoveTracksBySourceId - | RemoveTracksFromCache - | SaveEnclosedUserData - | SaveFavourites - | SavePlaylists - | SaveProgress - | SaveSettings - | SaveSources - | SaveTracks - | SetSyncMethod - | StopProcessing - | StoreTracksInCache - | SyncHypaethralData - | SyncTrackTags - | ToCache - | UnsetSyncMethod - | UpdateEncryptionKey - ----------------------------------------- - -- to UI - ----------------------------------------- - | AddTracks - | FinishedProcessingSource - | FinishedProcessingSources - | FinishedSyncing - | GotCachedCover - | HideLoadingScreen - | LoadEnclosedUserData - | LoadHypaethralUserData - | ReloadTracks - | RemoveTracksByPath - | ReportError - | ReportProcessingError - | ReportProcessingProgress - | StartedSyncing - | UpdateSourceData - - -enum : Enum Tag -enum = - Enum.create - [ ( "ENCLOSED_DATA", EnclosedData ) - , ( "SEARCH_TRACKS", SearchTracks ) - , ( "SECRET_KEY", SecretKey ) - , ( "SYNC_LOCAL", SyncLocal ) - , ( "SYNC_METHOD", SyncMethod ) - - ----------------------------------------- - -- From UI - ----------------------------------------- - , ( "DOWNLOAD_TRACKS", DownloadTracks ) - , ( "PROCESS_SOURCES", ProcessSources ) - , ( "REFRESHED_ACCESS_TOKEN", RefreshedAccessToken ) - , ( "REMOVE_ENCRYPTION_KEY", RemoveEncryptionKey ) - , ( "REMOVE_TRACKS_BY_SOURCE_ID", RemoveTracksBySourceId ) - , ( "REMOVE_TRACKS_FROM_CACHE", RemoveTracksFromCache ) - , ( "SAVE_ENCLOSED_USER_DATA", SaveEnclosedUserData ) - , ( "SAVE_FAVOURITES", SaveFavourites ) - , ( "SAVE_PLAYLISTS", SavePlaylists ) - , ( "SAVE_PROGRESS", SaveProgress ) - , ( "SAVE_SETTINGS", SaveSettings ) - , ( "SAVE_SOURCES", SaveSources ) - , ( "SAVE_TRACKS", SaveTracks ) - , ( "SET_SYNC_METHOD", SetSyncMethod ) - , ( "STOP_PROCESSING", StopProcessing ) - , ( "STORE_TRACKS_IN_CACHE", StoreTracksInCache ) - , ( "SYNC_HYPAETHRAL_DATA", SyncHypaethralData ) - , ( "SYNC_TRACK_TAGS", SyncTrackTags ) - , ( "TO_CACHE", ToCache ) - , ( "UNSET_SYNC_METHOD", UnsetSyncMethod ) - , ( "UPDATE_ENCRYPTION_KEY", UpdateEncryptionKey ) - - ----------------------------------------- - -- To UI - ----------------------------------------- - , ( "ADD_TRACKS", AddTracks ) - , ( "FINISHED_PROCESSING_SOURCE", FinishedProcessingSource ) - , ( "FINISHED_PROCESSING_SOURCES", FinishedProcessingSources ) - , ( "GOT_CACHED_COVER", GotCachedCover ) - , ( "HIDE_LOADING_SCREEN", HideLoadingScreen ) - , ( "LOAD_ENCLOSED_USER_DATA", LoadEnclosedUserData ) - , ( "LOAD_HYPAETHRAL_USER_DATA", LoadHypaethralUserData ) - , ( "RELOAD_TRACKS", ReloadTracks ) - , ( "REMOVE_TRACKS_BY_PATH", RemoveTracksByPath ) - , ( "REPORT_ERROR", ReportError ) - , ( "REPORT_PROCESSING_ERROR", ReportProcessingError ) - , ( "REPORT_PROCESSING_PROGRESS", ReportProcessingProgress ) - , ( "STARTED_SYNCING", StartedSyncing ) - , ( "UPDATE_SOURCE_DATA", UpdateSourceData ) - ] - - - --- 🔱 - - -broadcast : Tag -> Json.Encode.Value -> Event -broadcast tag data = - { tag = tagToString tag - , data = data - , error = Nothing - } - - -report : Tag -> String -> Event -report tag error = - { tag = tagToString tag - , data = Json.Encode.null - , error = Just error - } - - -trigger : Tag -> Event -trigger tag = - { tag = tagToString tag - , data = Json.Encode.null - , error = Nothing - } - - -tagDecoder : Json.Decode.Decoder Tag -tagDecoder = - enum.decoder - - -tagToJson : Tag -> Json.Encode.Value -tagToJson = - enum.encode - - -tagToString : Tag -> String -tagToString = - enum.toString - - -tagFromJson : Json.Decode.Value -> Result Json.Decode.Error Tag -tagFromJson = - Json.Decode.decodeValue enum.decoder - - -tagFromString : String -> Maybe Tag -tagFromString = - enum.fromString - - - --- ⚗️ - - -{-| Decoder for an alien event inside another alient event. --} -hostDecoder : Json.Decode.Decoder Event -hostDecoder = - Json.Decode.map3 - (\tag data error -> - { tag = tag - , data = data - , error = error - } - ) - (Json.Decode.field "tag" Json.Decode.string) - (Json.Decode.field "data" Json.Decode.value) - (Json.Decode.field "error" <| Json.Decode.maybe Json.Decode.string) diff --git a/src/Library/Chunky.elm b/src/Library/Chunky.elm deleted file mode 100644 index 0f1f09c85..000000000 --- a/src/Library/Chunky.elm +++ /dev/null @@ -1,97 +0,0 @@ -module Chunky exposing (brick, bricky, chunk, chunky, inline, lineBreak, nothing, raw, rawy, slab, slaby) - -{-| Chunks, blocks and slabs. - -Convenience functions to build UIs with composable CSS classes. - --} - -import Html exposing (Html) -import Html.Attributes as A - - - --- 1 - - -slab : - (List (Html.Attribute msg) -> List (Html msg) -> Html msg) - -> List (Html.Attribute msg) - -> List String - -> List (Html msg) - -> Html msg -slab typ attributes stylingAttributes children = - typ - (A.class (String.join " " stylingAttributes) :: attributes) - children - - -slaby : - (List (Html.Attribute msg) -> List (Html msg) -> Html msg) - -> List (Html.Attribute msg) - -> List String - -> Html msg - -> Html msg -slaby a b c = - List.singleton >> slab a b c - - - --- 2 - - -brick : List (Html.Attribute msg) -> List String -> List (Html msg) -> Html msg -brick = - slab Html.div - - -bricky : List (Html.Attribute msg) -> List String -> Html msg -> Html msg -bricky a b = - List.singleton >> brick a b - - - --- 3 - - -chunk : List String -> List (Html msg) -> Html msg -chunk = - brick [] - - -chunky : List String -> Html msg -> Html msg -chunky a = - List.singleton >> chunk a - - -inline : List String -> List (Html msg) -> Html msg -inline = - slab Html.span [] - - - --- 4 - - -raw : List (Html msg) -> Html msg -raw = - chunk [] - - -rawy : Html msg -> Html msg -rawy = - List.singleton >> raw - - - --- 5 - - -nothing : Html msg -nothing = - Html.text "" - - -lineBreak : Html msg -lineBreak = - Html.br [] [] diff --git a/src/Library/Common.elm b/src/Library/Common.elm deleted file mode 100644 index 9e74a2df5..000000000 --- a/src/Library/Common.elm +++ /dev/null @@ -1,97 +0,0 @@ -module Common exposing (ServiceWorkerStatus(..), Switch(..), backToIndex, boolFromString, boolToString, queryString, translateHttpResponse, urlOrigin) - -import Http -import Tuple.Ext as Tuple -import Url exposing (Protocol(..), Url) -import Url.Builder as Url - - - --- ⛩ - - -backToIndex : String -backToIndex = - "Back to tracks" - - - --- 🌳 - - -type ServiceWorkerStatus - = InstallingInitial - | InstallingNew - | WaitingForActivation - | Activated - - -type Switch - = On - | Off - - - --- 🔱 - - -boolFromString : String -> Bool -boolFromString string = - case string of - "t" -> - True - - _ -> - False - - -boolToString : Bool -> String -boolToString bool = - if bool then - "t" - - else - "f" - - -queryString : List ( String, String ) -> String -queryString = - List.map (Tuple.uncurry Url.string) >> Url.toQuery - - -translateHttpResponse : Http.Response String -> Result Http.Error String -translateHttpResponse response = - case response of - Http.BadUrl_ u -> - Err (Http.BadUrl u) - - Http.Timeout_ -> - Err Http.Timeout - - Http.NetworkError_ -> - Err Http.NetworkError - - Http.BadStatus_ _ body -> - Err (Http.BadBody body) - - Http.GoodStatus_ _ body -> - Ok body - - -urlOrigin : Url -> String -urlOrigin { host, port_, path, protocol } = - let - scheme = - case protocol of - Http -> - "http://" - - Https -> - "https://" - - thePort = - port_ - |> Maybe.map (String.fromInt >> (++) ":") - |> Maybe.withDefault "" - in - scheme ++ host ++ thePort ++ path diff --git a/src/Library/Conditional.elm b/src/Library/Conditional.elm deleted file mode 100644 index 03bd32dc3..000000000 --- a/src/Library/Conditional.elm +++ /dev/null @@ -1,12 +0,0 @@ -module Conditional exposing (ifThenElse) - --- 🔱 - - -ifThenElse : Bool -> a -> a -> a -ifThenElse bool x y = - if bool then - x - - else - y diff --git a/src/Library/ContextMenu.elm b/src/Library/ContextMenu.elm deleted file mode 100644 index c517659d2..000000000 --- a/src/Library/ContextMenu.elm +++ /dev/null @@ -1,54 +0,0 @@ -module ContextMenu exposing (ContextMenu(..), Item(..), ItemProperties, anyItem, coordinates, justAnItem) - -import Coordinates exposing (Coordinates) -import Material.Icons.Types exposing (Coloring) -import Svg exposing (Svg) - - - --- 🌳 - - -type ContextMenu msg - = ContextMenu (List (Item msg)) Coordinates - - -type Item msg - = Item (ItemProperties msg) - | Divider - - -type alias ItemProperties msg = - { icon : Int -> Coloring -> Svg msg - , label : String - , msg : msg - , active : Bool - } - - - --- 🔱 - - -anyItem : (ItemProperties msg -> Bool) -> ContextMenu msg -> Bool -anyItem fn (ContextMenu items _) = - List.any - (\item -> - case item of - Item props -> - fn props - - Divider -> - False - ) - items - - -coordinates : ContextMenu msg -> Coordinates -coordinates (ContextMenu _ c) = - c - - -justAnItem : ItemProperties msg -> Maybe (Item msg) -justAnItem = - Just << Item diff --git a/src/Library/Coordinates.elm b/src/Library/Coordinates.elm deleted file mode 100644 index 0278081bc..000000000 --- a/src/Library/Coordinates.elm +++ /dev/null @@ -1,29 +0,0 @@ -module Coordinates exposing (..) - --- 🌳 - - -type alias Coordinates = - { x : Float, y : Float } - - -type alias Viewport = - { height : Float - , width : Float - } - - - --- 🔱 - - -fromTuple : ( Float, Float ) -> Coordinates -fromTuple ( x, y ) = - { x = x - , y = y - } - - -toTuple : Coordinates -> ( Float, Float ) -toTuple { x, y } = - ( x, y ) diff --git a/src/Library/Cryptography/Hmac.elm b/src/Library/Cryptography/Hmac.elm deleted file mode 100644 index 944a4919f..000000000 --- a/src/Library/Cryptography/Hmac.elm +++ /dev/null @@ -1,139 +0,0 @@ -module Cryptography.Hmac exposing (encrypt128, encrypt64) - -{-| Cryptography – HMAC --} - -import Binary exposing (Bits) - - -type alias HashFunction = - Bits -> Bits - - -{-| HMAC encryption for hashing algorithms with a `blockSize` of 64. -These include: SHA-0, SHA-1, SHA-224, SHA-256, MD5, etc. - - >>> import Binary - >>> import SHA - - >>> Binary.fromStringAsUtf8 "" - ..> |> encrypt64 SHA.sha256 "" - ..> |> Binary.toHex - ..> |> String.toLower - "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad" - - >>> Binary.fromStringAsUtf8 "key" - ..> |> encrypt64 SHA.sha256 "The quick brown fox jumps over the lazy dog" - ..> |> Binary.toHex - ..> |> String.toLower - "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" - - >>> Binary.fromHex "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b" - ..> |> encrypt64 SHA.sha256 "Hi There" - ..> |> Binary.toHex - ..> |> String.toLower - "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" - - >>> Binary.fromHex "4a656665" - ..> |> encrypt64 SHA.sha256 "what do ya want for nothing?" - ..> |> Binary.toHex - ..> |> String.toLower - "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" - - >>> Binary.fromHex "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ..> |> encrypt64 SHA.sha256 "Test Using Larger Than Block-Size Key - Hash Key First" - ..> |> Binary.toHex - ..> |> String.toLower - "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54" - --} -encrypt64 : HashFunction -> String -> Bits -> Bits -encrypt64 = - encrypt (64 * 8) - - -{-| HMAC encryption for hashing algorithms with a `blockSize` of 128. -These include: SHA-384, SHA-512, etc. --} -encrypt128 : HashFunction -> String -> Bits -> Bits -encrypt128 = - encrypt (128 * 8) - - - --- ENCRYPT - - -encrypt : Int -> HashFunction -> String -> Bits -> Bits -encrypt blockSize hash messageString key = - let - keySize = - Binary.width key - - keyWithBlockSize = - if keySize > blockSize then - padRight blockSize (hash key) - - else if keySize < blockSize then - padRight blockSize key - - else - key - - ( binSeqOne, binSeqTwo ) = - Tuple.mapBoth - (Binary.xor keyWithBlockSize) - (Binary.xor keyWithBlockSize) - (padding <| blockSize // 8) - in - messageString - |> Binary.fromString 8 - |> Binary.append binSeqOne - |> hash - |> Binary.append binSeqTwo - |> hash - - -padRight : Int -> Bits -> Bits -padRight int bits = - let - size = - Binary.width bits - in - False - |> List.repeat (int - size) - |> List.append (Binary.toBooleans bits) - |> Binary.fromBooleans - - - --- PADDING - - -padding : Int -> ( Bits, Bits ) -padding blockSize = - case blockSize of - 64 -> - padding64 - - 128 -> - padding128 - - _ -> - ( Binary.concat (List.repeat blockSize <| Binary.fromHex "36") - , Binary.concat (List.repeat blockSize <| Binary.fromHex "5C") - ) - - -padding64 : ( Bits, Bits ) -padding64 = - ( Binary.concat (List.repeat 64 <| Binary.fromHex "36") - , Binary.concat (List.repeat 64 <| Binary.fromHex "5C") - ) - - -padding128 : ( Bits, Bits ) -padding128 = - ( Binary.concat (List.repeat 128 <| Binary.fromHex "36") - , Binary.concat (List.repeat 128 <| Binary.fromHex "5C") - ) diff --git a/src/Library/Dict/Ext.elm b/src/Library/Dict/Ext.elm deleted file mode 100644 index 82be3a63e..000000000 --- a/src/Library/Dict/Ext.elm +++ /dev/null @@ -1,24 +0,0 @@ -module Dict.Ext exposing (fetch, fetchUnknown, unionFlipped) - -import Dict exposing (Dict) - - - --- 🔱 - - -unionFlipped : Dict comparable v -> Dict comparable v -> Dict comparable v -unionFlipped a b = - Dict.union b a - - -fetch : comparable -> v -> Dict comparable v -> v -fetch key default dict = - dict - |> Dict.get key - |> Maybe.withDefault default - - -fetchUnknown : comparable -> Dict comparable String -> String -fetchUnknown key dict = - fetch key "MISSING_VALUE" dict diff --git a/src/Library/Equalizer.elm b/src/Library/Equalizer.elm deleted file mode 100644 index 83ffdf1b2..000000000 --- a/src/Library/Equalizer.elm +++ /dev/null @@ -1,68 +0,0 @@ -module Equalizer exposing (..) - -import Coordinates exposing (Coordinates) -import Json.Decode -import Json.Encode - - - --- 🌳 - - -type Knob - = Low - | Mid - | High - | Volume - - -type alias KnobOperation = - { knob : Knob - , startingPosition : Coordinates - } - - -type alias Settings = - { low : Float - , mid : Float - , high : Float - , volume : Float - } - - -maxAngle : Float -maxAngle = - 135 - - - --- 🔱 - - -defaultSettings : Settings -defaultSettings = - { low = 0 - , mid = 0 - , high = 0 - , volume = 0.5 - } - - -encodeSettings : Settings -> Json.Encode.Value -encodeSettings settings = - Json.Encode.object - [ ( "low", Json.Encode.float settings.low ) - , ( "mid", Json.Encode.float settings.mid ) - , ( "high", Json.Encode.float settings.high ) - , ( "volume", Json.Encode.float settings.volume ) - ] - - -settingsDecoder : Json.Decode.Decoder Settings -settingsDecoder = - Json.Decode.map4 - Settings - (Json.Decode.field "low" Json.Decode.float) - (Json.Decode.field "mid" Json.Decode.float) - (Json.Decode.field "high" Json.Decode.float) - (Json.Decode.field "volume" Json.Decode.float) diff --git a/src/Library/Html/Ext.elm b/src/Library/Html/Ext.elm deleted file mode 100644 index d20b13429..000000000 --- a/src/Library/Html/Ext.elm +++ /dev/null @@ -1,50 +0,0 @@ -module Html.Ext exposing (..) - -import Html exposing (Attribute, Html) -import Html.Events exposing (keyCode, on, preventDefaultOn, stopPropagationOn) -import Json.Decode as Json - - -lineBreak : Html msg -lineBreak = - Html.br [] [] - - -onClickStopPropagation : msg -> Attribute msg -onClickStopPropagation msg = - stopPropagationOn "click" (Json.succeed ( msg, True )) - - -onDoubleTap : msg -> Attribute msg -onDoubleTap msg = - on "dbltap" (Json.succeed msg) - - -onEnterKey : msg -> Attribute msg -onEnterKey msg = - on "keydown" (Json.andThen (ifEnterKey msg) keyCode) - - -ifEnterKey : msg -> Int -> Json.Decoder msg -ifEnterKey msg key = - case key of - 13 -> - Json.succeed msg - - _ -> - Json.fail "Another key, that isn't enter, was pressed" - - -onTap : msg -> Attribute msg -onTap msg = - on "tap" (Json.succeed msg) - - -onTapPreventDefault : msg -> Attribute msg -onTapPreventDefault msg = - preventDefaultOn "tap" (Json.succeed ( msg, True )) - - -onTapStopPropagation : msg -> Attribute msg -onTapStopPropagation msg = - stopPropagationOn "tap" (Json.succeed ( msg, True )) diff --git a/src/Library/Http/Ext.elm b/src/Library/Http/Ext.elm deleted file mode 100644 index 0cf925ec1..000000000 --- a/src/Library/Http/Ext.elm +++ /dev/null @@ -1,27 +0,0 @@ -module Http.Ext exposing (errorToString) - -import Http exposing (Error(..)) - - - --- 🛠 - - -errorToString : Http.Error -> String -errorToString err = - -- Thanks to: https://github.com/hercules-ci/elm-hercules-extras/blob/1.0.0/src/Http/Extras.elm - case err of - Timeout -> - "Timeout exceeded" - - NetworkError -> - "Network error" - - BadStatus code -> - "Something went wrong, got status code: " ++ String.fromInt code - - BadBody text -> - "Unexpected response: " ++ text - - BadUrl url -> - "Malformed url: " ++ url diff --git a/src/Library/Icons.elm b/src/Library/Icons.elm deleted file mode 100644 index 40fd078c0..000000000 --- a/src/Library/Icons.elm +++ /dev/null @@ -1,25 +0,0 @@ -module Icons exposing (..) - -import Chunky exposing (slaby) -import Html -import Material.Icons.Types exposing (Coloring) -import VirtualDom - - - --- 🌳 - - -type alias Icon msg = - Int -> Coloring -> VirtualDom.Node msg - - - --- 🔱 - - -wrapped : List String -> Icon msg -> Int -> Coloring -> VirtualDom.Node msg -wrapped classes icon size coloring = - coloring - |> icon size - |> slaby Html.span [] classes diff --git a/src/Library/Json/Decode/Ext.elm b/src/Library/Json/Decode/Ext.elm deleted file mode 100644 index 20a20a150..000000000 --- a/src/Library/Json/Decode/Ext.elm +++ /dev/null @@ -1,28 +0,0 @@ -module Json.Decode.Ext exposing (listIgnore, optionalField) - -import Json.Decode as Decode exposing (Decoder) -import Maybe.Extra as Maybe - - - --- 🔱 - - -{-| A list decoder that always succeeds, throwing away the failures. --} -listIgnore : Decoder a -> Decoder (List a) -listIgnore decoder = - decoder - |> Decode.maybe - |> Decode.list - |> Decode.map Maybe.values - - -{-| Provide a default value for a field that might not be there. --} -optionalField : String -> Decoder a -> a -> Decoder a -optionalField field decoder defaultValue = - decoder - |> Decode.field field - |> Decode.maybe - |> Decode.map (Maybe.withDefault defaultValue) diff --git a/src/Library/Json/Encode/Ext.elm b/src/Library/Json/Encode/Ext.elm deleted file mode 100644 index cb768bf68..000000000 --- a/src/Library/Json/Encode/Ext.elm +++ /dev/null @@ -1,10 +0,0 @@ -module Json.Encode.Ext exposing (..) - -import Json.Encode as Encode - - -encodeMaybe : Maybe a -> (a -> Encode.Value) -> Encode.Value -encodeMaybe maybe encoder = - maybe - |> Maybe.map encoder - |> Maybe.withDefault Encode.null diff --git a/src/Library/LastFm.elm b/src/Library/LastFm.elm deleted file mode 100644 index d9280ded0..000000000 --- a/src/Library/LastFm.elm +++ /dev/null @@ -1,200 +0,0 @@ -module LastFm exposing (..) - -import Common -import Http -import Json.Decode as Json -import List.Ext as List -import MD5 -import String.Ext as String -import Tracks exposing (Track) -import Tuple.Ext as Tuple -import Url exposing (Url) -import Url.Ext as Url - - - --- 🏔 - - -apiKey = - "4f0fe85b67baef8bb7d008a8754a95e5" - - -apiUrl = - "https://ws.audioscrobbler.com/2.0" - - -notSoSecret = - "0cec3ca0f58e04a5082f1131aba1e0d3" - - - --- 🌳 - - -type alias Model = - { authenticating : Bool - , sessionKey : Maybe String - } - - -initialModel : Model -initialModel = - { authenticating = False - , sessionKey = Nothing - } - - -authenticationCommand : (Result Http.Error String -> msg) -> Url -> Cmd msg -authenticationCommand msg url = - case Url.extractQueryParam "token" url of - Just token -> - Http.get - { url = - authenticatedUrl - [ ( "method", "auth.getSession" ) - , ( "token", token ) - ] - , expect = - Json.string - |> Json.at [ "session", "key" ] - |> Http.expectJson msg - } - - Nothing -> - Cmd.none - - - --- 📣 - - -disconnect : Model -> Model -disconnect model = - { model | sessionKey = Nothing } - - -failedToAuthenticate : Model -> Model -failedToAuthenticate model = - { model | authenticating = False } - - -gotSessionKey : String -> Model -> Model -gotSessionKey sessionKey model = - { model | sessionKey = Just sessionKey } - - - --- 🎵 - - -nowPlaying : Model -> { duration : Int, msg : msg, track : Track } -> Cmd msg -nowPlaying model { duration, msg, track } = - case model.sessionKey of - Just sessionKey -> - Http.post - { url = - apiUrl - , body = - [ ( "duration", String.fromInt duration ) - , ( "track", track.tags.title ) - , ( "trackNumber", String.fromInt track.tags.nr ) - - -- - , ( "method", "track.updateNowPlaying" ) - , ( "sk", sessionKey ) - ] - |> addAlbum track - |> addArtist track - |> authenticatedBody - , expect = - Http.expectWhatever (always msg) - } - - Nothing -> - Cmd.none - - -scrobble : Model -> { duration : Int, msg : msg, timestamp : Int, track : Track } -> Cmd msg -scrobble model { duration, msg, timestamp, track } = - case model.sessionKey of - Just sessionKey -> - Http.post - { url = - apiUrl - , body = - [ ( "duration", String.fromInt duration ) - , ( "track", track.tags.title ) - , ( "trackNumber", String.fromInt track.tags.nr ) - - -- - , ( "method", "track.scrobble" ) - , ( "sk", sessionKey ) - , ( "timestamp", String.fromInt timestamp ) - ] - |> addAlbum track - |> addArtist track - |> authenticatedBody - , expect = - Http.expectWhatever (always msg) - } - - Nothing -> - Cmd.none - - -addAlbum track list = - case track.tags.album of - Just album -> - ( "album", album ) :: list - - Nothing -> - list - - -addArtist track list = - case track.tags.artist of - Just artist -> - ( "artist", artist ) :: list - - Nothing -> - list - - - --- 🔱 - - -authenticatedBody : List ( String, String ) -> Http.Body -authenticatedBody params = - params - |> authenticatedParams - |> Common.queryString - |> String.dropLeft 1 - |> Http.stringBody "application/x-www-form-urlencoded" - - -authenticatedUrl : List ( String, String ) -> String -authenticatedUrl params = - params - |> authenticatedParams - |> Common.queryString - |> String.append apiUrl - - -authenticatedParams : List ( String, String ) -> List ( String, String ) -authenticatedParams params = - let - extendedParams = - ( "api_key", apiKey ) :: params - in - extendedParams - |> List.sortBy Tuple.first - |> List.map (Tuple.uncurry String.append) - |> String.concat - |> String.addSuffix notSoSecret - |> MD5.hex - |> Tuple.pair "api_sig" - |> List.addTo extendedParams - |> (::) ( "format", "json" ) - |> List.sortBy Tuple.first diff --git a/src/Library/Lens/Ext.elm b/src/Library/Lens/Ext.elm deleted file mode 100644 index ee39cd285..000000000 --- a/src/Library/Lens/Ext.elm +++ /dev/null @@ -1,17 +0,0 @@ -module Lens.Ext exposing (..) - -import Monocle.Lens as Lens exposing (Lens) - - -{-| Flipped version of `Lens.modify`. --} -adjust : Lens a b -> a -> (b -> b) -> a -adjust lens a fn = - Lens.modify lens fn a - - -{-| Flipped version of `lens.set`. --} -replace : Lens a b -> a -> b -> a -replace lens a b = - lens.set b a diff --git a/src/Library/List/Ext.elm b/src/Library/List/Ext.elm deleted file mode 100644 index 04b76488c..000000000 --- a/src/Library/List/Ext.elm +++ /dev/null @@ -1,75 +0,0 @@ -module List.Ext exposing (..) - -import List.Extra as List - - -{-| Flipped version of `append`. - - >>> add [2, 3] [1] - [1, 2, 3] - --} -add : List a -> List a -> List a -add a b = - List.append b a - - -{-| Flipped version of (::). - - >>> addTo [2, 3] 1 - [1, 2, 3] - --} -addTo : List a -> a -> List a -addTo list item = - item :: list - - -{-| Move an item "from" an index "to" another index. -Putting the item in front of the `to` index. - - >>> move { from = 0, to = 2, amount = 1 } [1, 2, 3] - [2, 1, 3] - - >>> move { from = 2, to = 0, amount = 1 } [1, 2, 3] - [3, 1, 2] - - >>> move { from = 2, to = 7, amount = 3 } [0, 1, 2, 3, 4, 5, 6, 7] - [0, 1, 5, 6, 2, 3, 4, 7] - - >>> move { from = 2, to = 1, amount = 3 } [0, 1, 2, 3, 4, 5, 6, 7] - [0, 2, 3, 4, 1, 5, 6, 7] - --} -move : { amount : Int, from : Int, to : Int } -> List a -> List a -move { from, to, amount } list = - [] - ++ (list |> List.take (min from to)) - ++ (list |> List.take to |> List.drop (from + amount)) - ++ (list |> List.drop from |> List.take amount) - ++ (list |> List.take from |> List.drop to) - ++ (list |> List.drop (max (from + amount) to)) - - -pickIndexes : List Int -> List a -> List a -pickIndexes indexes items = - List.foldr - (\idx acc -> - items - |> List.getAt idx - |> Maybe.map (addTo acc) - |> Maybe.withDefault acc - ) - [] - indexes - - -{-| Exclude a list from another list. - - >>> without [ 2 ] [ 1, 2, 3 ] - [ 1, 3 ] - --} -without : List a -> List a -> List a -without exclude = - List.filter (\c -> List.notMember c exclude) diff --git a/src/Library/Management.elm b/src/Library/Management.elm deleted file mode 100644 index 01efbde84..000000000 --- a/src/Library/Management.elm +++ /dev/null @@ -1,43 +0,0 @@ -module Management exposing (..) - -import Monocle.Lens exposing (Lens) - - - --- 📣 - - -type alias Manager msg model = - model -> ( model, Cmd msg ) - - - --- 🔱 - - -{-| For working with nested models. - - organize : Manager Msg NestedModel -> Manager Msg Model - organize = - { get = .nested - , set = \nested ui -> { ui | nested = nested } - } - |> Monocle.Lens.Lens - |> Management.organize - - update : Nested.Msg -> Manager Msg Model - update msg = - case msg of - NestedMsg -> - organize handleNestedMsg - --} -organize : - Lens parent nested - -> Manager msg nested - -> Manager msg parent -organize lens manager parent = - parent - |> lens.get - |> manager - |> Tuple.mapFirst (\nested -> lens.set nested parent) diff --git a/src/Library/Maybe/Ext.elm b/src/Library/Maybe/Ext.elm deleted file mode 100644 index 298a13229..000000000 --- a/src/Library/Maybe/Ext.elm +++ /dev/null @@ -1,13 +0,0 @@ -module Maybe.Ext exposing (preferFirst, preferSecond) - -import Maybe.Extra - - -preferFirst : Maybe a -> Maybe a -> Maybe a -preferFirst = - Maybe.Extra.or - - -preferSecond : Maybe a -> Maybe a -> Maybe a -preferSecond = - Maybe.Extra.orElse diff --git a/src/Library/MediaSession.elm b/src/Library/MediaSession.elm deleted file mode 100644 index 299fe63f6..000000000 --- a/src/Library/MediaSession.elm +++ /dev/null @@ -1,8 +0,0 @@ -module MediaSession exposing (states) - - -states = - { none = "none" - , paused = "paused" - , playing = "playing" - } diff --git a/src/Library/Notifications.elm b/src/Library/Notifications.elm deleted file mode 100644 index 1d1068628..000000000 --- a/src/Library/Notifications.elm +++ /dev/null @@ -1,186 +0,0 @@ -module Notifications exposing (Action, Kind(..), Notification, Options, casual, contents, dismiss, error, errorWithCode, id, kind, options, stickyCasual, stickyError, stickySuccess, success) - -import Chunky exposing (..) -import Html exposing (Html) -import Markdown -import Murmur3 exposing (..) - - - --- 🌳 - - -type Notification msg - = Notification Kind Int Options (Html msg) - - -type alias Action msg = - { label : String, msg : msg } - - -type alias Options = - { sticky : Bool, wasDismissed : Bool } - - -type Kind - = Casual - | Error - | Success - - - --- 🔱 - - -id : Notification msg -> Int -id (Notification _ i _ _) = - i - - -contents : Notification msg -> Html msg -contents (Notification _ _ _ c) = - c - - -kind : Notification msg -> Kind -kind (Notification k _ _ _) = - k - - -options : Notification msg -> Options -options (Notification _ _ o _) = - o - - - --- ⚗️ - - -dismiss : Notification msg -> Notification msg -dismiss (Notification k i o c) = - Notification k i { o | wasDismissed = True } c - - - --- 🚨 - - -error : String -> Notification msg -error content = - Notification - Error - (hashString 0 content) - { sticky = False - , wasDismissed = False - } - (render content) - - -stickyError : String -> Notification msg -stickyError content = - Notification - Error - (hashString 0 content) - { sticky = True - , wasDismissed = False - } - (render content) - - -errorWithCode : String -> String -> List (Action msg) -> Notification msg -errorWithCode content code _ = - Notification - Error - (hashString 0 content) - { sticky = True - , wasDismissed = False - } - (Html.div - [] - [ render content - , if String.isEmpty (String.trim code) then - nothing - - else - chunk - [ "bg-black-50" - , "break-all" - , "rounded" - , "mb-0" - , "mt-3" - , "p-2" - , "text-xxs" - ] - [ slab - Html.code - [] - [ "align-middle" ] - [ Html.text code ] - ] - ] - ) - - - --- 💚 - - -success : String -> Notification msg -success content = - Notification - Success - (hashString 0 content) - { sticky = False - , wasDismissed = False - } - (render content) - - -stickySuccess : String -> Notification msg -stickySuccess content = - Notification - Success - (hashString 0 content) - { sticky = True - , wasDismissed = False - } - (render content) - - - --- 🦉 - - -casual : String -> Notification msg -casual content = - Notification - Casual - (hashString 0 content) - { sticky = False - , wasDismissed = False - } - (render content) - - -stickyCasual : String -> Notification msg -stickyCasual content = - Notification - Casual - (hashString 0 content) - { sticky = True - , wasDismissed = False - } - (render content) - - - --- ⚗️ - - -render : String -> Html msg -render content = - content - |> String.lines - |> List.map String.trimLeft - |> String.join "\n" - |> Markdown.toHtml [] diff --git a/src/Library/Playlists.elm b/src/Library/Playlists.elm deleted file mode 100644 index f398b8b67..000000000 --- a/src/Library/Playlists.elm +++ /dev/null @@ -1,41 +0,0 @@ -module Playlists exposing (..) - -import Time - - - --- 🌳 - - -type alias Playlist = - { autoGenerated : Maybe { level : Int } - , collection : Bool - , name : String - , public : Bool - , tracks : List PlaylistTrack - } - - -type alias PlaylistTrackWithoutMetadata = - { album : Maybe String - , artist : Maybe String - , title : String - } - - -type alias PlaylistTrack = - { album : Maybe String - , artist : Maybe String - , title : String - - -- - , insertedAt : Time.Posix - } - - -type alias IdentifiedPlaylistTrack = - ( Identifiers, PlaylistTrack ) - - -type alias Identifiers = - { index : Int } diff --git a/src/Library/Playlists/Encoding.elm b/src/Library/Playlists/Encoding.elm deleted file mode 100644 index 102427f7d..000000000 --- a/src/Library/Playlists/Encoding.elm +++ /dev/null @@ -1,66 +0,0 @@ -module Playlists.Encoding exposing (decoder, encode, encodePlaylistTrack, playlistTrackDecoder) - -import Json.Decode as Decode -import Json.Decode.Ext as Decode -import Json.Encode as Encode -import Json.Encode.Ext exposing (..) -import Playlists exposing (..) -import Time.Ext as Time - - - --- ENCODE - - -encode : Playlist -> Encode.Value -encode playlist = - Encode.object - [ ( "autoGenerated" - , case playlist.autoGenerated of - Just { level } -> - Encode.object - [ ( "level", Encode.int level ) ] - - Nothing -> - Encode.null - ) - , ( "collection", Encode.bool playlist.collection ) - , ( "name", Encode.string playlist.name ) - , ( "public", Encode.bool playlist.public ) - , ( "tracks", Encode.list encodePlaylistTrack playlist.tracks ) - ] - - -encodePlaylistTrack : PlaylistTrack -> Encode.Value -encodePlaylistTrack playlistTrack = - Encode.object - [ ( "album", encodeMaybe playlistTrack.album Encode.string ) - , ( "artist", encodeMaybe playlistTrack.artist Encode.string ) - , ( "title", Encode.string playlistTrack.title ) - - -- - , ( "insertAt", Time.encode playlistTrack.insertedAt ) - ] - - - --- DECODE - - -decoder : Decode.Decoder Playlist -decoder = - Decode.map5 Playlist - (Decode.field "autoGenerated" <| Decode.maybe <| Decode.map (\l -> { level = l }) <| Decode.field "level" Decode.int) - (Decode.optionalField "collection" Decode.bool False) - (Decode.field "name" Decode.string) - (Decode.optionalField "public" Decode.bool False) - (Decode.field "tracks" <| Decode.list playlistTrackDecoder) - - -playlistTrackDecoder : Decode.Decoder PlaylistTrack -playlistTrackDecoder = - Decode.map4 PlaylistTrack - (Decode.maybe <| Decode.field "album" Decode.string) - (Decode.maybe <| Decode.field "artist" Decode.string) - (Decode.field "title" Decode.string) - (Decode.optionalField "insertedAt" Time.decoder Time.default) diff --git a/src/Library/Playlists/Matching.elm b/src/Library/Playlists/Matching.elm deleted file mode 100644 index 1e47b9f4a..000000000 --- a/src/Library/Playlists/Matching.elm +++ /dev/null @@ -1,53 +0,0 @@ -module Playlists.Matching exposing (match) - -import Playlists exposing (..) -import Tracks exposing (IdentifiedTrack) - - - --- 🔱 - - -match : Playlist -> List IdentifiedTrack -> ( List IdentifiedTrack, List IdentifiedPlaylistTrack ) -match playlist = - List.foldl - (\( i, t ) ( identifiedTracks, remainingPlaylistTracks ) -> - let - im = - { album = t.tags.album - , artist = t.tags.artist - , title = t.tags.title - } - - ( matches, remainingPlaylistTracksWithoutMatches ) = - List.foldl - (\( pi, pt ) -> - if im.title == pt.title && im.album == pt.album && im.artist == pt.artist then - Tuple.mapBoth - ((::) ( playlistTrackIdentifiers i pi, t )) - identity - - else - Tuple.mapBoth - identity - ((::) ( pi, pt )) - ) - ( [], [] ) - remainingPlaylistTracks - in - ( identifiedTracks ++ matches - , remainingPlaylistTracksWithoutMatches - ) - ) - ( [] - , List.indexedMap (\idx -> Tuple.pair { index = idx }) playlist.tracks - ) - - - --- ㊙️ - - -playlistTrackIdentifiers : Tracks.Identifiers -> Playlists.Identifiers -> Tracks.Identifiers -playlistTrackIdentifiers i pi = - { i | indexInPlaylist = Just pi.index } diff --git a/src/Library/Queue.elm b/src/Library/Queue.elm deleted file mode 100644 index 3188597f0..000000000 --- a/src/Library/Queue.elm +++ /dev/null @@ -1,78 +0,0 @@ -module Queue exposing (EngineItem, Item, makeEngineItem, makeItem, makeTrackUrl) - -import Dict exposing (Dict) -import List.Extra as List -import Sources exposing (Source) -import Sources.Processing exposing (HttpMethod(..)) -import Sources.Services -import Time -import Tracks exposing (IdentifiedTrack, Tags, Track) - - - --- 🌳 - - -type alias Item = - { manualEntry : Bool - , identifiedTrack : IdentifiedTrack - } - - -type alias EngineItem = - { isCached : Bool - , isPreload : Bool - , progress : Maybe Float - , sourceId : String - , trackId : String - , trackTags : Tags - , trackPath : String - , url : String - } - - - --- 🔱 - - -makeEngineItem : Bool -> Time.Posix -> List Source -> List String -> Dict String Float -> Track -> EngineItem -makeEngineItem preload timestamp sources cachedTrackIds progressTable track = - { isCached = List.member track.id cachedTrackIds - , isPreload = preload - , progress = Dict.get track.id progressTable - , sourceId = track.sourceId - , trackId = track.id - , trackPath = track.path - , trackTags = track.tags - , url = makeTrackUrl timestamp sources track - } - - -makeItem : Bool -> IdentifiedTrack -> Item -makeItem isManualEntry identifiedTrack = - { manualEntry = isManualEntry - , identifiedTrack = identifiedTrack - } - - -makeTrackUrl : Time.Posix -> List Source -> Track -> String -makeTrackUrl timestamp sources track = - sources - |> List.find (.id >> (==) track.sourceId) - |> Maybe.map (makeTrackUrl_ timestamp track) - |> Maybe.withDefault "" - - - --- ㊙️ - - -makeTrackUrl_ : Time.Posix -> Track -> Source -> String -makeTrackUrl_ timestamp track source = - Sources.Services.makeTrackUrl - source.service - timestamp - source.id - source.data - Get - track.path diff --git a/src/Library/Return/Ext.elm b/src/Library/Return/Ext.elm deleted file mode 100644 index abccc6e07..000000000 --- a/src/Library/Return/Ext.elm +++ /dev/null @@ -1,19 +0,0 @@ -module Return.Ext exposing (..) - -import Task - - - --- 🔱 - - -communicate : Cmd msg -> model -> ( model, Cmd msg ) -communicate c m = - ( m, c ) - - -task : msg -> Cmd msg -task msg = - msg - |> Task.succeed - |> Task.perform identity diff --git a/src/Library/Settings.elm b/src/Library/Settings.elm deleted file mode 100644 index 73199a090..000000000 --- a/src/Library/Settings.elm +++ /dev/null @@ -1,63 +0,0 @@ -module Settings exposing (Settings, decoder, encode) - -import Json.Decode as Json -import Json.Decode.Pipeline exposing (optional) -import Json.Encode -import Maybe.Extra as Maybe - - - --- 🌳 - - -type alias Settings = - { backgroundImage : Maybe String - , coverSelectionReducesPool : Bool - , hideDuplicates : Bool - , lastFm : Maybe String - , processAutomatically : Bool - , rememberProgress : Bool - } - - - --- ENCODING - - -encode : Settings -> Json.Value -encode settings = - Json.Encode.object - [ ( "backgroundImage" - , Maybe.unwrap Json.Encode.null Json.Encode.string settings.backgroundImage - ) - , ( "coverSelectionReducesPool" - , Json.Encode.bool settings.coverSelectionReducesPool - ) - , ( "hideDuplicates" - , Json.Encode.bool settings.hideDuplicates - ) - , ( "lastFm" - , Maybe.unwrap Json.Encode.null Json.Encode.string settings.lastFm - ) - , ( "processAutomatically" - , Json.Encode.bool settings.processAutomatically - ) - , ( "rememberProgress" - , Json.Encode.bool settings.rememberProgress - ) - ] - - - --- DECODING - - -decoder : Json.Decoder Settings -decoder = - Json.succeed Settings - |> optional "backgroundImage" (Json.maybe Json.string) Nothing - |> optional "coverSelectionReducesPool" Json.bool True - |> optional "hideDuplicates" Json.bool False - |> optional "lastFm" (Json.maybe Json.string) Nothing - |> optional "processAutomatically" Json.bool True - |> optional "rememberProgress" Json.bool True diff --git a/src/Library/Sources.elm b/src/Library/Sources.elm deleted file mode 100644 index 250a231cf..000000000 --- a/src/Library/Sources.elm +++ /dev/null @@ -1,120 +0,0 @@ -module Sources exposing (..) - -import Conditional exposing (..) -import Dict exposing (Dict) -import Json.Decode -import Time - - - --- 🌳 - - -type alias Source = - { id : String - , data : SourceData - , directoryPlaylists : Bool - , enabled : Bool - , service : Service - } - - - --- PIECES - - -type alias Property = - { key : String - , label : String - , placeholder : String - , password : Bool - } - - -type alias SourceData = - Dict String String - - - --- SERVICES - - -type Service - = AmazonS3 - | AzureBlob - | AzureFile - | Dropbox - | Google - | Ipfs - | WebDav - - -serviceDictionary : Dict String Service -serviceDictionary = - Dict.fromList - [ ( "amazons3", AmazonS3 ) - , ( "amazon_s3", AmazonS3 ) - , ( "azureblob", AzureBlob ) - , ( "azure_blob", AzureBlob ) - , ( "azurefile", AzureFile ) - , ( "azure_file", AzureFile ) - , ( "dropbox", Dropbox ) - , ( "google", Google ) - , ( "ipfs", Ipfs ) - , ( "webdav", WebDav ) - , ( "web_dav", WebDav ) - ] - - -serviceDecoder : Json.Decode.Decoder Service -serviceDecoder = - Json.Decode.andThen - (\string -> - serviceDictionary - |> Dict.get string - |> Maybe.map Json.Decode.succeed - |> Maybe.withDefault (Json.Decode.fail "Invalid source kind") - ) - Json.Decode.string - - - ---- 🔱 - - -enabledSourceIds : List Source -> List String -enabledSourceIds = - List.filterMap (\s -> ifThenElse s.enabled (Just s.id) Nothing) - - -setProperId : Int -> Time.Posix -> Source -> Source -setProperId n time source = - { source | id = String.fromInt (Time.posixToMillis time) ++ String.fromInt n } - - -worksOffline : Source -> Bool -worksOffline source = - case source.service of - AmazonS3 -> - False - - AzureBlob -> - False - - AzureFile -> - False - - Dropbox -> - False - - Google -> - False - - Ipfs -> - True - - WebDav -> - source.data - |> Dict.get "url" - |> Maybe.map (\u -> String.contains "localhost" u || String.contains "127.0.0.1" u) - |> Maybe.withDefault False diff --git a/src/Library/Sources/Encoding.elm b/src/Library/Sources/Encoding.elm deleted file mode 100644 index 722e008da..000000000 --- a/src/Library/Sources/Encoding.elm +++ /dev/null @@ -1,74 +0,0 @@ -module Sources.Encoding exposing (decode, decoder, encode, encodeData, serviceDecoder) - -{-| Encoding. --} - -import Dict -import Json.Decode as Decode -import Json.Encode as Encode -import Sources exposing (..) -import Sources.Services as Services - - - --- ENCODE - - -encode : Source -> Encode.Value -encode source = - Encode.object - [ ( "id", Encode.string source.id ) - , ( "data", encodeData source.data ) - , ( "directoryPlaylists", Encode.bool source.directoryPlaylists ) - , ( "enabled", Encode.bool source.enabled ) - , ( "service", Encode.string (Services.typeToKey source.service) ) - ] - - -encodeData : SourceData -> Encode.Value -encodeData data = - data - |> Dict.toList - |> List.map (Tuple.mapSecond Encode.string) - |> Encode.object - - - --- DECODE - - -decode : Decode.Value -> Maybe Source -decode value = - value - |> Decode.decodeValue decoder - |> Result.toMaybe - - -decoder : Decode.Decoder Source -decoder = - Decode.map5 Source - (Decode.field "id" Decode.string) - (Decode.field "data" (Decode.dict Decode.string)) - (Decode.field "directoryPlaylists" Decode.bool - |> Decode.maybe - |> Decode.map (Maybe.withDefault True) - ) - (Decode.field "enabled" Decode.bool - |> Decode.maybe - |> Decode.map (Maybe.withDefault True) - ) - (Decode.field "service" serviceDecoder) - - -serviceDecoder : Decode.Decoder Service -serviceDecoder = - Decode.andThen - (\key -> - case Services.keyToType key of - Just service -> - Decode.succeed service - - Nothing -> - Decode.fail "Unrecognizable source service" - ) - Decode.string diff --git a/src/Library/Sources/Pick.elm b/src/Library/Sources/Pick.elm deleted file mode 100644 index c9d4205ba..000000000 --- a/src/Library/Sources/Pick.elm +++ /dev/null @@ -1,30 +0,0 @@ -module Sources.Pick exposing (isMusicFile, selectMusicFiles) - -import Regex - - - --- 🔱 - - -isMusicFile : String -> Bool -isMusicFile = - Regex.contains musicFileRegex - - -selectMusicFiles : List String -> List String -selectMusicFiles = - List.filter isMusicFile - - - ------------------------------------------ --- ㊙️ ------------------------------------------ - - -musicFileRegex : Regex.Regex -musicFileRegex = - "\\.(mp3|mp4|m4a|flac|ogg|opus|wav|webm)$" - |> Regex.fromStringWith { caseInsensitive = True, multiline = False } - |> Maybe.withDefault Regex.never diff --git a/src/Library/Sources/Processing.elm b/src/Library/Sources/Processing.elm deleted file mode 100644 index 8bf842674..000000000 --- a/src/Library/Sources/Processing.elm +++ /dev/null @@ -1,97 +0,0 @@ -module Sources.Processing exposing (..) - -import Sources exposing (Source, SourceData) -import Tracks exposing (Tags, Track) - - - --- 🌳 - - -type Status - = Processing ( Source, List Track ) (List ( Source, List Track )) - | NotProcessing - - -type alias Arguments = - { origin : String - , sources : List Source - } - - - --- MARKERS & RESPONSES - - -type Marker - = TheBeginning - | InProgress String - | TheEnd - - -type alias PrepationAnswer marker = - { sourceData : SourceData - , marker : marker - } - - -type alias TreeAnswer marker = - { filePaths : List String - , marker : marker - } - - - --- CONTEXTS - - -type alias Context = - { filePaths : List String - , origin : String - , preparationMarker : Marker - , source : Source - , treeMarker : Marker - } - - -type alias ContextForTags = - { amount : Int - , nextFilePaths : List String - , receivedFilePaths : List String - , receivedTags : List (Maybe Tags) - , sourceId : String - , urlsForTags : List TagUrls - } - - -type alias ContextForTagsSync = - { receivedFilePaths : List String - , receivedTags : List (Maybe Tags) - , trackIds : List String - , urlsForTags : List TagUrls - } - - -type alias TagUrls = - { getUrl : String - , headUrl : String - } - - - --- HTTP - - -type HttpMethod - = Get - | Head - - -httpMethod : HttpMethod -> String -httpMethod method = - case method of - Get -> - "GET" - - Head -> - "HEAD" diff --git a/src/Library/Sources/Processing/Encoding.elm b/src/Library/Sources/Processing/Encoding.elm deleted file mode 100644 index 0401f6ea1..000000000 --- a/src/Library/Sources/Processing/Encoding.elm +++ /dev/null @@ -1,19 +0,0 @@ -module Sources.Processing.Encoding exposing (argumentsDecoder) - -{-| Encoding. --} - -import Json.Decode as Decode exposing (Decoder) -import Sources.Encoding as Sources -import Sources.Processing exposing (Arguments) - - - --- 🔱 - - -argumentsDecoder : Decoder Arguments -argumentsDecoder = - Decode.map2 Arguments - (Decode.field "origin" Decode.string) - (Decode.field "sources" <| Decode.list Sources.decoder) diff --git a/src/Library/Sources/Refresh/AccessToken.elm b/src/Library/Sources/Refresh/AccessToken.elm deleted file mode 100644 index 2152a5a96..000000000 --- a/src/Library/Sources/Refresh/AccessToken.elm +++ /dev/null @@ -1,39 +0,0 @@ -module Sources.Refresh.AccessToken exposing (..) - -import Json.Decode exposing (Decoder) -import Sources exposing (Service) -import Sources.Encoding exposing (serviceDecoder) - - - --- 🌳 - - -type alias PortArguments = - { service : Service - , sourceId : String - , accessToken : String - - -- Unix timestamp in milliseconds - , expiresAt : Int - } - - - --- 🛠 - - -portArgumentsDecoder : Decoder PortArguments -portArgumentsDecoder = - Json.Decode.map4 - (\service sourceId accessToken expiresAt -> - { service = service - , sourceId = sourceId - , accessToken = accessToken - , expiresAt = expiresAt - } - ) - (Json.Decode.field "service" serviceDecoder) - (Json.Decode.field "sourceId" Json.Decode.string) - (Json.Decode.field "accessToken" Json.Decode.string) - (Json.Decode.field "expiresAt" Json.Decode.int) diff --git a/src/Library/Sources/Services.elm b/src/Library/Sources/Services.elm deleted file mode 100644 index a3b16fcef..000000000 --- a/src/Library/Sources/Services.elm +++ /dev/null @@ -1,328 +0,0 @@ -module Sources.Services exposing (initialData, keyToType, labels, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties, typeToKey) - -{-| Service functions used in other modules. --} - -import Http -import Sources exposing (..) -import Sources.Processing exposing (..) -import Sources.Services.AmazonS3 as AmazonS3 -import Sources.Services.AzureBlob as AzureBlob -import Sources.Services.AzureFile as AzureFile -import Sources.Services.Dropbox as Dropbox -import Sources.Services.Google as Google -import Sources.Services.Ipfs as Ipfs -import Sources.Services.WebDav as WebDav -import Time - - - --- FUNCTIONS - - -initialData : Service -> SourceData -initialData service = - case service of - AmazonS3 -> - AmazonS3.initialData - - AzureBlob -> - AzureBlob.initialData - - AzureFile -> - AzureFile.initialData - - Dropbox -> - Dropbox.initialData - - Google -> - Google.initialData - - Ipfs -> - Ipfs.initialData - - WebDav -> - WebDav.initialData - - -makeTrackUrl : Service -> Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl service = - case service of - AmazonS3 -> - AmazonS3.makeTrackUrl - - AzureBlob -> - AzureBlob.makeTrackUrl - - AzureFile -> - AzureFile.makeTrackUrl - - Dropbox -> - Dropbox.makeTrackUrl - - Google -> - Google.makeTrackUrl - - Ipfs -> - Ipfs.makeTrackUrl - - WebDav -> - WebDav.makeTrackUrl - - -makeTree : - Service - -> SourceData - -> Marker - -> Time.Posix - -> (Result Http.Error String -> msg) - -> Cmd msg -makeTree service = - case service of - AmazonS3 -> - AmazonS3.makeTree - - AzureBlob -> - AzureBlob.makeTree - - AzureFile -> - AzureFile.makeTree - - Dropbox -> - Dropbox.makeTree - - Google -> - Google.makeTree - - Ipfs -> - Ipfs.makeTree - - WebDav -> - WebDav.makeTree - - -parseErrorResponse : Service -> String -> Maybe String -parseErrorResponse service = - case service of - AmazonS3 -> - AmazonS3.parseErrorResponse - - AzureBlob -> - AzureBlob.parseErrorResponse - - AzureFile -> - AzureFile.parseErrorResponse - - Dropbox -> - Dropbox.parseErrorResponse - - Google -> - Google.parseErrorResponse - - Ipfs -> - Ipfs.parseErrorResponse - - WebDav -> - WebDav.parseErrorResponse - - -parsePreparationResponse : Service -> String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse service = - case service of - AmazonS3 -> - AmazonS3.parsePreparationResponse - - AzureBlob -> - AzureBlob.parsePreparationResponse - - AzureFile -> - AzureFile.parsePreparationResponse - - Dropbox -> - Dropbox.parsePreparationResponse - - Google -> - Google.parsePreparationResponse - - Ipfs -> - Ipfs.parsePreparationResponse - - WebDav -> - WebDav.parsePreparationResponse - - -parseTreeResponse : Service -> String -> Marker -> TreeAnswer Marker -parseTreeResponse service = - case service of - AmazonS3 -> - AmazonS3.parseTreeResponse - - AzureBlob -> - AzureBlob.parseTreeResponse - - AzureFile -> - AzureFile.parseTreeResponse - - Dropbox -> - Dropbox.parseTreeResponse - - Google -> - Google.parseTreeResponse - - Ipfs -> - Ipfs.parseTreeResponse - - WebDav -> - WebDav.parseTreeResponse - - -postProcessTree : Service -> List String -> List String -postProcessTree service = - case service of - AmazonS3 -> - AmazonS3.postProcessTree - - AzureBlob -> - AzureBlob.postProcessTree - - AzureFile -> - AzureFile.postProcessTree - - Dropbox -> - Dropbox.postProcessTree - - Google -> - Google.postProcessTree - - Ipfs -> - Ipfs.postProcessTree - - WebDav -> - WebDav.postProcessTree - - -prepare : - Service - -> String - -> SourceData - -> Marker - -> (Result Http.Error String -> msg) - -> Maybe (Cmd msg) -prepare service = - case service of - AmazonS3 -> - AmazonS3.prepare - - AzureBlob -> - AzureBlob.prepare - - AzureFile -> - AzureFile.prepare - - Dropbox -> - Dropbox.prepare - - Google -> - Google.prepare - - Ipfs -> - Ipfs.prepare - - WebDav -> - WebDav.prepare - - -properties : Service -> List Property -properties service = - case service of - AmazonS3 -> - AmazonS3.properties - - AzureBlob -> - AzureBlob.properties - - AzureFile -> - AzureFile.properties - - Dropbox -> - Dropbox.properties - - Google -> - Google.properties - - Ipfs -> - Ipfs.properties - - WebDav -> - WebDav.properties - - - --- KEYS & LABELS - - -keyToType : String -> Maybe Service -keyToType str = - case str of - "AmazonS3" -> - Just AmazonS3 - - "AzureBlob" -> - Just AzureBlob - - "AzureFile" -> - Just AzureFile - - "Dropbox" -> - Just Dropbox - - "Google" -> - Just Google - - "Ipfs" -> - Just Ipfs - - "WebDav" -> - Just WebDav - - _ -> - Nothing - - -typeToKey : Service -> String -typeToKey service = - case service of - AmazonS3 -> - "AmazonS3" - - AzureBlob -> - "AzureBlob" - - AzureFile -> - "AzureFile" - - Dropbox -> - "Dropbox" - - Google -> - "Google" - - Ipfs -> - "Ipfs" - - WebDav -> - "WebDav" - - -{-| Service labels. -Maps a service key to a label. --} -labels : List ( String, String ) -labels = - [ ( typeToKey AmazonS3, "Amazon S3" ) - , ( typeToKey AzureBlob, "Azure Blob Storage" ) - , ( typeToKey AzureFile, "Azure File Storage" ) - , ( typeToKey Dropbox, "Dropbox" ) - , ( typeToKey Ipfs, "IPFS" ) - , ( typeToKey WebDav, "WebDAV" ) - ] diff --git a/src/Library/Sources/Services/AmazonS3.elm b/src/Library/Sources/Services/AmazonS3.elm deleted file mode 100644 index 34cc66bca..000000000 --- a/src/Library/Sources/Services/AmazonS3.elm +++ /dev/null @@ -1,190 +0,0 @@ -module Sources.Services.AmazonS3 exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) - -{-| Amazon S3 Service. - -Resources: - - - - --} - -import Common -import Dict -import Http -import Sources exposing (Property, SourceData) -import Sources.Pick -import Sources.Processing exposing (..) -import Sources.Services.AmazonS3.Parser as Parser -import Sources.Services.AmazonS3.Presign exposing (..) -import Sources.Services.Common exposing (cleanPath, noPrep) -import Time - - - --- PROPERTIES --- 📟 - - -defaults = - { bucketName = "music" - , name = "Music from Amazon S3" - , region = "eu-west-1" - } - - -{-| The list of properties we need from the user. --} -properties : List Property -properties = - [ { key = "accessKey" - , label = "Access key" - , placeholder = "Fv6EWfLfCcMo" - , password = True - } - , { key = "secretKey" - , label = "Secret key" - , placeholder = "qeNcqiMpgqC8" - , password = True - } - , { key = "bucketName" - , label = "Bucket name" - , placeholder = "music" - , password = False - } - , { key = "region" - , label = "Region" - , placeholder = defaults.region - , password = False - } - , { key = "directoryPath" - , label = "Directory (Optional)" - , placeholder = "/" - , password = False - } - , { key = "host" - , label = "Host (Optional)" - , placeholder = "http://localhost:9000" - , password = False - } - ] - - -{-| Initial data set. --} -initialData : SourceData -initialData = - Dict.fromList - [ ( "accessKey", "" ) - , ( "bucketName", defaults.bucketName ) - , ( "directoryPath", "" ) - , ( "host", "" ) - , ( "name", defaults.name ) - , ( "region", defaults.region ) - , ( "secretKey", "" ) - ] - - - --- PREPARATION - - -prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) -prepare _ _ _ _ = - Nothing - - - --- TREE - - -{-| Create a directory tree. - -List all the tracks in the bucket. -Or a specific directory in the bucket. - --} -makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg -makeTree srcData marker currentTime resultMsg = - let - directoryPath = - srcData - |> Dict.get "directoryPath" - |> Maybe.withDefault "" - |> cleanPath - - initialParams = - [ ( "list-type", "2" ) - , ( "max-keys", "500" ) - ] - - prefix = - if String.length directoryPath > 0 then - [ ( "prefix", directoryPath ) ] - - else - [] - - continuation = - case marker of - InProgress s -> - [ ( "continuation-token", s ) ] - - _ -> - [] - - params = - initialParams ++ prefix ++ continuation - - url = - presignedUrl Get (60 * 5) params currentTime srcData "/" - in - Http.get - { url = url - , expect = Http.expectStringResponse resultMsg Common.translateHttpResponse - } - - -{-| Re-export parser functions. --} -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse = - noPrep - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse = - Parser.parseTreeResponse - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Parser.parseErrorResponse - - - --- POST - - -{-| Post process the tree results. - -Make sure we only use music files that we can use. - --} -postProcessTree : List String -> List String -postProcessTree = - Sources.Pick.selectMusicFiles - - - --- TRACK URL - - -{-| Create a public url for a file. - -We need this to play the track. -Creates a presigned url that's valid for 48 hours - --} -makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime _ srcData method pathToFile = - presignedUrl method 172800 [] currentTime srcData pathToFile diff --git a/src/Library/Sources/Services/AmazonS3/Parser.elm b/src/Library/Sources/Services/AmazonS3/Parser.elm deleted file mode 100644 index 17e6ef3c8..000000000 --- a/src/Library/Sources/Services/AmazonS3/Parser.elm +++ /dev/null @@ -1,66 +0,0 @@ -module Sources.Services.AmazonS3.Parser exposing (parseErrorResponse, parseTreeResponse) - -import Conditional exposing (..) -import Sources.Processing exposing (Marker(..), TreeAnswer) -import Xml.Decode exposing (..) - - - --- TREE - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse response _ = - response - |> decodeString - (map2 - (\f m -> { filePaths = f, marker = m }) - filePathsDecoder - markerDecoder - ) - |> Result.withDefault { filePaths = [], marker = TheEnd } - - -filePathsDecoder : Decoder (List String) -filePathsDecoder = - string - |> single - |> path [ "Key" ] - |> list - |> path [ "Contents" ] - - -markerDecoder : Decoder Marker -markerDecoder = - map2 - (\a b -> - Maybe.withDefault - TheEnd - (Maybe.map2 - (\isTruncated token -> - ifThenElse (isTruncated == "true") (InProgress token) TheEnd - ) - a - b - ) - ) - (maybe <| path [ "IsTruncated" ] <| single string) - (maybe <| path [ "NextContinuationToken" ] <| single string) - - - --- ERROR - - -parseErrorResponse : String -> Maybe String -parseErrorResponse response = - response - |> decodeString errorMessagesDecoder - |> Result.toMaybe - - -errorMessagesDecoder : Decoder String -errorMessagesDecoder = - string - |> single - |> path [ "Message" ] diff --git a/src/Library/Sources/Services/AmazonS3/Presign.elm b/src/Library/Sources/Services/AmazonS3/Presign.elm deleted file mode 100644 index 6510534c6..000000000 --- a/src/Library/Sources/Services/AmazonS3/Presign.elm +++ /dev/null @@ -1,208 +0,0 @@ -module Sources.Services.AmazonS3.Presign exposing (presignedUrl) - -import Binary exposing (Bits) -import Common -import Cryptography.Hmac as Hmac -import DateFormat as Date -import Dict.Ext as Dict -import Hex -import Maybe.Extra as Maybe -import Regex -import SHA -import Sources exposing (SourceData) -import Sources.Processing exposing (HttpMethod, httpMethod) -import String.Ext as String -import Time -import Url - - - --- 🔱 - - -presignedUrl : - HttpMethod - -> Int - -> List ( String, String ) - -> Time.Posix - -> SourceData - -> String - -> String -presignedUrl method lifeExpectancyInSeconds extraParams currentTime srcData pathToFile = - let - aws = - srcData - - region = - Dict.fetchUnknown "region" aws - - bucketName = - Dict.fetchUnknown "bucketName" aws - - customHost = - case Dict.fetch "host" "" aws of - "" -> - Nothing - - x -> - Just x - - host = - case customHost of - Just h -> - h - |> String.chopStart "http://" - |> String.chopStart "https://" - |> String.chopEnd "/" - - Nothing -> - case String.trim region of - "" -> - bucketName ++ ".s3.amazonaws.com" - - r -> - bucketName ++ ".s3." ++ r ++ ".amazonaws.com" - - protocol = - if String.contains "http://" (Maybe.withDefault "" customHost) then - "http://" - - else - "https://" - - -- {var} Paths - filePathPrefix = - if Maybe.isJust customHost then - bucketName ++ "/" - - else - "" - - filePath = - pathToFile - |> String.chopStart "/" - |> String.split "/" - |> List.map (Url.percentEncode >> encodeAdditionalCharacters) - |> String.join "/" - |> String.append ("/" ++ filePathPrefix) - - -- {var} Time - -- timestamp -> 20130721T201207Z - -- date -> 20130721 - timestamp = - Date.format - [ Date.yearNumber - , Date.monthFixed - , Date.dayOfMonthFixed - , Date.text "T" - , Date.hourMilitaryFixed - , Date.minuteFixed - , Date.secondFixed - , Date.text "Z" - ] - Time.utc - currentTime - - date = - Date.format - [ Date.yearNumber - , Date.monthFixed - , Date.dayOfMonthFixed - ] - Time.utc - currentTime - - -- Request - credential = - [ Dict.fetchUnknown "accessKey" aws - , date - , region - , "s3" - , "aws4_request" - ] - |> String.join "/" - - queryString = - [ ( "X-Amz-Algorithm", "AWS4-HMAC-SHA256" ) - , ( "X-Amz-Credential", credential ) - , ( "X-Amz-Date", timestamp ) - , ( "X-Amz-Expires", String.fromInt lifeExpectancyInSeconds ) - , ( "X-Amz-SignedHeaders", "host" ) - ] - |> List.append extraParams - |> List.sortBy Tuple.first - |> Common.queryString - |> String.dropLeft 1 - |> encodeAdditionalCharacters - - request = - String.join - "\n" - [ httpMethod method - , filePath - , queryString - , "host:" ++ host - , "" - , "host" - , "UNSIGNED-PAYLOAD" - ] - - -- String to sign - stringToSign = - String.join - "\n" - [ "AWS4-HMAC-SHA256" - , timestamp - , String.join "/" [ date, region, "s3", "aws4_request" ] - - -- - , request - |> Binary.fromStringAsUtf8 - |> SHA.sha256 - |> Binary.toHex - |> String.toLower - ] - - -- Signature - signature = - ("AWS4" ++ Dict.fetchUnknown "secretKey" aws) - |> Binary.fromStringAsUtf8 - |> hmacSha256 date - |> hmacSha256 region - |> hmacSha256 "s3" - |> hmacSha256 "aws4_request" - |> hmacSha256 stringToSign - |> Binary.toHex - |> String.toLower - in - String.concat - [ protocol - , host - , filePath - , "?" - , queryString - , "&X-Amz-Signature=" - , signature - ] - - - --- ⚗️ - - -encodeAdditionalCharacters : String -> String -encodeAdditionalCharacters query = - Regex.replace - (Maybe.withDefault Regex.never <| Regex.fromString "[!*'()]") - (\{ match } -> - match - |> String.toList - |> List.map (Char.toCode >> Hex.toString >> String.toUpper >> (++) "%") - |> String.concat - ) - query - - -hmacSha256 : String -> Bits -> Bits -hmacSha256 = - Hmac.encrypt64 SHA.sha256 diff --git a/src/Library/Sources/Services/Azure/Authorization.elm b/src/Library/Sources/Services/Azure/Authorization.elm deleted file mode 100755 index 8c5a8b0fe..000000000 --- a/src/Library/Sources/Services/Azure/Authorization.elm +++ /dev/null @@ -1,225 +0,0 @@ -module Sources.Services.Azure.Authorization exposing (Computation(..), SignatureDependencies, StorageMethod(..), makeSignature, presignedUrl) - -{-| Resources: - - - - - - --} - -import Binary -import BinaryBase64 -import Common -import Cryptography.Hmac as Hmac -import DateFormat as Date -import Dict.Ext as Dict -import SHA -import Sources exposing (SourceData) -import Sources.Processing exposing (HttpMethod) -import String.Ext as String -import Time -import Url - - - --- Types - - -type Computation - = List - | Read - - -type StorageMethod - = Blob - | File - - - --- Public functions - - -presignedUrl : - StorageMethod - -> Computation - -> HttpMethod - -> Int - -> Time.Posix - -> SourceData - -> String - -> List ( String, String ) - -> String -presignedUrl storageMethod computation _ _ currentTime srcData pathToFile params = - let - azure = - srcData - - accountName = - Dict.fetchUnknown "accountName" azure - - accountKey = - Dict.fetchUnknown "accountKey" azure - - container = - Dict.fetchUnknown "container" azure - - -- {var} Time (y-MM-ddTHH:mmZ) - expiryTime = - Date.format - [ Date.yearNumber - , Date.text "-" - , Date.monthFixed - , Date.text "-" - , Date.dayOfMonthFixed - , Date.text "T" - , Date.hourMilitaryFixed - , Date.text ":" - , Date.minuteFixed - , Date.text "Z" - ] - Time.utc - (currentTime - |> Time.posixToMillis - |> (+) 3600000 - |> Time.millisToPosix - ) - - -- {var} Other - permissions = - case computation of - List -> - "l" - - Read -> - "r" - - resourceType = - case storageMethod of - Blob -> - "blob" - - File -> - "file" - - resType = - case storageMethod of - Blob -> - "container" - - File -> - "directory" - - -- Signature - signatureStuff = - { accountKey = accountKey - , accountName = accountName - , expiryTime = expiryTime - , permissions = permissions - , protocol = "https" - , resources = "co" - , services = "bf" - , startTime = "" - , version = "2017-04-17" - } - in - String.concat - [ "https://" - , Url.percentEncode accountName - , "." - , Url.percentEncode resourceType - , ".core.windows.net/" - , Url.percentEncode container - , "/" - , Url.percentEncode (String.chopStart "/" pathToFile) - - -- Start query params - , case Common.queryString params of - "" -> - "?" - - qs -> - qs - - -- Query params for certain requests - , case computation of - List -> - "&restype=" ++ resType ++ "&comp=list" - - _ -> - "" - - -- Signature things - , "&sv=" - , Url.percentEncode signatureStuff.version - , "&ss=" - , Url.percentEncode signatureStuff.services - , "&srt=" - , Url.percentEncode signatureStuff.resources - , "&sp=" - , Url.percentEncode signatureStuff.permissions - , "&se=" - , Url.percentEncode signatureStuff.expiryTime - , "&spr=" - , Url.percentEncode signatureStuff.protocol - , "&sig=" - , Url.percentEncode (makeSignature signatureStuff) - ] - - - --- Signature - - -type alias SignatureDependencies = - { accountKey : String - , accountName : String - , expiryTime : String - , permissions : String - , protocol : String - , resources : String - , services : String - , startTime : String - , version : String - } - - -{-| Make a signature. - - >>> makeSignature { accountKey = "93K17Co74T2lDHk2rA+wmb/avIAS6u6lPnZrk2hyT+9+aov82qNhrcXSNGZCzm9mjd4d75/oxxOr6r1JVpgTLA==", accountName = "tsmatsuzsttest0001", expiryTime = "2016-07-08T04:41:20Z", permissions = "rwdlacup", protocol = "https", resources = "sco", services = "bfqt", startTime = "2016-06-29T04:41:20Z", version = "2015-04-05" } - "+XuDjuLE1Sv/FrJTLz8YjsaDukWNTKX7e8G8Ew+5aps=" - --} -makeSignature : SignatureDependencies -> String -makeSignature { accountKey, accountName, expiryTime, permissions, protocol, resources, services, startTime, version } = - let - message = - -- accountname + "\n" + - -- signedpermissions + "\n" + - -- signedservice + "\n" + - -- signedresourcetype + "\n" + - -- signedstart + "\n" + - -- signedexpiry + "\n" + - -- signedIP + "\n" + - -- signedProtocol + "\n" + - -- signedversion + "\n" - String.join "\n" - [ accountName - , permissions - , services - , resources - , startTime - , expiryTime - , "" - , protocol - , version ++ "\n" - ] - in - accountKey - |> BinaryBase64.decode - |> Result.withDefault [] - |> List.map (Binary.fromDecimal >> Binary.ensureSize 8) - |> Binary.concat - |> Hmac.encrypt64 SHA.sha256 message - |> Binary.chunksOf 8 - |> List.map Binary.toDecimal - |> BinaryBase64.encode diff --git a/src/Library/Sources/Services/Azure/BlobParser.elm b/src/Library/Sources/Services/Azure/BlobParser.elm deleted file mode 100755 index c7d6f3900..000000000 --- a/src/Library/Sources/Services/Azure/BlobParser.elm +++ /dev/null @@ -1,64 +0,0 @@ -module Sources.Services.Azure.BlobParser exposing (parseErrorResponse, parseTreeResponse) - -import Sources.Processing exposing (Marker(..), TreeAnswer) -import Xml.Decode exposing (..) - - - --- TREE - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse response _ = - response - |> decodeString - (map2 - (\f m -> { filePaths = f, marker = m }) - filePathsDecoder - markerDecoder - ) - |> Result.withDefault { filePaths = [], marker = TheEnd } - - -filePathsDecoder : Decoder (List String) -filePathsDecoder = - string - |> single - |> path [ "Name" ] - |> list - |> path [ "Blobs", "Blob" ] - - -markerDecoder : Decoder Marker -markerDecoder = - map - (\maybeNextMarker -> - case maybeNextMarker of - Just "" -> - TheEnd - - Just nextMarker -> - InProgress nextMarker - - Nothing -> - TheEnd - ) - (maybe <| path [ "NextMarker" ] <| single string) - - - --- ERROR - - -parseErrorResponse : String -> Maybe String -parseErrorResponse response = - response - |> decodeString errorMessagesDecoder - |> Result.toMaybe - - -errorMessagesDecoder : Decoder String -errorMessagesDecoder = - string - |> single - |> path [ "Message" ] diff --git a/src/Library/Sources/Services/Azure/FileMarker.elm b/src/Library/Sources/Services/Azure/FileMarker.elm deleted file mode 100755 index 5bbcc497b..000000000 --- a/src/Library/Sources/Services/Azure/FileMarker.elm +++ /dev/null @@ -1,154 +0,0 @@ -module Sources.Services.Azure.FileMarker exposing (MarkerItem(..), concat, itemToString, paramSeparator, prefixer, removeOne, separator, stringToItem, takeOne) - -{-| Custom `Marker` for the Azure File API. - -The Azure File API currently doesn't make a recursive list, -so we have to manage that ourselves. - -This custom marker is a combination of: - - - The default `marker` param, see URI parameters at - - Our custom logic to handle recursive listings - -Example: InProgress "dir=example ¶ param=defaultMarker" - --} - -import Sources.Processing exposing (Marker(..)) - - -type MarkerItem - = Directory String - | Param { directory : String, marker : String } - - -separator : String -separator = - " ɑ " - - -prefixer : String -prefixer = - " β " - - -paramSeparator : String -paramSeparator = - " ɣ " - - - --- IN - - -concat : List MarkerItem -> Marker -> Marker -concat list marker = - let - listStringified = - List.map itemToString list - - result = - case marker of - InProgress m -> - [ listStringified, String.split separator m ] - |> List.concat - |> String.join separator - - _ -> - String.join separator listStringified - in - case result of - "" -> - TheEnd - - r -> - InProgress r - - - --- OUT - - -{-| Take the first item and return it. --} -takeOne : Marker -> Maybe MarkerItem -takeOne marker = - case marker of - InProgress m -> - m - |> String.split separator - |> List.head - |> Maybe.andThen stringToItem - - _ -> - Nothing - - -{-| Remove the first item if there is one. --} -removeOne : Marker -> Marker -removeOne marker = - case marker of - InProgress m -> - let - tmp = - m - |> String.split separator - |> List.drop 1 - |> String.join separator - in - case tmp of - "" -> - TheEnd - - x -> - InProgress x - - _ -> - TheEnd - - - --- CONVERSIONS - - -itemToString : MarkerItem -> String -itemToString item = - case item of - Directory d -> - "dir" ++ prefixer ++ d - - Param { directory, marker } -> - "par" ++ prefixer ++ directory ++ paramSeparator ++ marker - - -stringToItem : String -> Maybe MarkerItem -stringToItem string = - let - exploded = - String.split prefixer string - in - case List.head exploded of - Just "dir" -> - exploded - |> List.drop 1 - |> String.join prefixer - |> Directory - |> Just - - Just "par" -> - exploded - |> List.drop 1 - |> String.join prefixer - |> String.split paramSeparator - |> (\x -> - case x of - [ dir, mar ] -> - Just (Param { directory = dir, marker = mar }) - - _ -> - Nothing - ) - - _ -> - Nothing diff --git a/src/Library/Sources/Services/Azure/FileParser.elm b/src/Library/Sources/Services/Azure/FileParser.elm deleted file mode 100755 index ed1c82036..000000000 --- a/src/Library/Sources/Services/Azure/FileParser.elm +++ /dev/null @@ -1,98 +0,0 @@ -module Sources.Services.Azure.FileParser exposing (parseErrorResponse, parseTreeResponse) - -import Sources.Processing exposing (Marker(..), TreeAnswer) -import Sources.Services.Azure.FileMarker as FileMarker exposing (MarkerItem(..)) -import Sources.Services.Common exposing (cleanPath) -import Xml.Decode exposing (..) - - - --- TREE - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse response previousMarker = - response - |> decodeString (treeDecoder previousMarker) - |> Result.withDefault { filePaths = [], marker = TheEnd } - - -treeDecoder : Marker -> Decoder (TreeAnswer Marker) -treeDecoder previousMarker = - usedDirectoryDecoder - |> map cleanPath - |> andThen - (\usedDirectory -> - map2 - (\a b -> ( usedDirectory, a, b )) - (map (List.map <| String.append usedDirectory) filePathsDecoder) - (map (List.map <| String.append usedDirectory) directoryPathsDecoder) - ) - |> andThen - (\( usedDirectory, filePaths, directoryPaths ) -> - previousMarker - |> FileMarker.removeOne - |> FileMarker.concat (List.map Directory directoryPaths) - |> markerDecoder usedDirectory - |> map (\marker -> { filePaths = filePaths, marker = marker }) - ) - - -usedDirectoryDecoder : Decoder String -usedDirectoryDecoder = - stringAttr "DirectoryPath" - - -filePathsDecoder : Decoder (List String) -filePathsDecoder = - string - |> single - |> path [ "Name" ] - |> list - |> path [ "Entries", "File" ] - - -directoryPathsDecoder : Decoder (List String) -directoryPathsDecoder = - string - |> single - |> path [ "Name" ] - |> list - |> path [ "Entries", "Directory" ] - - -markerDecoder : String -> Marker -> Decoder Marker -markerDecoder usedDirectory markerWithDirectories = - map - (\maybeNextMarker -> - case maybeNextMarker of - Just "" -> - markerWithDirectories - - Just marker -> - FileMarker.concat - [ Param { directory = usedDirectory, marker = marker } ] - markerWithDirectories - - Nothing -> - markerWithDirectories - ) - (maybe <| path [ "NextMarker" ] <| single string) - - - --- ERROR - - -parseErrorResponse : String -> Maybe String -parseErrorResponse response = - response - |> decodeString errorMessagesDecoder - |> Result.toMaybe - - -errorMessagesDecoder : Decoder String -errorMessagesDecoder = - string - |> single - |> path [ "Message" ] diff --git a/src/Library/Sources/Services/AzureBlob.elm b/src/Library/Sources/Services/AzureBlob.elm deleted file mode 100644 index d9ca88f05..000000000 --- a/src/Library/Sources/Services/AzureBlob.elm +++ /dev/null @@ -1,168 +0,0 @@ -module Sources.Services.AzureBlob exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) - -{-| Microsoft Azure Blob Service. - -Resources: - - - - --} - -import Common -import Dict -import Http -import Sources exposing (Property, SourceData) -import Sources.Pick -import Sources.Processing exposing (..) -import Sources.Services.Azure.Authorization exposing (..) -import Sources.Services.Azure.BlobParser as Parser -import Sources.Services.Common exposing (cleanPath, noPrep) -import Time - - - --- PROPERTIES --- 📟 - - -defaults = - { name = "Music from Azure Blob Storage" - } - - -{-| The list of properties we need from the user. - -Tuple: (property, label, placeholder, isPassword) -Will be used for the forms. - --} -properties : List Property -properties = - [ { key = "accountName" - , label = "Account name" - , placeholder = "myaccount" - , password = False - } - , { key = "accountKey" - , label = "Account key" - , placeholder = "MXFPDkaN4KBT" - , password = True - } - , { key = "container" - , label = "Container" - , placeholder = "music" - , password = False - } - , { key = "directoryPath" - , label = "Directory (aka. Prefix, Optional)" - , placeholder = "/" - , password = False - } - ] - - -{-| Initial data set. --} -initialData : SourceData -initialData = - Dict.fromList - [ ( "accountName", "" ) - , ( "accountKey", "" ) - , ( "container", "" ) - , ( "directoryPath", "" ) - , ( "name", defaults.name ) - ] - - - --- PREPARATION - - -prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) -prepare _ _ _ _ = - Nothing - - - --- TREE - - -{-| Create a directory tree. - -List all the tracks in the container. -Or a specific directory in the container. - --} -makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg -makeTree srcData marker currentTime resultMsg = - let - directoryPath = - srcData - |> Dict.get "directoryPath" - |> Maybe.withDefault "" - |> cleanPath - - baseParams = - [ ( "maxresults", "1000" ) ] - - params = - case marker of - InProgress s -> - [ ( "marker", s ) ] - - _ -> - [] - - url = - presignedUrl Blob List Get 1 currentTime srcData directoryPath (baseParams ++ params) - in - Http.get - { url = url - , expect = Http.expectStringResponse resultMsg Common.translateHttpResponse - } - - -{-| Re-export parser functions. --} -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse = - noPrep - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse = - Parser.parseTreeResponse - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Parser.parseErrorResponse - - - --- POST - - -{-| Post process the tree results. - -!!! Make sure we only use music files that we can use. - --} -postProcessTree : List String -> List String -postProcessTree = - Sources.Pick.selectMusicFiles - - - --- TRACK URL - - -{-| Create a public url for a file. - -We need this to play the track. -(!) Creates a presigned url that's valid for 48 hours - --} -makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime _ srcData _ pathToFile = - presignedUrl Blob Read Get 48 currentTime srcData pathToFile [] diff --git a/src/Library/Sources/Services/AzureFile.elm b/src/Library/Sources/Services/AzureFile.elm deleted file mode 100644 index e49a68bb8..000000000 --- a/src/Library/Sources/Services/AzureFile.elm +++ /dev/null @@ -1,172 +0,0 @@ -module Sources.Services.AzureFile exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) - -{-| Microsoft Azure File Service. - -Resources: - - - - --} - -import Common -import Dict -import Http -import Sources exposing (Property, SourceData) -import Sources.Pick -import Sources.Processing exposing (..) -import Sources.Services.Azure.Authorization exposing (..) -import Sources.Services.Azure.FileMarker as FileMarker exposing (MarkerItem(..)) -import Sources.Services.Azure.FileParser as Parser -import Sources.Services.Common exposing (cleanPath, noPrep) -import Time - - - --- PROPERTIES --- 📟 - - -defaults = - { name = "Music from Azure File Storage" - } - - -{-| The list of properties we need from the user. - -Tuple: (property, label, placeholder, isPassword) -Will be used for the forms. - --} -properties : List Property -properties = - [ { key = "accountName" - , label = "Account name" - , placeholder = "myaccount" - , password = False - } - , { key = "accountKey" - , label = "Account key" - , placeholder = "MXFPDkaN4KBT" - , password = True - } - , { key = "container" - , label = "Share name" - , placeholder = "music" - , password = False - } - , { key = "directoryPath" - , label = "Directory (aka. Prefix, Optional)" - , placeholder = "/" - , password = False - } - ] - - -{-| Initial data set. --} -initialData : SourceData -initialData = - Dict.fromList - [ ( "accountName", "" ) - , ( "accountKey", "" ) - , ( "container", "" ) - , ( "directoryPath", "" ) - , ( "name", defaults.name ) - ] - - - --- PREPARATION - - -prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) -prepare _ _ _ _ = - Nothing - - - --- TREE - - -{-| Create a directory tree. - -List all the tracks in the container. -Or a specific directory in the container. - --} -makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg -makeTree srcData marker currentTime resultMsg = - let - directoryPathFromSrcData = - srcData - |> Dict.get "directoryPath" - |> Maybe.withDefault "" - |> cleanPath - - baseParams = - [ ( "maxresults", "1000" ) ] - - ( directoryPath, params ) = - case FileMarker.takeOne marker of - Just (Directory directory) -> - Tuple.pair directory [] - - Just (Param param) -> - Tuple.pair param.directory [ ( "marker", param.marker ) ] - - _ -> - Tuple.pair directoryPathFromSrcData [] - - url = - presignedUrl File List Get 1 currentTime srcData directoryPath (baseParams ++ params) - in - Http.get - { url = url - , expect = Http.expectStringResponse resultMsg Common.translateHttpResponse - } - - -{-| Re-export parser functions. --} -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse = - noPrep - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse = - Parser.parseTreeResponse - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Parser.parseErrorResponse - - - --- POST - - -{-| Post process the tree results. - -!!! Make sure we only use music files that we can use. - --} -postProcessTree : List String -> List String -postProcessTree = - Sources.Pick.selectMusicFiles - - - --- TRACK URL - - -{-| Create a public url for a file. - -We need this to play the track. -(!) Creates a presigned url that's valid for 48 hours - --} -makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime _ srcData _ pathToFile = - presignedUrl File Read Get 48 currentTime srcData pathToFile [] diff --git a/src/Library/Sources/Services/Common.elm b/src/Library/Sources/Services/Common.elm deleted file mode 100644 index 286e3e472..000000000 --- a/src/Library/Sources/Services/Common.elm +++ /dev/null @@ -1,49 +0,0 @@ -module Sources.Services.Common exposing (cleanPath, noPrep) - -import Sources exposing (..) -import Sources.Processing exposing (..) -import String.Ext as String -import Time - - - --- PATHS - - -{-| Clean a path. - - >>> cleanPath " " - "" - - >>> cleanPath "/example" - "example/" - - >>> cleanPath "example" - "example/" - - >>> cleanPath "example/" - "example/" - --} -cleanPath : String -> String -cleanPath dirtyPath = - dirtyPath - |> String.trim - |> String.chopStart "/" - |> String.chopEnd "/" - |> (\p -> - if String.isEmpty p then - p - - else - p ++ "/" - ) - - - --- PARSING - - -noPrep : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -noPrep _ _ srcData _ = - { sourceData = srcData, marker = TheEnd } diff --git a/src/Library/Sources/Services/Dropbox.elm b/src/Library/Sources/Services/Dropbox.elm deleted file mode 100644 index 130b52d4f..000000000 --- a/src/Library/Sources/Services/Dropbox.elm +++ /dev/null @@ -1,230 +0,0 @@ -module Sources.Services.Dropbox exposing (authorizationSourceData, authorizationUrl, defaults, getProperDirectoryPath, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) - -{-| Dropbox Service. --} - -import Base64 -import Common -import Dict -import Dict.Ext as Dict -import Http -import Json.Decode -import Json.Encode -import Sources exposing (Property, SourceData) -import Sources.Pick -import Sources.Processing exposing (..) -import Sources.Services.Common exposing (cleanPath, noPrep) -import Sources.Services.Dropbox.Parser as Parser -import Time - - - --- PROPERTIES --- 📟 - - -defaults = - { appKey = "kwsydtrzban41zr" - , name = "Music from Dropbox" - } - - -{-| The list of properties we need from the user. - -Tuple: (property, label, placeholder, isPassword) -Will be used for the forms. - --} -properties : List Property -properties = - [ { key = "directoryPath" - , label = "Directory (Optional)" - , placeholder = "/" - , password = False - } - , { key = "appKey" - , label = "App key" - , placeholder = defaults.appKey - , password = False - } - ] - - -{-| Initial data set. --} -initialData : SourceData -initialData = - Dict.fromList - [ ( "appKey", defaults.appKey ) - , ( "directoryPath", "" ) - , ( "name", defaults.name ) - ] - - - --- AUTHORIZATION - - -{-| Authorization url. --} -authorizationUrl : SourceData -> String -> String -authorizationUrl sourceData origin = - let - encodeData data = - data - |> Dict.toList - |> List.map (Tuple.mapSecond Json.Encode.string) - |> Json.Encode.object - - state = - sourceData - |> encodeData - |> Json.Encode.encode 0 - |> Base64.encode - in - [ ( "response_type", "token" ) - , ( "client_id", Dict.fetch "appKey" "unknown" sourceData ) - , ( "redirect_uri", origin ++ "?path=sources/new/dropbox" ) - , ( "state", state ) - ] - |> Common.queryString - |> String.append "https://www.dropbox.com/oauth2/authorize" - - -{-| Authorization source data. --} -authorizationSourceData : { codeOrToken : Maybe String, state : Maybe String } -> SourceData -authorizationSourceData args = - args.state - |> Maybe.andThen (Base64.decode >> Result.toMaybe) - |> Maybe.withDefault "{}" - |> Json.Decode.decodeString (Json.Decode.dict Json.Decode.string) - |> Result.withDefault Dict.empty - |> Dict.unionFlipped initialData - |> Dict.update "accessToken" (\_ -> args.codeOrToken) - - - --- PREPARATION - - -prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) -prepare _ _ _ _ = - Nothing - - - --- TREE - - -{-| Create a directory tree. - -List all the tracks in the bucket. -Or a specific directory in the bucket. - --} -makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg -makeTree srcData marker _ resultMsg = - let - accessToken = - Dict.fetch "accessToken" "" srcData - - body = - (case marker of - TheBeginning -> - [ ( "limit", Json.Encode.int 2000 ) - , ( "path", Json.Encode.string (getProperDirectoryPath srcData) ) - , ( "recursive", Json.Encode.bool True ) - ] - - InProgress cursor -> - [ ( "cursor", Json.Encode.string cursor ) - ] - - TheEnd -> - [] - ) - |> Json.Encode.object - |> Http.jsonBody - - url = - case marker of - TheBeginning -> - "https://api.dropboxapi.com/2/files/list_folder" - - InProgress _ -> - "https://api.dropboxapi.com/2/files/list_folder/continue" - - TheEnd -> - "" - in - Http.request - { method = "POST" - , headers = [ Http.header "Authorization" ("Bearer " ++ accessToken) ] - , url = url - , body = body - , expect = Http.expectStringResponse resultMsg Common.translateHttpResponse - , timeout = Nothing - , tracker = Nothing - } - - -getProperDirectoryPath : SourceData -> String -getProperDirectoryPath srcData = - let - path = - srcData - |> Dict.get "directoryPath" - |> Maybe.withDefault "" - |> cleanPath - in - if path == "" then - "" - - else - "/" ++ path - - -{-| Re-export parser functions. --} -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse = - noPrep - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse = - Parser.parseTreeResponse - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Parser.parseErrorResponse - - - --- POST - - -{-| Post process the tree results. - -!!! Make sure we only use music files that we can use. - --} -postProcessTree : List String -> List String -postProcessTree = - Sources.Pick.selectMusicFiles - - - --- TRACK URL - - -{-| Create a public url for a file. - -We need this to play the track. - --} -makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ _ srcData _ pathToFile = - "dropbox://" ++ Dict.fetch "accessToken" "" srcData ++ "@" ++ pathToFile diff --git a/src/Library/Sources/Services/Dropbox/Parser.elm b/src/Library/Sources/Services/Dropbox/Parser.elm deleted file mode 100755 index 80dbb1982..000000000 --- a/src/Library/Sources/Services/Dropbox/Parser.elm +++ /dev/null @@ -1,43 +0,0 @@ -module Sources.Services.Dropbox.Parser exposing (parseErrorResponse, parseTreeResponse) - -import Json.Decode exposing (..) -import Sources.Processing exposing (Marker(..), TreeAnswer) - - - --- TREE - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse response _ = - let - hasMore = - decodeString (field "has_more" bool) response - - cursor = - decodeString (field "cursor" string) response - - paths = - decodeString - (field "entries" <| list <| field "path_display" string) - response - in - { filePaths = Result.withDefault [] paths - , marker = - if Result.withDefault False hasMore then - InProgress (Result.withDefault "" cursor) - - else - TheEnd - } - - - --- Error - - -parseErrorResponse : String -> Maybe String -parseErrorResponse response = - response - |> decodeString (field "error_summary" string) - |> Result.toMaybe diff --git a/src/Library/Sources/Services/Google.elm b/src/Library/Sources/Services/Google.elm deleted file mode 100644 index 76db9a089..000000000 --- a/src/Library/Sources/Services/Google.elm +++ /dev/null @@ -1,323 +0,0 @@ -module Sources.Services.Google exposing (authorizationSourceData, authorizationUrl, defaultClientId, defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) - -{-| Google Drive Service. --} - -import Base64 -import Common -import Conditional exposing (..) -import Dict -import Dict.Ext as Dict -import Http -import Json.Decode -import Json.Encode -import Sources exposing (Property, SourceData) -import Sources.Processing exposing (..) -import Sources.Services.Google.Marker as Marker -import Sources.Services.Google.Parser as Parser -import String.Path -import Time - - - --- PROPERTIES --- 📟 - - -defaults = - { clientId = defaultClientId - , clientSecret = "uHBInBeGnA38FOlpLTEyPlUv" - , folderId = "" - , name = "Music from Google Drive" - } - - -defaultClientId : String -defaultClientId = - String.concat - [ "720114869239-74amkqeila5ursobjqvo9c263u1cllhu" - , ".apps.googleusercontent.com" - ] - - -{-| The list of properties we need from the user. - -Tuple: (property, label, placeholder, isPassword) -Will be used for the forms. - --} -properties : List Property -properties = - [ { key = "folderId" - , label = "Folder Id (Optional)" - , placeholder = defaults.folderId - , password = False - } - , { key = "clientId" - , label = "Client Id (Google Console)" - , placeholder = defaults.clientId - , password = False - } - , { key = "clientSecret" - , label = "Client Secret (Google Console)" - , placeholder = defaults.clientSecret - , password = False - } - ] - - -{-| Initial data set. --} -initialData : SourceData -initialData = - Dict.fromList - [ ( "clientId", defaults.clientId ) - , ( "clientSecret", defaults.clientSecret ) - , ( "folderId", defaults.folderId ) - , ( "name", defaults.name ) - ] - - - --- AUTHORIZATION - - -{-| Authorization url. --} -authorizationUrl : SourceData -> String -> String -authorizationUrl sourceData origin = - let - encodeData data = - data - |> Dict.toList - |> List.map (Tuple.mapSecond Json.Encode.string) - |> Json.Encode.object - - state = - sourceData - |> encodeData - |> Json.Encode.encode 0 - |> Base64.encode - in - [ ( "access_type", "offline" ) - , ( "client_id", Dict.fetch "clientId" "unknown" sourceData ) - , ( "prompt", "consent" ) - , ( "redirect_uri", origin ++ "?path=sources/new/google" ) - , ( "response_type", "code" ) - , ( "scope", "https://www.googleapis.com/auth/drive.readonly" ) - , ( "state", state ) - ] - |> Common.queryString - |> String.append "https://accounts.google.com/o/oauth2/v2/auth" - - -{-| Authorization source data. --} -authorizationSourceData : { codeOrToken : Maybe String, state : Maybe String } -> SourceData -authorizationSourceData args = - args.state - |> Maybe.andThen (Base64.decode >> Result.toMaybe) - |> Maybe.withDefault "{}" - |> Json.Decode.decodeString (Json.Decode.dict Json.Decode.string) - |> Result.withDefault Dict.empty - |> Dict.unionFlipped initialData - |> Dict.update "authCode" (\_ -> args.codeOrToken) - - - --- PREPARATION - - -{-| Before processing we need to prepare the source. -In this case this means that we will refresh the `access_token`. -Or if we don't have an access token yet, get one. --} -prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) -prepare origin srcData _ resultMsg = - let - maybeCode = - Dict.get "authCode" srcData - - queryParams = - case maybeCode of - -- Exchange authorization code for access token & request token - Just _ -> - [ ( "client_id", Dict.fetch "clientId" "" srcData ) - , ( "client_secret", Dict.fetch "clientSecret" "" srcData ) - , ( "code", Dict.fetch "authCode" "" srcData ) - , ( "grant_type", "authorization_code" ) - , ( "redirect_uri", origin ++ "?path=sources/new/google" ) - ] - - -- Refresh access token - Nothing -> - [ ( "client_id", Dict.fetch "clientId" "" srcData ) - , ( "client_secret", Dict.fetch "clientSecret" "" srcData ) - , ( "refresh_token", Dict.fetch "refreshToken" "" srcData ) - , ( "grant_type", "refresh_token" ) - ] - - query = - Common.queryString queryParams - - url = - "https://www.googleapis.com/oauth2/v4/token" ++ query - in - (Just << Http.post) - { url = url - , body = Http.emptyBody - , expect = Http.expectStringResponse resultMsg Common.translateHttpResponse - } - - - --- TREE - - -{-| Create a directory tree. - -List all the tracks in the bucket. -Or a specific directory in the bucket. - --} -makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg -makeTree srcData marker _ resultMsg = - let - accessToken = - Dict.fetch "accessToken" "" srcData - - folderId = - Dict.fetch "folderId" "" srcData - - parentId = - marker - |> Marker.takeOne - |> Maybe.map Marker.itemDirectory - |> Maybe.andThen (\dir -> ifThenElse (String.isEmpty dir) Nothing <| Just dir) - |> Maybe.withDefault folderId - |> String.Path.file - - query = - case parentId of - "" -> - [ "mimeType contains 'audio/'" ] - - pid -> - [ "(mimeType contains 'audio/'" - , "or mimeType = 'application/vnd.google-apps.folder')" - , "and ('" ++ pid ++ "' in parents)" - ] - - paramsBase = - [ ( "fields" - , String.join ", " - [ "nextPageToken" - , "files/id" - , "files/mimeType" - , "files/name" - , "files/trashed" - ] - ) - , ( "includeItemsFromAllDrives", "true" ) - , ( "pageSize", "1000" ) - , ( "q", String.concat query ) - , ( "spaces", "drive" ) - , ( "supportsAllDrives", "true" ) - ] - - queryString = - (case Marker.takeOne marker of - Just (Marker.Param { token }) -> - [ ( "pageToken", token ) ] - - _ -> - [] - ) - |> List.append paramsBase - |> Common.queryString - in - Http.request - { method = "GET" - , headers = [ Http.header "Authorization" ("Bearer " ++ accessToken) ] - , url = "https://www.googleapis.com/drive/v3/files" ++ queryString - , body = Http.emptyBody - , expect = Http.expectStringResponse resultMsg Common.translateHttpResponse - , timeout = Nothing - , tracker = Nothing - } - - -{-| Re-export parser functions. --} -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse = - Parser.parsePreparationResponse - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse = - Parser.parseTreeResponse - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Parser.parseErrorResponse - - - --- POST - - -{-| Post process the tree results. - -!!! Make sure we only use music files that we can use. - --} -postProcessTree : List String -> List String -postProcessTree = - identity - - - --- TRACK URL - - -{-| Create a public url for a file. - -We need this to play the track. - --} -makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime srcId srcData _ path = - let - file = - String.Path.file path - - fileId = - file - |> String.split "?" - |> List.head - |> Maybe.withDefault file - - now = - Time.posixToMillis currentTime - - expiresAt = - Dict.fetch "expiresAt" (String.fromInt now) srcData - in - String.concat - [ "google://" - , Dict.fetch "accessToken" "" srcData - , ":" - , expiresAt - , ":" - , Dict.fetch "refreshToken" "" srcData - , ":" - , Dict.fetch "clientId" "" srcData - , ":" - , Dict.fetch "clientSecret" "" srcData - , ":" - , srcId - , "@" - , fileId - ] diff --git a/src/Library/Sources/Services/Google/Marker.elm b/src/Library/Sources/Services/Google/Marker.elm deleted file mode 100644 index 79c2e2976..000000000 --- a/src/Library/Sources/Services/Google/Marker.elm +++ /dev/null @@ -1,164 +0,0 @@ -module Sources.Services.Google.Marker exposing (..) - -{-| Custom `Marker` for the Google Drive API. - -The Google Drive API currently doesn't make a recursive list, -so we have to manage that ourselves. - -This custom marker is a combination of: - - - The standard `nextPageToken` param - - Our custom logic to handle recursive listings - -Example: InProgress "dir=example ¶ param=defaultMarker" - --} - -import Sources.Processing exposing (Marker(..)) - - -type MarkerItem - = Directory String - | Param { directory : String, token : String } - - -separator : String -separator = - " ɑ " - - -prefixer : String -prefixer = - " β " - - -paramSeparator : String -paramSeparator = - " ɣ " - - - --- IN - - -concat : List MarkerItem -> Marker -> Marker -concat list marker = - let - listStringified = - List.map itemToString list - - result = - case marker of - InProgress m -> - [ listStringified, String.split separator m ] - |> List.concat - |> String.join separator - - _ -> - String.join separator listStringified - in - case result of - "" -> - TheEnd - - r -> - InProgress r - - - --- OUT - - -{-| Take the first item and return it. --} -takeOne : Marker -> Maybe MarkerItem -takeOne marker = - case marker of - InProgress m -> - m - |> String.split separator - |> List.head - |> Maybe.andThen stringToItem - - _ -> - Nothing - - -{-| Remove the first item if there is one. --} -removeOne : Marker -> Marker -removeOne marker = - case marker of - InProgress m -> - let - tmp = - m - |> String.split separator - |> List.drop 1 - |> String.join separator - in - case tmp of - "" -> - TheEnd - - x -> - InProgress x - - _ -> - TheEnd - - - --- CONVERSIONS - - -itemDirectory : MarkerItem -> String -itemDirectory item = - case item of - Directory dir -> - dir - - Param { directory } -> - directory - - -itemToString : MarkerItem -> String -itemToString item = - case item of - Directory d -> - "dir" ++ prefixer ++ d - - Param { directory, token } -> - "par" ++ prefixer ++ directory ++ paramSeparator ++ token - - -stringToItem : String -> Maybe MarkerItem -stringToItem string = - let - exploded = - String.split prefixer string - in - case List.head exploded of - Just "dir" -> - exploded - |> List.drop 1 - |> String.join prefixer - |> Directory - |> Just - - Just "par" -> - exploded - |> List.drop 1 - |> String.join prefixer - |> String.split paramSeparator - |> (\x -> - case x of - [ dir, tok ] -> - Just (Param { directory = dir, token = tok }) - - _ -> - Nothing - ) - - _ -> - Nothing diff --git a/src/Library/Sources/Services/Google/Parser.elm b/src/Library/Sources/Services/Google/Parser.elm deleted file mode 100755 index ed2ad27a1..000000000 --- a/src/Library/Sources/Services/Google/Parser.elm +++ /dev/null @@ -1,185 +0,0 @@ -module Sources.Services.Google.Parser exposing (..) - -import Dict -import Json.Decode exposing (..) -import Json.Decode.Ext exposing (..) -import Maybe.Extra -import Sources exposing (SourceData) -import Sources.Pick -import Sources.Processing exposing (Marker(..), PrepationAnswer, TreeAnswer) -import Sources.Services.Google.Marker as Marker -import String.Path -import Time - - - --- PREPARATION - - -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse response currentTimePosix srcData _ = - let - newAccessToken = - response - |> decodeString (field "access_token" string) - |> Result.withDefault "" - - currentTime = - -- Current time in milliseconds - Time.posixToMillis currentTimePosix - - expiresAt = - -- Unix timestamp in milliseconds - response - |> decodeString (field "expires_in" int) - -- time in seconds - |> Result.withDefault 2500 - |> (\s -> currentTime + s * 1000) - - maybeRefreshToken = - response - |> decodeString (maybe <| field "refresh_token" string) - |> Result.toMaybe - |> Maybe.Extra.join - - refreshTokenUpdater dict = - case maybeRefreshToken of - Just refreshToken -> - Dict.insert "refreshToken" refreshToken dict - - Nothing -> - dict - in - srcData - |> Dict.insert "accessToken" newAccessToken - |> Dict.insert "expiresAt" (String.fromInt expiresAt) - |> refreshTokenUpdater - |> Dict.remove "authCode" - |> (\s -> { sourceData = s, marker = TheEnd }) - - - --- TREE - - -type alias Properties = - { id : String, name : String } - - -type Item - = File Properties - | Directory Properties - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse response previousMarker = - let - nextPageToken = - response - |> decodeString (maybe <| field "nextPageToken" string) - |> Result.toMaybe - |> Maybe.Extra.join - - items = - response - |> decodeString (field "files" <| listIgnore itemDecoder) - |> Result.withDefault [] - - usedDirectory = - previousMarker - |> Marker.takeOne - |> Maybe.map Marker.itemDirectory - |> Maybe.withDefault "" - - usedPath = - usedDirectory - |> String.Path.dropRight 1 - |> String.Path.addSuffix - - ( directories, files ) = - List.partition - (\item -> - case item of - Directory _ -> - True - - File _ -> - False - ) - items - in - { filePaths = - files - |> List.map itemProperties - |> List.filter (.name >> Sources.Pick.isMusicFile) - |> List.map (\{ id, name } -> usedPath ++ id ++ "?name=" ++ name) - , marker = - previousMarker - |> Marker.removeOne - |> Marker.concat - (List.map - (itemProperties - >> (\props -> props.name ++ "/" ++ props.id) - >> String.append usedPath - >> Marker.Directory - ) - directories - ) - |> (case nextPageToken of - Just token -> - { directory = usedDirectory, token = token } - |> Marker.Param - |> List.singleton - |> Marker.concat - - Nothing -> - identity - ) - } - - -itemDecoder : Decoder Item -itemDecoder = - map4 - (\id name mime _ -> - case mime of - "application/vnd.google-apps.folder" -> - Directory { id = id, name = name } - - _ -> - File { id = id, name = name } - ) - (field "id" string) - (field "name" string) - (field "mimeType" string) - (andThen - (\b -> - if b then - fail "Exclude deleted files" - - else - succeed b - ) - (field "trashed" bool) - ) - - -itemProperties : Item -> Properties -itemProperties item = - case item of - Directory props -> - props - - File props -> - props - - - --- ERROR - - -parseErrorResponse : String -> Maybe String -parseErrorResponse response = - response - |> decodeString (at [ "error", "message" ] string) - |> Result.toMaybe diff --git a/src/Library/Sources/Services/Ipfs.elm b/src/Library/Sources/Services/Ipfs.elm deleted file mode 100644 index af17a8649..000000000 --- a/src/Library/Sources/Services/Ipfs.elm +++ /dev/null @@ -1,318 +0,0 @@ -module Sources.Services.Ipfs exposing (..) - -{-| IPFS Service. - -Resources: - - - - --} - -import Common exposing (boolFromString, boolToString) -import Conditional exposing (ifThenElse) -import Dict -import Dict.Ext as Dict -import Http -import Json.Decode as Json -import Sources exposing (Property, SourceData) -import Sources.Processing exposing (..) -import Sources.Services.Ipfs.Marker as Marker -import Sources.Services.Ipfs.Parser as Parser -import String.Ext as String -import Task -import Time -import Url - - - --- PROPERTIES --- 📟 - - -defaults = - { gateway = "" - , local = boolToString False - , name = "Music from IPFS" - , ipns = boolToString False - } - - -defaultGateway = - "https://ipfs.io" - - -{-| The list of properties we need from the user. - -Tuple: (property, label, placeholder, isPassword) -Will be used for the forms. - --} -properties : List Property -properties = - [ { key = "directoryHash" - , label = "Directory hash / DNSLink domain" - , placeholder = "QmVLDAhCY3X9P2u" - , password = False - } - , { key = "ipns" - , label = "Resolve using IPNS" - , placeholder = defaults.ipns - , password = False - } - , { key = "gateway" - , label = "Gateway (Optional)" - , placeholder = defaultGateway - , password = False - } - , { key = "local" - , label = "Resolve IPNS locally" - , placeholder = defaults.local - , password = False - } - ] - - -{-| Initial data set. --} -initialData : SourceData -initialData = - Dict.fromList - [ ( "directoryHash", "" ) - , ( "gateway", defaults.gateway ) - , ( "ipns", defaults.ipns ) - , ( "local", defaults.local ) - , ( "name", defaults.name ) - ] - - - --- PREPARATION - - -prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) -prepare _ srcData _ toMsg = - let - domainName = - srcData - |> Dict.get "directoryHash" - |> Maybe.withDefault "" - |> String.chopStart "http://" - |> String.chopStart "https://" - |> String.chopEnd "/" - |> String.chopStart "_dnslink." - in - if isDnsLink srcData then - (Just << Http.request) - { method = "POST" - , headers = [] - , url = extractGateway srcData ++ "/api/v0/dns?arg=" ++ domainName - , body = Http.emptyBody - , expect = Http.expectString toMsg - , timeout = Nothing - , tracker = Nothing - } - - else - Nothing - - - --- TREE - - -{-| Create a directory tree. --} -makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg -makeTree srcData marker _ resultMsg = - let - gateway = - extractGateway srcData - - resolveWithIpns = - case marker of - InProgress _ -> - False - - _ -> - srcData - |> Dict.fetch "ipns" defaults.ipns - |> boolFromString - - resolveLocally = - srcData - |> Dict.fetch "local" defaults.local - |> boolFromString - |> (\b -> ifThenElse b "true" "false") - - root = - rootHash srcData - - path = - case marker of - InProgress _ -> - marker - |> Marker.takeOne - |> Maybe.map (\p -> root ++ "/" ++ p) - |> Maybe.withDefault "" - - _ -> - root - in - (if resolveWithIpns then - Http.task - { method = "POST" - , headers = [] - , url = gateway ++ "/api/v0/name/resolve?arg=" ++ encodedPath path ++ "&local=" ++ resolveLocally ++ "&encoding=json" - , body = Http.emptyBody - , resolver = Http.stringResolver ipnsResolver - , timeout = Just (60 * 15 * 1000) - } - - else - Task.succeed { ipfsPath = path } - ) - |> Task.andThen - (\{ ipfsPath } -> - Http.task - { method = "POST" - , headers = [] - , url = gateway ++ "/api/v0/ls?arg=" ++ encodedPath ipfsPath ++ "&encoding=json" - , body = Http.emptyBody - , resolver = Http.stringResolver Common.translateHttpResponse - , timeout = Just (60 * 15 * 1000) - } - ) - |> Task.attempt resultMsg - - -ipnsResolver : Http.Response String -> Result Http.Error { ipfsPath : String } -ipnsResolver response = - case response of - Http.BadUrl_ u -> - Err (Http.BadUrl u) - - Http.Timeout_ -> - Err Http.Timeout - - Http.NetworkError_ -> - Err Http.NetworkError - - Http.BadStatus_ _ body -> - Err (Http.BadBody body) - - Http.GoodStatus_ _ body -> - body - |> Json.decodeString (Json.field "Path" Json.string) - |> Result.map (\path -> { ipfsPath = String.chopStart "/ipfs/" path }) - |> Result.mapError (Json.errorToString >> Http.BadBody) - - -{-| Re-export parser functions. --} -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse = - Parser.parseDnsLookup - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse = - Parser.parseTreeResponse - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Parser.parseErrorResponse - - - --- POST - - -{-| Post process the tree results. - -!!! Make sure we only use music files that we can use. - --} -postProcessTree : List String -> List String -postProcessTree = - identity - - - --- TRACK URL - - -{-| Create a public url for a file. - -We need this to play the track. - --} -makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ _ srcData _ path = - if not (String.contains "/" path) && not (String.contains "." path) then - -- If it still uses the old way of doing things - -- (ie. each path was a cid) - extractGateway srcData ++ "/ipfs/" ++ path - - else - -- Or the new way - extractGateway srcData ++ "/ipfs/" ++ rootHash srcData ++ "/" ++ encodedPath path - - - --- ⚗️ - - -encodedPath : String -> String -encodedPath path = - path - |> String.split "/" - |> List.map Url.percentEncode - |> String.join "/" - - -extractGateway : SourceData -> String -extractGateway srcData = - srcData - |> Dict.get "gateway" - |> Maybe.map String.trim - |> Maybe.andThen - (\s -> - case s of - "" -> - Nothing - - _ -> - Just s - ) - |> Maybe.map (String.chopEnd "/") - |> Maybe.withDefault defaultGateway - - -isDnsLink : SourceData -> Bool -isDnsLink srcData = - srcData - |> Dict.get "directoryHash" - |> Maybe.map pathIsDnsLink - |> Maybe.withDefault False - - -pathIsDnsLink : String -> Bool -pathIsDnsLink = - String.contains "." - - -rootHash : SourceData -> String -rootHash srcData = - srcData - |> Dict.get "directoryHash" - |> Maybe.andThen - (\path -> - if pathIsDnsLink path then - Dict.get "directoryHashFromDnsLink" srcData - - else - Just path - ) - |> Maybe.withDefault "" - |> String.chopEnd "/" diff --git a/src/Library/Sources/Services/Ipfs/Marker.elm b/src/Library/Sources/Services/Ipfs/Marker.elm deleted file mode 100755 index 1be8f727c..000000000 --- a/src/Library/Sources/Services/Ipfs/Marker.elm +++ /dev/null @@ -1,93 +0,0 @@ -module Sources.Services.Ipfs.Marker exposing (concat, removeOne, separator, takeOne) - -{-| Marker stuff for IPFS. - -The IPFS API currently doesn't have a way to return a tree. -So we have build one ourselves. - -How it works: - -1. We list the objects in a given directory -2. We make a list of the sub directories -3. The marker becomes either: - - InProgress `hashOfSubDirA/hashOfSubDirB/hashOfSubDirC` - - TheEnd -4. If the marker was of the type `InProgress`, - the next request will make a list of the objects in `hashOfSubDirA`. - And so on ... - --} - -import Sources.Processing exposing (Marker(..)) - - -separator : String -separator = - " ɑ " - - - --- In - - -concat : List String -> Marker -> Marker -concat list marker = - let - result = - case marker of - InProgress m -> - [ list, String.split separator m ] - |> List.concat - |> String.join separator - - _ -> - String.join separator list - in - case result of - "" -> - TheEnd - - r -> - InProgress r - - - --- Out - - -{-| Take the first item and return it. --} -takeOne : Marker -> Maybe String -takeOne marker = - case marker of - InProgress m -> - m - |> String.split separator - |> List.head - - _ -> - Nothing - - -{-| Remove the first item if there is one. --} -removeOne : Marker -> Marker -removeOne marker = - case marker of - InProgress m -> - let - tmp = - m - |> String.split separator - |> List.drop 1 - |> String.join separator - in - case tmp of - "" -> - TheEnd - - x -> - InProgress x - - _ -> - TheEnd diff --git a/src/Library/Sources/Services/Ipfs/Parser.elm b/src/Library/Sources/Services/Ipfs/Parser.elm deleted file mode 100755 index 8175f757c..000000000 --- a/src/Library/Sources/Services/Ipfs/Parser.elm +++ /dev/null @@ -1,146 +0,0 @@ -module Sources.Services.Ipfs.Parser exposing (Link, linkDecoder, parseDnsLookup, parseErrorResponse, parseTreeResponse, treeDecoder) - -import Dict -import Json.Decode exposing (..) -import Sources exposing (SourceData) -import Sources.Pick exposing (isMusicFile) -import Sources.Processing exposing (Marker(..), PrepationAnswer, TreeAnswer) -import Sources.Services.Ipfs.Marker as Marker -import String.Ext as String -import Time - - - --- PREPARATION - - -parseDnsLookup : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parseDnsLookup response _ srcData _ = - case decodeString dnsResultDecoder response of - Ok path -> - srcData - |> Dict.insert "directoryHashFromDnsLink" (String.chopStart "/ipfs/" path) - |> (\s -> { sourceData = s, marker = TheEnd }) - - Err _ -> - { sourceData = srcData, marker = TheEnd } - - -dnsResultDecoder : Decoder String -dnsResultDecoder = - oneOf - [ at [ "Path" ] string - , cloudflareDnsResultDecoder - ] - - -cloudflareDnsResultDecoder : Decoder String -cloudflareDnsResultDecoder = - string - |> at [ "Answer", "0", "data" ] - |> map - (\txt -> - txt - |> String.chopEnd "\"" - |> String.chopStart "\"" - |> String.chopStart "dnslink=/ipfs/" - ) - - - --- TREE - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse response previousMarker = - let - prefix = - case previousMarker of - TheBeginning -> - "" - - _ -> - response - |> decodeString prefixDecoder - |> Result.map - (String.chopStart "/ipfs/" - >> String.split "/" - >> List.drop 1 - >> String.join "/" - ) - |> Result.map - (\s -> - if String.isEmpty s then - "" - - else - s ++ "/" - ) - |> Result.withDefault "" - - links = - case decodeString treeDecoder response of - Ok l -> - l - - Err _ -> - [] - - dirs = - links - |> List.filter (.typ >> (==) 1) - |> List.map (\l -> prefix ++ l.name) - - files = - links - |> List.filter (.typ >> (==) 2) - |> List.filter (.name >> isMusicFile) - |> List.map (\l -> prefix ++ l.name) - in - { filePaths = - files - , marker = - previousMarker - |> Marker.removeOne - |> Marker.concat dirs - } - - -prefixDecoder : Decoder String -prefixDecoder = - field "Objects" <| index 0 <| field "Hash" <| string - - -treeDecoder : Decoder (List Link) -treeDecoder = - field "Objects" <| index 0 <| field "Links" <| list linkDecoder - - - --- LINKS - - -type alias Link = - { hash : String - , name : String - , typ : Int - } - - -linkDecoder : Decoder Link -linkDecoder = - map3 Link - (field "Hash" string) - (field "Name" string) - (field "Type" int) - - - --- ERRORS - - -parseErrorResponse : String -> Maybe String -parseErrorResponse response = - response - |> decodeString (field "Message" string) - |> Result.toMaybe diff --git a/src/Library/Sources/Services/WebDav.elm b/src/Library/Sources/Services/WebDav.elm deleted file mode 100644 index 3d1f6dfe4..000000000 --- a/src/Library/Sources/Services/WebDav.elm +++ /dev/null @@ -1,214 +0,0 @@ -module Sources.Services.WebDav exposing (defaults, initialData, makeTrackUrl, makeTree, parseErrorResponse, parsePreparationResponse, parseTreeResponse, postProcessTree, prepare, properties) - -{-| IPFS Service. - -Resources: - - - - --} - -import Base64 -import Common -import Dict -import Dict.Ext as Dict -import Http -import Sources exposing (Property, SourceData) -import Sources.Pick exposing (selectMusicFiles) -import Sources.Processing exposing (..) -import Sources.Services.Common exposing (noPrep) -import Sources.Services.WebDav.Marker as Marker -import Sources.Services.WebDav.Parser as Parser -import String.Ext as String -import Time -import Url - - - --- PROPERTIES --- 📟 - - -defaults = - { name = "Music from WebDAV" - } - - -{-| The list of properties we need from the user. - -Tuple: (property, label, placeholder, isPassword) -Will be used for the forms. - --} -properties : List Property -properties = - [ { key = "url" - , label = "Host URL" - , placeholder = "https://demo.nextcloud.com" - , password = False - } - , { key = "directoryPath" - , label = "Directory (Optional)" - , placeholder = "/icidasset/remote.php/webdav/" - , password = False - } - , { key = "username" - , label = "Username (Optional)" - , placeholder = "" - , password = False - } - , { key = "password" - , label = "Password (Optional)" - , placeholder = "" - , password = True - } - ] - - -{-| Initial data set. --} -initialData : SourceData -initialData = - Dict.fromList - [ ( "directoryPath", "" ) - , ( "url", "" ) - , ( "username", "" ) - , ( "password", "" ) - , ( "name", defaults.name ) - ] - - - --- PREPARATION - - -prepare : String -> SourceData -> Marker -> (Result Http.Error String -> msg) -> Maybe (Cmd msg) -prepare _ _ _ _ = - Nothing - - - --- TREE - - -{-| Create a directory tree. --} -makeTree : SourceData -> Marker -> Time.Posix -> (Result Http.Error String -> msg) -> Cmd msg -makeTree srcData marker _ resultMsg = - let - directory = - case marker of - InProgress _ -> - marker - |> Marker.takeOne - |> Maybe.withDefault "" - - _ -> - srcData - |> Dict.get "directoryPath" - |> Maybe.withDefault "" - - username = - Dict.fetch "username" "" srcData - - password = - Dict.fetch "password" "" srcData - - auth = - "Basic " ++ Base64.encode (username ++ ":" ++ password) - in - Http.request - { method = "PROPFIND" - , headers = [ Http.header "Authorization" auth, Http.header "Depth" "1" ] - , url = url { addAuth = False } srcData directory - , body = Http.emptyBody - , expect = Http.expectStringResponse resultMsg Common.translateHttpResponse - , timeout = Nothing - , tracker = Nothing - } - - -{-| Re-export parser functions. --} -parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse = - noPrep - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse = - Parser.parseTreeResponse - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Parser.parseErrorResponse - - - --- POST - - -{-| Post process the tree results. - -!!! Make sure we only use music files that we can use. - --} -postProcessTree : List String -> List String -postProcessTree = - selectMusicFiles - - - --- TRACK URL - - -{-| Create a public url for a file. - -We need this to play the track. - --} -makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ _ srcData _ filePath = - url { addAuth = True } srcData filePath - - - --- COMMON - - -url : { addAuth : Bool } -> SourceData -> String -> String -url { addAuth } srcData path = - let - host = - String.chopEnd "/" (Dict.fetch "url" "" srcData) - - username = - Dict.fetch "username" "" srcData - - password = - Dict.fetch "password" "" srcData - - authPrefix = - case ( username, password ) of - ( "", "" ) -> - "" - - ( u, p ) -> - u ++ ":" ++ p - - authBit = - if addAuth && String.length authPrefix > 0 then - "?basic_auth=" ++ Url.percentEncode (Base64.encode authPrefix) - - else - "" - - encodedPath = - path - |> String.chopStart "/" - |> String.split "/" - |> List.map Url.percentEncode - |> String.join "/" - in - host ++ "/" ++ encodedPath ++ authBit diff --git a/src/Library/Sources/Services/WebDav/Marker.elm b/src/Library/Sources/Services/WebDav/Marker.elm deleted file mode 100644 index 105c36903..000000000 --- a/src/Library/Sources/Services/WebDav/Marker.elm +++ /dev/null @@ -1,32 +0,0 @@ -module Sources.Services.WebDav.Marker exposing (concat, removeOne, takeOne) - -{-| Custom `Marker`. - -WebDAV doesn't make a recursive list, so we have to manage that ourselves. -The spec seems to have `Depth: Infinity` which should allow that. -But a lot of clients don't implement it. -This basically re-exports `Sources.Services.IPFS.Marker`. - --} - -import Sources.Services.Ipfs.Marker as IpfsMarker - - - --- IN - - -concat = - IpfsMarker.concat - - - --- OUT - - -takeOne = - IpfsMarker.takeOne - - -removeOne = - IpfsMarker.removeOne diff --git a/src/Library/Sources/Services/WebDav/Parser.elm b/src/Library/Sources/Services/WebDav/Parser.elm deleted file mode 100644 index 8e2d359f9..000000000 --- a/src/Library/Sources/Services/WebDav/Parser.elm +++ /dev/null @@ -1,135 +0,0 @@ -module Sources.Services.WebDav.Parser exposing (..) - -import Maybe.Extra as Maybe -import Sources.Pick exposing (isMusicFile) -import Sources.Processing exposing (Marker, TreeAnswer) -import Sources.Services.Ipfs.Marker as Marker -import String.Ext as String -import Url -import Xml.Decode exposing (..) -import XmlParser - - - --- TREE - - -parseTreeResponse : String -> Marker -> TreeAnswer Marker -parseTreeResponse response previousMarker = - let - currentDir = - Maybe.withDefault "//" (Marker.takeOne previousMarker) - - parseResult = - XmlParser.parse response - - namespace = - parseResult - |> Result.map - (\xml -> - case xml.root of - XmlParser.Element nodeName _ _ -> - nodeName - |> String.split ":" - |> List.head - - _ -> - Nothing - ) - |> Result.withDefault Nothing - |> (\maybe -> - case maybe of - Just n -> - n ++ ":" - - Nothing -> - if String.contains " decodeString (treeDecoder namespace) - |> Result.withDefault [] - |> List.map Url.percentDecode - |> Maybe.values - |> List.filter ((/=) currentDir) - - ( dirs, files ) = - List.partition (String.endsWith "/") entries - in - { filePaths = - List.map (String.chopStart "/") files - , marker = - previousMarker - |> Marker.removeOne - |> Marker.concat dirs - } - - -treeDecoder : String -> Decoder (List String) -treeDecoder namespace = - path - [ namespace ++ "response" ] - (leakyList <| treeItemDecoder namespace) - - -treeItemDecoder : String -> Decoder String -treeItemDecoder namespace = - let - withNamespace = - String.append namespace - in - string - |> single - |> path [ withNamespace "href" ] - |> andThen - (\href -> - oneOf - [ -- Audio - -------- - string - |> single - |> path [ withNamespace "propstat", withNamespace "prop", withNamespace "getcontenttype" ] - |> andThen (mustBeAudio href) - |> map (\_ -> href) - - -- Directory - ------------ - , string - |> single - |> path [ withNamespace "propstat", withNamespace "prop", withNamespace "resourcetype", withNamespace "collection" ] - |> andThen - (\_ -> - if String.endsWith "/@eaDir/" href then - fail "Ignore Synology metadata" - - else - succeed href - ) - ] - ) - - -mustBeAudio : String -> String -> Decoder String -mustBeAudio href contentType = - if isMusicFile href then - succeed contentType - - else if String.startsWith "audio/" contentType then - succeed contentType - - else - fail "Ignore this, not an audio file" - - - --- ERROR - - -parseErrorResponse : String -> Maybe String -parseErrorResponse = - Just diff --git a/src/Library/String/Ext.elm b/src/Library/String/Ext.elm deleted file mode 100644 index f16b45a73..000000000 --- a/src/Library/String/Ext.elm +++ /dev/null @@ -1,46 +0,0 @@ -module String.Ext exposing (..) - -{-| Flipped version of `append`. --} - --- 🔱 - - -{-| Flipped version of `append`. --} -addSuffix : String -> String -> String -addSuffix a b = - String.append b a - - -{-| Chop something from the end of a string until it's not there anymore. --} -chopEnd : String -> String -> String -chopEnd needle str = - if String.endsWith needle str then - str - |> String.dropRight (String.length needle) - |> chopEnd needle - - else - str - - -{-| Chop something from the beginning of a string until it's not there anymore. --} -chopStart : String -> String -> String -chopStart needle str = - if String.startsWith needle str then - str - |> String.dropLeft (String.length needle) - |> chopStart needle - - else - str - - -{-| Join a list of Strings with a space in between. --} -joinWithSpace : List String -> String -joinWithSpace = - String.join " " diff --git a/src/Library/String/Path.elm b/src/Library/String/Path.elm deleted file mode 100644 index becbe6d9b..000000000 --- a/src/Library/String/Path.elm +++ /dev/null @@ -1,48 +0,0 @@ -module String.Path exposing (..) - -import List.Extra as List - - - --- ⛩ - - -sep : String -sep = - "/" - - - --- 🔱 - - -addSuffix : String -> String -addSuffix path = - case path of - "" -> - "" - - p -> - p ++ sep - - -{-| Drop an amount of path segments from the right side. - - >>> dropRight 5 "a/b/c/d/e/f" - "a" - --} -dropRight : Int -> String -> String -dropRight int path = - path - |> String.split sep - |> (\l -> List.take (List.length l - int) l) - |> String.join sep - - -file : String -> String -file path = - path - |> String.split sep - |> List.last - |> Maybe.withDefault path diff --git a/src/Library/Syncing.elm b/src/Library/Syncing.elm deleted file mode 100644 index 19622e3fc..000000000 --- a/src/Library/Syncing.elm +++ /dev/null @@ -1,198 +0,0 @@ -module Syncing exposing (LocalConfig, RemoteConfig, task) - -import Json.Decode as Decode -import Json.Encode as Json -import Maybe.Extra as Maybe -import Task exposing (Task) -import Task.Extra as Task -import Time -import Time.Ext as Time -import User.Layer as User exposing (..) - - - --- 🌳 - - -type alias LocalConfig = - { localData : HypaethralData - , saveLocal : HypaethralBit -> Decode.Value -> Task String () - } - - -type alias RemoteConfig = - { retrieve : HypaethralBit -> Task String (Maybe Decode.Value) - , save : HypaethralBit -> Decode.Value -> Task String () - } - - - --- 🛠 - - -{-| Syncs all hypaethral data. - -Returns `Nothing` if the local data is preferred. - -🏝️ LOCAL -🛰️ REMOTE - -1. Try to pull remote `modified.json` timestamp - a. If newer, continue (#2) - b. If same, do nothing - c. If older, or not present, prefer local data 🏝️ (stop & push) -2. Try to download all remote data - a. If any remote data, continue (#3) - b. If none, prefer local data 🏝️ (stop & push) -3. Decode remote data and compare timestamps - a. If newer, use remote data 🛰️ - b. If same, do nothing - c. If older, prefer local data 🏝️ (stop & push) - d. If no timestamps, if local data, prefer local 🏝️ (stop & push), otherwise remote 🛰️ - --} -task : - Task String a - -> LocalConfig - -> RemoteConfig - -> Task String (Maybe HypaethralData) -task initialTask localConfig remoteConfig = - initialTask - |> Task.andThen - (\_ -> - remoteConfig.retrieve ModifiedAt - ) - |> Task.andThen - (\maybeModifiedAt -> - let - maybeRemoteModifiedAt = - Maybe.andThen - (Decode.decodeValue Time.decoder >> Result.toMaybe) - maybeModifiedAt - in - case ( maybeRemoteModifiedAt, localConfig.localData.modifiedAt ) of - ( Just remoteModifiedAt, Just localModifiedAt ) -> - if Time.posixToMillis remoteModifiedAt == Time.posixToMillis localModifiedAt then - -- 🏝️ - Task.succeed Nothing - - else if Time.posixToMillis remoteModifiedAt > Time.posixToMillis localModifiedAt then - -- 🛰️ - fetchRemote localConfig remoteConfig - - else - -- 🏝️ → 🛰️ - pushLocalToRemote localConfig remoteConfig { return = Nothing } - - ( Just _, Nothing ) -> - -- 🛰️ - fetchRemote localConfig remoteConfig - - ( Nothing, _ ) -> - -- 🛰️ - fetchRemote localConfig remoteConfig - ) - - -fetchRemote : - LocalConfig - -> RemoteConfig - -> Task String (Maybe HypaethralData) -fetchRemote localConfig remoteConfig = - let - { localData, saveLocal } = - localConfig - - { retrieve } = - remoteConfig - - saveLocally data = - data - |> User.saveHypaethralData saveLocal - |> Task.map (\_ -> Just data) - - noLocalData = - List.isEmpty localData.sources - && List.isEmpty localData.favourites - && List.isEmpty localData.playlists - in - retrieve - |> User.retrieveHypaethralData - |> Task.andThen - (\list -> - let - remoteHasExistingData = - List.any (Tuple.second >> Maybe.isJust) list - in - if remoteHasExistingData then - -- 🛰️ - Task.succeed list - - else - -- 🏝️ → 🛰️ - pushLocalToRemote localConfig remoteConfig { return = list } - ) - |> Task.andThen - (\list -> - -- Decode remote - list - |> List.map (\( a, b ) -> ( hypaethralBitKey a, Maybe.withDefault Json.null b )) - |> Json.object - |> User.decodeHypaethralData - |> Task.fromResult - |> Task.mapError Decode.errorToString - ) - |> Task.andThen - (\remoteData -> - -- Compare modifiedAt timestamps - case ( remoteData.modifiedAt, localData.modifiedAt ) of - ( Just remoteModifiedAt, Just localModifiedAt ) -> - if Time.posixToMillis remoteModifiedAt == Time.posixToMillis localModifiedAt then - -- 🏝️ - Task.succeed Nothing - - else if Time.posixToMillis remoteModifiedAt > Time.posixToMillis localModifiedAt then - -- 🛰️ - saveLocally remoteData - - else - -- 🏝️ → 🛰️ - pushLocalToRemote localConfig remoteConfig { return = Nothing } - - ( Just _, Nothing ) -> - -- 🛰️ - saveLocally remoteData - - ( Nothing, Just _ ) -> - -- 🏝️ → 🛰️ - pushLocalToRemote localConfig remoteConfig { return = Nothing } - - _ -> - if noLocalData then - -- 🛰️ - saveLocally remoteData - - else - -- 🏝️ - Task.succeed Nothing - ) - - - --- ㊙️ - - -pushLocalToRemote : LocalConfig -> RemoteConfig -> { return : a } -> Task String a -pushLocalToRemote localConfig remoteConfig { return } = - localConfig.localData - |> User.encodedHypaethralDataList - |> (case localConfig.localData.modifiedAt of - Just localModifiedAt -> - (::) ( ModifiedAt, Time.encode localModifiedAt ) - - Nothing -> - identity - ) - |> List.map (\( bit, data ) -> remoteConfig.save bit data) - |> Task.sequence - |> Task.map (\_ -> return) diff --git a/src/Library/Syncing/Services/Dropbox/Token.elm b/src/Library/Syncing/Services/Dropbox/Token.elm deleted file mode 100644 index 21b07d6bf..000000000 --- a/src/Library/Syncing/Services/Dropbox/Token.elm +++ /dev/null @@ -1,18 +0,0 @@ -module Syncing.Services.Dropbox.Token exposing (..) - -import Time - - -isExpired : { currentTime : Time.Posix, expiresAt : Int } -> Bool -isExpired { currentTime, expiresAt } = - let - currentTimeInSeconds = - Time.posixToMillis currentTime // 1000 - - currentTimeWithOffset = - -- We add 60 seconds here because we only get the current time every minute, - -- so there's always the chance the "current time" is 1-60 seconds behind. - currentTimeInSeconds + 60 - in - -- If the access token is expired - currentTimeWithOffset >= expiresAt diff --git a/src/Library/Task/Extra.elm b/src/Library/Task/Extra.elm deleted file mode 100644 index a18e786f8..000000000 --- a/src/Library/Task/Extra.elm +++ /dev/null @@ -1,28 +0,0 @@ -module Task.Extra exposing (do, doDelayed, fromResult) - -import Process -import Task - - - --- 🔱 - - -do : msg -> Cmd msg -do msg = - Task.perform identity (Task.succeed msg) - - -doDelayed : Float -> msg -> Cmd msg -doDelayed delay msg = - Task.perform (always msg) (Process.sleep delay) - - -fromResult : Result error value -> Task.Task error value -fromResult result = - case result of - Ok v -> - Task.succeed v - - Err e -> - Task.fail e diff --git a/src/Library/TaskPort/Extra.elm b/src/Library/TaskPort/Extra.elm deleted file mode 100644 index 8a1792f52..000000000 --- a/src/Library/TaskPort/Extra.elm +++ /dev/null @@ -1,18 +0,0 @@ -module TaskPort.Extra exposing (..) - -import TaskPort - - -errorToStringCustom : TaskPort.Error -> String -errorToStringCustom err = - case err of - TaskPort.JSError jsErr -> - case jsErr of - TaskPort.ErrorObject _ errRecord -> - errRecord.message - - TaskPort.ErrorValue _ -> - TaskPort.errorToString err - - TaskPort.InteropError _ -> - TaskPort.errorToString err diff --git a/src/Library/Theme.elm b/src/Library/Theme.elm deleted file mode 100644 index ff61848ba..000000000 --- a/src/Library/Theme.elm +++ /dev/null @@ -1,28 +0,0 @@ -module Theme exposing (..) - -import Html exposing (Html) -import Json.Decode as Decode -import Json.Encode as Encode -import Material.Icons.Types exposing (Icon) - - -type alias Id = - { id : String } - - -type alias Theme msg model = - { id : String - , title : String - , icon : Icon msg - , view : model -> Html msg - } - - -idDecoder : Decode.Decoder Id -idDecoder = - Decode.map (\s -> { id = s }) Decode.string - - -encodeId : Id -> Encode.Value -encodeId { id } = - Encode.string id diff --git a/src/Library/Time/Ext.elm b/src/Library/Time/Ext.elm deleted file mode 100644 index b91d31aed..000000000 --- a/src/Library/Time/Ext.elm +++ /dev/null @@ -1,104 +0,0 @@ -module Time.Ext exposing (decoder, default, encode, monthName, monthNumber) - -import Json.Decode as Decode exposing (Decoder) -import Json.Encode as Json -import Time exposing (Month(..)) - - - --- 🔱 - - -decoder : Decoder Time.Posix -decoder = - Decode.map Time.millisToPosix Decode.int - - -default : Time.Posix -default = - Time.millisToPosix 0 - - -encode : Time.Posix -> Json.Value -encode time = - Json.int (Time.posixToMillis time) - - -monthName : Time.Month -> String -monthName month = - case month of - Jan -> - "January" - - Feb -> - "February" - - Mar -> - "March" - - Apr -> - "April" - - May -> - "May" - - Jun -> - "June" - - Jul -> - "July" - - Aug -> - "August" - - Sep -> - "September" - - Oct -> - "October" - - Nov -> - "November" - - Dec -> - "December" - - -monthNumber : Time.Month -> Int -monthNumber month = - case month of - Jan -> - 1 - - Feb -> - 2 - - Mar -> - 3 - - Apr -> - 4 - - May -> - 5 - - Jun -> - 6 - - Jul -> - 7 - - Aug -> - 8 - - Sep -> - 9 - - Oct -> - 10 - - Nov -> - 11 - - Dec -> - 12 diff --git a/src/Library/Tracks.elm b/src/Library/Tracks.elm deleted file mode 100644 index 372fba7d5..000000000 --- a/src/Library/Tracks.elm +++ /dev/null @@ -1,469 +0,0 @@ -module Tracks exposing (..) - -import Base64 -import List.Extra as List -import Maybe.Extra as Maybe -import Playlists exposing (Playlist, PlaylistTrackWithoutMetadata) -import String.Ext as String -import Time -import Time.Ext as Time - - - --- 🌳 - - -type alias Track = - { id : String - , insertedAt : Time.Posix - , path : String - , sourceId : String - , tags : Tags - } - - - --- PIECES - - -type alias Tags = - { disc : Int - , nr : Int - - -- Main - , album : Maybe String - , artist : Maybe String - , title : String - - -- Extra - , genre : Maybe String - , picture : Maybe String - , year : Maybe Int - } - - - --- DERIVATIVES & SUPPLEMENTS - - -type alias Cover = - { group : String - , identifiedTrackCover : IdentifiedTrack - , key : String - , sameAlbum : Bool - , sameArtist : Bool - , trackIds : List String - , tracks : List IdentifiedTrack - , variousArtists : Bool - } - - -type alias Favourite = - { artist : Maybe String - , title : String - } - - -type alias IdentifiedTrack = - ( Identifiers, Track ) - - -type alias Identifiers = - { isFavourite : Bool - , isMissing : Bool - - -- - , filename : String - , group : Maybe { name : String, firstInGroup : Bool } - , indexInList : Int - , indexInPlaylist : Maybe Int - , parentDirectory : String - } - - - --- COLLECTIONS - - -type alias Collection = - { untouched : List Track - - -- `Track`s with `Identifiers` - , identified : List IdentifiedTrack - - -- Sorted, grouped and filtered by playlist (if not auto-generated) - , arranged : List IdentifiedTrack - - -- Filtered by search results, favourites, etc. - , harvested : List IdentifiedTrack - - -- Contexts - ----------- - , scrollContext : String - } - - -type alias CollectionDependencies = - { cached : List String - , cachedOnly : Bool - , enabledSourceIds : List String - , favourites : List Favourite - , favouritesOnly : Bool - , grouping : Maybe Grouping - , hideDuplicates : Bool - , selectedPlaylist : Maybe Playlist - , searchResults : Maybe (List String) - , sortBy : SortBy - , sortDirection : SortDirection - } - - -type alias Parcel = - ( CollectionDependencies, Collection ) - - -type alias CoverCollection = - { arranged : List Cover - , harvested : List Cover - } - - - --- GROUPING & SORTING - - -type Grouping - = AddedOn - | Directory - | FirstAlphaCharacter - | TrackYear - - -type SortBy - = Artist - | Album - | PlaylistIndex - | Title - - -type SortDirection - = Asc - | Desc - - - --- VIEW - - -type Scene - = Covers - | List - - - --- 🔱 - - -emptyTrack : Track -emptyTrack = - { id = "" - , insertedAt = Time.default - , path = "" - , sourceId = "" - , tags = emptyTags - } - - -emptyTags : Tags -emptyTags = - { disc = 1 - , nr = 0 - , album = Nothing - , artist = Nothing - , title = "Empty" - , genre = Nothing - , picture = Nothing - , year = Nothing - } - - -emptyIdentifiedTrack : IdentifiedTrack -emptyIdentifiedTrack = - ( emptyIdentifiers - , emptyTrack - ) - - -emptyIdentifiers : Identifiers -emptyIdentifiers = - { isFavourite = False - , isMissing = False - - -- - , filename = "" - , group = Nothing - , indexInList = 0 - , indexInPlaylist = Nothing - , parentDirectory = "" - } - - -emptyCollection : Collection -emptyCollection = - { untouched = [] - , identified = [] - , arranged = [] - , harvested = [] - - -- Contexts - ----------- - , scrollContext = "" - } - - -{-| If a track doesn't fit into a group, where does it go? --} -fallbackCoverGroup : String -fallbackCoverGroup = - "MISSING_TRACK_INFO" - - -{-| This value is used as a fallback in the UI if the album is missing. --} -fallbackAlbum : String -fallbackAlbum = - "" - - -{-| This value is used as a fallback in the UI if the artist is missing. --} -fallbackArtist : String -fallbackArtist = - "" - - - --- MORE STUFF - - -coverGroup : SortBy -> IdentifiedTrack -> String -coverGroup sort ( identifiers, { tags } as track ) = - if identifiers.isMissing then - "MISSING_TRACKS" - - else - case sort of - Artist -> - Maybe.unwrap fallbackCoverGroup (String.trim >> String.toLower) tags.artist - - Album -> - -- There is the possibility of albums with the same name, - -- such as "Greatests Hits". - -- To make sure we treat those as different albums, - -- we prefix the album by its parent directory. - case tags.album of - Just album -> - (identifiers.parentDirectory ++ album) - |> String.trim - |> String.toLower - - Nothing -> - fallbackCoverGroup - - PlaylistIndex -> - "" - - Title -> - tags.title - - -coverKey : Bool -> Track -> String -coverKey isVariousArtists { tags } = - if isVariousArtists then - Maybe.withDefault "?" tags.album - - else - Maybe.withDefault "?" tags.artist ++ " --- " ++ Maybe.withDefault "?" tags.album - - -isNowPlaying : IdentifiedTrack -> IdentifiedTrack -> Bool -isNowPlaying ( a, b ) ( x, y ) = - a.indexInPlaylist == x.indexInPlaylist && b.id == y.id - - -makeTrack : String -> ( String, Tags ) -> Track -makeTrack sourceId ( path, tags ) = - { id = - (sourceId ++ "//" ++ path) - |> Base64.encode - |> String.chopEnd "=" - , insertedAt = Time.default - , path = path - , sourceId = sourceId - , tags = tags - } - - -matchesAutoGeneratedPlaylist : Playlist -> Track -> Bool -matchesAutoGeneratedPlaylist playlist track = - case playlist.autoGenerated of - Just { level } -> - track.path - |> String.split "/" - |> List.drop (max 0 (level - 1)) - |> List.head - |> (==) (Just playlist.name) - - Nothing -> - False - - -missingId : String -missingId = - "" - - -pathParts : Track -> { filename : String, parentDirectory : String } -pathParts { path } = - let - s = - String.split "/" path - - l = - List.length s - in - case List.drop (max 0 <| l - 2) s of - [ p, f ] -> - { filename = f, parentDirectory = p } - - [ f ] -> - { filename = f, parentDirectory = "" } - - _ -> - { filename = "", parentDirectory = "" } - - -{-| Given a collection of tracks, pick out the tracks by id in order. -Note that track ids in the ids list may occur multiple times. --} -pick : List String -> List Track -> List Track -pick ids collection = - collection - |> List.foldr - (\track -> - List.map - (\picking -> - case picking of - PickId id -> - if id == track.id then - PickTrack track - - else - PickId id - - p -> - p - ) - ) - (List.map PickId ids) - |> List.foldr - (\picking acc -> - case picking of - PickId _ -> - acc - - PickTrack track -> - track :: acc - ) - [] - - -removeByPaths : { sourceId : String, paths : List String } -> List Track -> { kept : List Track, removed : List Track } -removeByPaths { sourceId, paths } tracks = - tracks - |> List.foldr - (\t ( kept, removed, remainingPathsToRemove ) -> - if t.sourceId == sourceId && List.member t.path remainingPathsToRemove then - ( kept, t :: removed, List.remove t.path remainingPathsToRemove ) - - else - ( t :: kept, removed, remainingPathsToRemove ) - ) - ( [], [], paths ) - |> (\( k, r, _ ) -> - { kept = k, removed = r } - ) - - -removeBySourceId : String -> List Track -> { kept : List Track, removed : List Track } -removeBySourceId removedSourceId tracks = - tracks - |> List.foldr - (\t ( kept, removed ) -> - if t.sourceId == removedSourceId then - ( kept, t :: removed ) - - else - ( t :: kept, removed ) - ) - ( [], [] ) - |> (\( k, r ) -> - { kept = k, removed = r } - ) - - -removeFromPlaylist : List IdentifiedTrack -> Playlist -> Playlist -removeFromPlaylist tracks playlist = - playlist.tracks - |> List.indexedFoldr - (\idx t ( acc, remaining ) -> - case List.partition ((==) (Just idx)) remaining of - ( _ :: _, rem ) -> - ( acc, rem ) - - ( _, rem ) -> - ( t :: acc, rem ) - ) - ( [] - , List.map (Tuple.first >> .indexInPlaylist) tracks - ) - |> (\( t, _ ) -> { playlist | tracks = t }) - - -shouldNoteProgress : { duration : Float } -> Bool -shouldNoteProgress { duration } = - duration >= 30 * 60 - - -shouldRenderGroup : Identifiers -> Bool -shouldRenderGroup identifiers = - identifiers.group - |> Maybe.map (.firstInGroup >> (==) True) - |> Maybe.withDefault False - - -playlistTrackFromTrack : Track -> PlaylistTrackWithoutMetadata -playlistTrackFromTrack track = - { album = track.tags.album - , artist = track.tags.artist - , title = track.tags.title - } - - -sortByIndexInPlaylist : List IdentifiedTrack -> List IdentifiedTrack -sortByIndexInPlaylist = - List.sortBy (\( i, t ) -> Maybe.withDefault (t.tags.disc * 1000 + t.tags.nr) i.indexInPlaylist) - - -toPlaylistTracks : List IdentifiedTrack -> List PlaylistTrackWithoutMetadata -toPlaylistTracks = - List.map (Tuple.second >> playlistTrackFromTrack) - - - --- ㊙️ - - -type Pick - = PickId String - | PickTrack Track diff --git a/src/Library/Tracks/Collection.elm b/src/Library/Tracks/Collection.elm deleted file mode 100644 index 33b91f17c..000000000 --- a/src/Library/Tracks/Collection.elm +++ /dev/null @@ -1,92 +0,0 @@ -module Tracks.Collection exposing (add, arrange, harvest, identifiedTracksChanged, identify, map, replace, tracksChanged) - -import Tracks exposing (IdentifiedTrack, Parcel, Track, emptyCollection) -import Tracks.Collection.Internal as Internal - - - --- 🔱 - - -identify : Parcel -> Parcel -identify = - Internal.identify >> Internal.arrange >> Internal.harvest - - -arrange : Parcel -> Parcel -arrange = - Internal.arrange >> Internal.harvest - - -harvest : Parcel -> Parcel -harvest = - Internal.harvest - - -map : (List IdentifiedTrack -> List IdentifiedTrack) -> Parcel -> Parcel -map fn ( model, collection ) = - ( model - , { collection - | identified = fn collection.identified - , arranged = fn collection.arranged - , harvested = fn collection.harvested - } - ) - - - --- ⚗️ - - -add : List Track -> Parcel -> Parcel -add tracks ( deps, { untouched } ) = - identify - ( deps - , { emptyCollection | untouched = untouched ++ tracks } - ) - - -replace : List Track -> Parcel -> Parcel -replace tracks ( deps, { untouched } ) = - identify - ( deps - , { emptyCollection | untouched = tracks } - ) - - - --- ⚗️ - - -tracksChanged : List Track -> List Track -> Bool -tracksChanged listA listB = - case ( listA, listB ) of - ( [], [] ) -> - False - - ( a :: restA, b :: restB ) -> - if a.id /= b.id then - True - - else - tracksChanged restA restB - - _ -> - True - - -identifiedTracksChanged : List IdentifiedTrack -> List IdentifiedTrack -> Bool -identifiedTracksChanged listA listB = - case ( listA, listB ) of - ( [], [] ) -> - False - - ( ( _, a ) :: restA, ( _, b ) :: restB ) -> - if a.id /= b.id then - True - - else - identifiedTracksChanged restA restB - - _ -> - True diff --git a/src/Library/Tracks/Collection/Internal.elm b/src/Library/Tracks/Collection/Internal.elm deleted file mode 100644 index 16d5b048c..000000000 --- a/src/Library/Tracks/Collection/Internal.elm +++ /dev/null @@ -1,29 +0,0 @@ -module Tracks.Collection.Internal exposing - ( arrange - , harvest - , identify - ) - -import Tracks exposing (Parcel) -import Tracks.Collection.Internal.Arrange as Internal -import Tracks.Collection.Internal.Harvest as Internal -import Tracks.Collection.Internal.Identify as Internal - - - --- 🔱 - - -identify : Parcel -> Parcel -identify = - Internal.identify - - -arrange : Parcel -> Parcel -arrange = - Internal.arrange - - -harvest : Parcel -> Parcel -harvest = - Internal.harvest diff --git a/src/Library/Tracks/Collection/Internal/Arrange.elm b/src/Library/Tracks/Collection/Internal/Arrange.elm deleted file mode 100644 index 71a7c7b04..000000000 --- a/src/Library/Tracks/Collection/Internal/Arrange.elm +++ /dev/null @@ -1,305 +0,0 @@ -module Tracks.Collection.Internal.Arrange exposing (arrange) - -import Conditional exposing (ifThenElse) -import Dict exposing (Dict) -import List.Extra as List -import Maybe.Extra as Maybe -import Playlists exposing (..) -import Playlists.Matching -import String.Ext as String -import Time -import Time.Ext as Time -import Tracks exposing (..) -import Tracks.Sorting as Sorting - - - --- 🍯 - - -arrange : Parcel -> Parcel -arrange ( deps, collection ) = - case deps.selectedPlaylist of - Just playlist -> - if playlist.collection then - case playlist.autoGenerated of - Just _ -> - arrangeByGroup ( deps, collection ) - - Nothing -> - arrangeByCollection ( deps, collection ) playlist - - else - arrangeByPlaylist ( deps, collection ) playlist - - Nothing -> - arrangeByGroup ( deps, collection ) - - - --- GROUPING - - -arrangeByGroup : Parcel -> Parcel -arrangeByGroup ( deps, collection ) = - case deps.grouping of - Just AddedOn -> - ( deps, groupByInsertedAt deps collection ) - - Just Directory -> - ( deps, groupByDirectory deps collection ) - - Just FirstAlphaCharacter -> - ( deps, groupByFirstAlphaCharacter deps collection ) - - Just TrackYear -> - ( deps, groupByYear deps collection ) - - Nothing -> - collection.identified - |> Sorting.sort deps.sortBy deps.sortDirection - |> (\x -> { collection | arranged = x }) - |> (\x -> ( deps, x )) - - -addToList : a -> Maybe (List a) -> Maybe (List a) -addToList item maybeList = - case maybeList of - Just list -> - Just (item :: list) - - Nothing -> - Just [ item ] - - -groupBy : { reversed : Bool } -> (IdentifiedTrack -> Dict a (List IdentifiedTrack) -> Dict a (List IdentifiedTrack)) -> CollectionDependencies -> Collection -> Collection -groupBy { reversed } folder deps collection = - collection.identified - |> List.foldl folder Dict.empty - |> Dict.values - |> ifThenElse reversed List.reverse identity - |> List.concatMap (Sorting.sort deps.sortBy deps.sortDirection) - |> (\arranged -> { collection | arranged = arranged }) - - - --- GROUPING ░░ ADDED ON - - -groupByInsertedAt : CollectionDependencies -> Collection -> Collection -groupByInsertedAt = - groupBy { reversed = True } groupByInsertedAtFolder - - -groupByInsertedAtFolder : IdentifiedTrack -> Dict Int (List IdentifiedTrack) -> Dict Int (List IdentifiedTrack) -groupByInsertedAtFolder ( i, t ) = - let - ( year, month ) = - ( Time.toYear Time.utc t.insertedAt - , Time.toMonth Time.utc t.insertedAt - ) - - group = - { name = insertedAtGroupName year month - , firstInGroup = False - } - - item = - ( { i | group = Just group } - , t - ) - in - Dict.update - (year * 1000 + Time.monthNumber month) - (addToList item) - - -insertedAtGroupName : Int -> Time.Month -> String -insertedAtGroupName year month = - if year == 1970 then - "MANY MOONS AGO" - - else - Time.monthName month ++ " " ++ String.fromInt year - - - --- GROUPING ░░ DIRECTORY - - -groupByDirectory : CollectionDependencies -> Collection -> Collection -groupByDirectory deps = - groupBy { reversed = False } (groupByDirectoryFolder deps) deps - - -groupByDirectoryFolder : CollectionDependencies -> IdentifiedTrack -> Dict String (List IdentifiedTrack) -> Dict String (List IdentifiedTrack) -groupByDirectoryFolder deps ( i, t ) = - let - directory = - t.path - |> String.chopStart "/" - |> String.split "/" - |> (case Maybe.andThen .autoGenerated deps.selectedPlaylist of - Just { level } -> - List.drop (max 0 (level - 1) + 1) - - Nothing -> - identity - ) - |> List.init - |> Maybe.map (String.join " / ") - |> Maybe.withDefault t.path - - group = - { name = directory - , firstInGroup = False - } - - item = - ( { i | group = Just group } - , t - ) - in - Dict.update - directory - (addToList item) - - - --- GROUPING ░░ FIRST LETTER - - -groupByFirstAlphaCharacter : CollectionDependencies -> Collection -> Collection -groupByFirstAlphaCharacter deps = - groupBy { reversed = False } (groupByFirstAlphaCharacterFolder deps) deps - - -groupByFirstAlphaCharacterFolder : CollectionDependencies -> IdentifiedTrack -> Dict String (List IdentifiedTrack) -> Dict String (List IdentifiedTrack) -groupByFirstAlphaCharacterFolder deps ( i, t ) = - let - tag = - case deps.sortBy of - Artist -> - Maybe.withDefault fallbackArtist t.tags.artist - - Album -> - Maybe.withDefault fallbackAlbum t.tags.album - - PlaylistIndex -> - "" - - Title -> - t.tags.title - - group = - { name = - tag - |> String.toList - |> List.head - |> Maybe.andThen - (\char -> - if Char.isAlpha char then - Just (String.fromList [ Char.toUpper char ]) - - else - Nothing - ) - |> Maybe.withDefault "#" - , firstInGroup = False - } - - item = - ( { i | group = Just group } - , t - ) - in - Dict.update - group.name - (addToList item) - - - --- GROUPING ░░ YEAR - - -groupByYear : CollectionDependencies -> Collection -> Collection -groupByYear = - groupBy { reversed = True } groupByYearFolder - - -groupByYearFolder : IdentifiedTrack -> Dict Int (List IdentifiedTrack) -> Dict Int (List IdentifiedTrack) -groupByYearFolder ( i, t ) = - let - group = - { name = Maybe.unwrap "0000 - Unknown" String.fromInt t.tags.year - , firstInGroup = False - } - - item = - ( { i | group = Just group } - , t - ) - in - Dict.update - (Maybe.withDefault 0 t.tags.year) - (addToList item) - - - --- PLAYLISTS - - -arrangeByCollection : Parcel -> Playlist -> Parcel -arrangeByCollection ( deps, collection ) playlist = - collection.identified - |> Playlists.Matching.match playlist - |> dealWithMissingPlaylistTracks - |> Sorting.sort deps.sortBy deps.sortDirection - |> (\x -> { collection | arranged = x }) - |> (\x -> ( deps, x )) - - -arrangeByPlaylist : Parcel -> Playlist -> Parcel -arrangeByPlaylist ( deps, collection ) playlist = - collection.identified - |> Playlists.Matching.match playlist - |> dealWithMissingPlaylistTracks - |> Sorting.sort PlaylistIndex Asc - |> (\x -> { collection | arranged = x }) - |> (\x -> ( deps, x )) - - -dealWithMissingPlaylistTracks : ( List IdentifiedTrack, List IdentifiedPlaylistTrack ) -> List IdentifiedTrack -dealWithMissingPlaylistTracks ( identifiedTracks, remainingPlaylistTracks ) = - identifiedTracks ++ List.map makeMissingPlaylistTrack remainingPlaylistTracks - - -makeMissingPlaylistTrack : IdentifiedPlaylistTrack -> IdentifiedTrack -makeMissingPlaylistTrack ( identifiers, playlistTrack ) = - let - tags = - { disc = 1 - , nr = 0 - , artist = playlistTrack.artist - , title = playlistTrack.title - , album = playlistTrack.album - , genre = Nothing - , picture = Nothing - , year = Nothing - } - in - Tuple.pair - { filename = "" - , group = Nothing - , indexInList = 0 - , indexInPlaylist = Just identifiers.index - , isFavourite = False - , isMissing = True - , parentDirectory = "" - } - { tags = tags - , id = missingId - , insertedAt = Time.default - , path = missingId - , sourceId = missingId - } diff --git a/src/Library/Tracks/Collection/Internal/Harvest.elm b/src/Library/Tracks/Collection/Internal/Harvest.elm deleted file mode 100644 index dbd45918a..000000000 --- a/src/Library/Tracks/Collection/Internal/Harvest.elm +++ /dev/null @@ -1,148 +0,0 @@ -module Tracks.Collection.Internal.Harvest exposing (harvest) - -import Dict -import List.Extra as List -import Maybe.Extra as Maybe -import Tracks exposing (..) - - - --- 🍯 - - -harvest : Parcel -> Parcel -harvest ( deps, collection ) = - let - harvested = - case deps.searchResults of - Just [] -> - [] - - Just trackIds -> - collection.arranged - |> List.foldl harvester ( [], trackIds ) - |> Tuple.first - - Nothing -> - collection.arranged - - filters = - [ -- Favourites / Missing - ----------------------- - if deps.favouritesOnly then - Tuple.first >> .isFavourite >> (==) True - - else if Maybe.map .autoGenerated deps.selectedPlaylist == Just Nothing then - always True - - else - Tuple.first >> .isMissing >> (==) False - - -- Playlist - ----------- - , case deps.selectedPlaylist of - Just playlist -> - case playlist.autoGenerated of - Just _ -> - \( _, t ) -> - Tracks.matchesAutoGeneratedPlaylist playlist t - - Nothing -> - \( i, _ ) -> - Maybe.isJust i.indexInPlaylist - - Nothing -> - always True - - -- Cached - --------- - , if deps.cachedOnly then - \( _, t ) -> List.member t.id deps.cached - - else - always True - ] - - theFilter x = - List.foldl - (\filter bool -> - if True == bool then - filter x - - else - bool - ) - True - filters - in - harvested - |> List.foldl - (\( i, t ) ( dict, ( idx, prevIdentifiers ), acc ) -> - let - -- Identifier used to distinguish duplicates - s = - case t.tags.artist of - Just artist -> - String.toLower (artist ++ t.tags.title) - - Nothing -> - String.toLower t.tags.title - in - if theFilter ( i, t ) == False then - ( dict, ( idx, prevIdentifiers ), acc ) - - else if deps.hideDuplicates && Dict.member s dict then - ( dict, ( idx, prevIdentifiers ), acc ) - - else - let - prevGroup = - Maybe.unwrap - "" - .name - prevIdentifiers.group - - newIdentifiers = - { i - | group = - Maybe.map - (\g -> { g | firstInGroup = prevGroup /= g.name }) - i.group - , indexInList = idx - } - in - ( if deps.hideDuplicates then - Dict.insert s () dict - - else - dict - -- - , ( idx + 1, newIdentifiers ) - , ( newIdentifiers, t ) :: acc - ) - ) - ( Dict.empty, ( 0, Tracks.emptyIdentifiers ), [] ) - |> (\( _, _, c ) -> c) - |> (\h -> { collection | harvested = List.reverse h }) - |> (\c -> ( deps, c )) - - - --- 🛠 - - -harvester : - IdentifiedTrack - -> ( List IdentifiedTrack, List String ) - -> ( List IdentifiedTrack, List String ) -harvester ( i, t ) ( acc, trackIds ) = - case List.findIndex ((==) t.id) trackIds of - Just idx -> - ( acc ++ [ ( i, t ) ] - , List.removeAt idx trackIds - ) - - Nothing -> - ( acc - , trackIds - ) diff --git a/src/Library/Tracks/Collection/Internal/Identify.elm b/src/Library/Tracks/Collection/Internal/Identify.elm deleted file mode 100644 index 42c5568c5..000000000 --- a/src/Library/Tracks/Collection/Internal/Identify.elm +++ /dev/null @@ -1,178 +0,0 @@ -module Tracks.Collection.Internal.Identify exposing (identify) - -import Dict -import List.Extra as List -import Time.Ext as Time -import Tracks exposing (..) - - - --- 🔱 - - -identify : Parcel -> Parcel -identify ( deps, collection ) = - let - ( favouritesDictionary, simplifiedFavourites ) = - List.foldr - (\fav ( dict, acc ) -> - let - simpl = - case fav.artist of - Just artist -> - String.toLower artist ++ String.toLower fav.title - - Nothing -> - String.toLower fav.title - in - ( Dict.insert simpl fav dict - , simpl :: acc - ) - ) - ( Dict.empty, [] ) - deps.favourites - - ( identifiedUnsorted, missingFavouritesSimplified ) = - List.foldl - (identifyTrack - deps.enabledSourceIds - simplifiedFavourites - ) - ( [], simplifiedFavourites ) - collection.untouched - - missingFavourites = - List.foldr - (\simpl acc -> - case Dict.get simpl favouritesDictionary of - Just fav -> - fav :: acc - - Nothing -> - acc - ) - [] - missingFavouritesSimplified - in - identifiedUnsorted - |> List.append (List.map makeMissingFavouriteTrack missingFavourites) - |> (\x -> { collection | identified = x }) - |> (\x -> ( deps, x )) - - - --- IDENTIFY - - -identifyTrack : - List String - -> List String - -> Track - -> ( List IdentifiedTrack, List String ) - -> ( List IdentifiedTrack, List String ) -identifyTrack enabledSourceIds favourites track = - if List.member track.sourceId enabledSourceIds then - partTwo favourites track - - else - identity - - -partTwo : - List String - -> Track - -> ( List IdentifiedTrack, List String ) - -> ( List IdentifiedTrack, List String ) -partTwo favourites track ( acc, remainingFavourites ) = - let - isFavourite_ = - isFavourite track - - isFav = - List.any isFavourite_ favourites - - { filename, parentDirectory } = - pathParts track - - identifiedTrack = - ( { isFavourite = isFav - , isMissing = False - - -- - , filename = filename - , group = Nothing - , indexInList = 0 - , indexInPlaylist = Nothing - , parentDirectory = parentDirectory - } - , track - ) - in - if isFav then - -- - -- A favourite - -- - ( identifiedTrack :: acc - , remainingFavourites - |> List.findIndex isFavourite_ - |> Maybe.map (\idx -> List.removeAt idx remainingFavourites) - |> Maybe.withDefault remainingFavourites - ) - - else - -- - -- Not a favourite - -- - ( identifiedTrack :: acc - , remainingFavourites - ) - - - --- FAVOURITES - - -isFavourite : Track -> String -> Bool -isFavourite track = - -- This needs to match the `simplifiedFavourites` format from above - (==) - (case track.tags.artist of - Just artist -> - String.toLower artist ++ String.toLower track.tags.title - - Nothing -> - String.toLower track.tags.title - ) - - -makeMissingFavouriteTrack : Favourite -> IdentifiedTrack -makeMissingFavouriteTrack fav = - let - tags = - { disc = 1 - , nr = 0 - , artist = fav.artist - , title = fav.title - , album = Nothing - , genre = Nothing - , picture = Nothing - , year = Nothing - } - in - ( { isFavourite = True - , isMissing = True - - -- - , filename = "" - , group = Nothing - , indexInList = 0 - , indexInPlaylist = Nothing - , parentDirectory = "" - } - , { tags = tags - , id = missingId - , insertedAt = Time.default - , path = missingId - , sourceId = missingId - } - ) diff --git a/src/Library/Tracks/Encoding.elm b/src/Library/Tracks/Encoding.elm deleted file mode 100644 index 00fd32cc7..000000000 --- a/src/Library/Tracks/Encoding.elm +++ /dev/null @@ -1,227 +0,0 @@ -module Tracks.Encoding exposing (..) - -import Json.Decode as Decode -import Json.Decode.Pipeline exposing (optional, required) -import Json.Encode as Encode -import Json.Encode.Ext exposing (..) -import Time.Ext as Time -import Tracks exposing (..) - - - --- ENCODE - - -encodeFavourite : Favourite -> Encode.Value -encodeFavourite fav = - Encode.object - [ ( "artist", encodeMaybe fav.artist Encode.string ) - , ( "title", Encode.string fav.title ) - ] - - -encodeGrouping : Grouping -> Encode.Value -encodeGrouping v = - case v of - AddedOn -> - Encode.string "ADDED_ON" - - Directory -> - Encode.string "DIRECTORY" - - FirstAlphaCharacter -> - Encode.string "FIRST_ALPHA_CHARACTER" - - TrackYear -> - Encode.string "TRACK_YEAR" - - -encodeScene : Scene -> Encode.Value -encodeScene scene = - case scene of - Covers -> - Encode.string "COVERS" - - List -> - Encode.string "LIST" - - -encodeSortBy : SortBy -> Encode.Value -encodeSortBy v = - case v of - Artist -> - Encode.string "ARTIST" - - Album -> - Encode.string "ALBUM" - - PlaylistIndex -> - Encode.string "PLAYLIST_INDEX" - - Title -> - Encode.string "TITLE" - - -encodeSortDirection : SortDirection -> Encode.Value -encodeSortDirection v = - case v of - Asc -> - Encode.string "ASC" - - Desc -> - Encode.string "DESC" - - -encodeTrack : Track -> Encode.Value -encodeTrack track = - Encode.object - [ ( "id", Encode.string track.id ) - , ( "insertedAt", Time.encode track.insertedAt ) - , ( "path", Encode.string track.path ) - , ( "sourceId", Encode.string track.sourceId ) - , ( "tags", encodeTags track.tags ) - ] - - -encodeTags : Tags -> Encode.Value -encodeTags tags = - Encode.object - [ ( "disc", Encode.int tags.disc ) - , ( "nr", Encode.int tags.nr ) - - -- - , ( "album", encodeMaybe tags.album Encode.string ) - , ( "artist", encodeMaybe tags.artist Encode.string ) - , ( "title", Encode.string tags.title ) - - -- - , ( "genre", encodeMaybe tags.genre Encode.string ) - , ( "picture", encodeMaybe tags.picture Encode.string ) - , ( "year", encodeMaybe tags.year Encode.int ) - ] - - - --- DECODE - - -decodeFavourite : Decode.Value -> Maybe Favourite -decodeFavourite = - Decode.decodeValue favouriteDecoder - >> Result.toMaybe - - -decodeTrack : Decode.Value -> Maybe Track -decodeTrack = - Decode.decodeValue trackDecoder - >> Result.toMaybe - - -favouriteDecoder : Decode.Decoder Favourite -favouriteDecoder = - Decode.map2 Favourite - (Decode.maybe <| Decode.field "artist" Decode.string) - (Decode.field "title" Decode.string) - - -groupingDecoder : Decode.Decoder Grouping -groupingDecoder = - Decode.andThen - (\string -> - case string of - "ADDED_ON" -> - Decode.succeed AddedOn - - "DIRECTORY" -> - Decode.succeed Directory - - "FIRST_ALPHA_CHARACTER" -> - Decode.succeed FirstAlphaCharacter - - "TRACK_YEAR" -> - Decode.succeed TrackYear - - _ -> - Decode.fail "Invalid Grouping" - ) - Decode.string - - -sceneDecoder : Decode.Decoder Scene -sceneDecoder = - Decode.andThen - (\string -> - case string of - "COVERS" -> - Decode.succeed Covers - - "LIST" -> - Decode.succeed List - - _ -> - Decode.fail "Invalid Scene" - ) - Decode.string - - -sortByDecoder : Decode.Decoder SortBy -sortByDecoder = - Decode.andThen - (\string -> - case string of - "ARTIST" -> - Decode.succeed Artist - - "ALBUM" -> - Decode.succeed Album - - "PLAYLIST_INDEX" -> - Decode.succeed PlaylistIndex - - "TITLE" -> - Decode.succeed Title - - _ -> - Decode.fail "Invalid SortBy" - ) - Decode.string - - -sortDirectionDecoder : Decode.Decoder SortDirection -sortDirectionDecoder = - Decode.andThen - (\string -> - case string of - "ASC" -> - Decode.succeed Asc - - "DESC" -> - Decode.succeed Desc - - _ -> - Decode.fail "Invalid SortDirection" - ) - Decode.string - - -tagsDecoder : Decode.Decoder Tags -tagsDecoder = - Decode.map8 Tags - (Decode.field "disc" Decode.int) - (Decode.field "nr" Decode.int) - (Decode.maybe <| Decode.field "album" Decode.string) - (Decode.maybe <| Decode.field "artist" Decode.string) - (Decode.field "title" Decode.string) - (Decode.maybe <| Decode.field "genre" Decode.string) - (Decode.maybe <| Decode.field "picture" Decode.string) - (Decode.maybe <| Decode.field "year" Decode.int) - - -trackDecoder : Decode.Decoder Track -trackDecoder = - Decode.succeed Track - |> required "id" Decode.string - |> optional "insertedAt" Time.decoder Time.default - |> required "path" Decode.string - |> required "sourceId" Decode.string - |> required "tags" tagsDecoder diff --git a/src/Library/Tracks/Favourites.elm b/src/Library/Tracks/Favourites.elm deleted file mode 100644 index c762b7ddf..000000000 --- a/src/Library/Tracks/Favourites.elm +++ /dev/null @@ -1,183 +0,0 @@ -module Tracks.Favourites exposing (completeFavouritesList, completeTracksList, match, removeFromFavouritesList, removeFromTracksList, toggleInFavouritesList, toggleInTracksList) - -import List.Extra as List -import Maybe.Extra as Maybe -import Tracks exposing (Favourite, IdentifiedTrack, Track, fallbackArtist) - - - --- 🔱 - - -completeFavouritesList : List IdentifiedTrack -> List Favourite -> List Favourite -completeFavouritesList tracks favourites = - List.append - favourites - (List.filterMap - (\( i, t ) -> - if not i.isFavourite then - Just - { artist = t.tags.artist - , title = t.tags.title - } - - else - Nothing - ) - tracks - ) - - -completeTracksList : List IdentifiedTrack -> List IdentifiedTrack -> List IdentifiedTrack -completeTracksList tracksToMakeFavourite tracks = - let - favs = - List.filterMap - (\( i, t ) -> - if not i.isFavourite then - Just ( lowercaseArtist t, lowercaseTitle t ) - - else - Nothing - ) - tracksToMakeFavourite - in - List.map - (\( i, t ) -> - let - ( la, lt ) = - ( lowercaseArtist t, lowercaseTitle t ) - in - List.foldr - (\( lartist, ltitle ) ( ai, at ) -> - if la == lartist && lt == ltitle && not ai.isFavourite then - ( { ai | isFavourite = True }, at ) - - else - ( ai, at ) - ) - ( i, t ) - favs - ) - tracks - - -match : Favourite -> Favourite -> Bool -match a b = - let - ( aa, at ) = - ( Maybe.unwrap "" String.toLower a.artist - , String.toLower a.title - ) - - ( ba, bt ) = - ( Maybe.unwrap "" String.toLower b.artist - , String.toLower b.title - ) - in - aa == ba && at == bt - - -removeFromFavouritesList : List IdentifiedTrack -> List Favourite -> List Favourite -removeFromFavouritesList tracks favourites = - List.foldr - (\( i, t ) acc -> - if i.isFavourite then - List.filterNot - (match { artist = t.tags.artist, title = t.tags.title }) - acc - - else - acc - ) - favourites - tracks - - -removeFromTracksList : List IdentifiedTrack -> List IdentifiedTrack -> List IdentifiedTrack -removeFromTracksList tracksToRemoveFromFavs tracks = - let - unfavs = - List.filterMap - (\( i, t ) -> - if i.isFavourite then - Just ( lowercaseArtist t, lowercaseTitle t ) - - else - Nothing - ) - tracksToRemoveFromFavs - in - List.map - (\( i, t ) -> - let - ( la, lt ) = - ( lowercaseArtist t, lowercaseTitle t ) - in - List.foldr - (\( lartist, ltitle ) ( ai, at ) -> - if la == lartist && lt == ltitle && ai.isFavourite then - ( { ai | isFavourite = False }, at ) - - else - ( ai, at ) - ) - ( i, t ) - unfavs - ) - tracks - - -toggleInTracksList : Track -> List IdentifiedTrack -> List IdentifiedTrack -toggleInTracksList track = - let - lartist = - lowercaseArtist track - - ltitle = - lowercaseTitle track - in - List.map - (\( i, t ) -> - if lowercaseArtist t == lartist && lowercaseTitle t == ltitle then - ( { i | isFavourite = not i.isFavourite }, t ) - - else - ( i, t ) - ) - - -toggleInFavouritesList : IdentifiedTrack -> List Favourite -> List Favourite -toggleInFavouritesList ( i, t ) favourites = - let - favourite = - { artist = t.tags.artist - , title = t.tags.title - } - in - if i.isFavourite then - -- Remove from list - List.filterNot - (match favourite) - favourites - - else - -- Add to list - List.append - favourites - [ favourite ] - - - --- ⚗️ - - -lowercaseArtist : Track -> String -lowercaseArtist = - -- NOTE: Not entirely sure this fallback is correct - .tags >> .artist >> Maybe.unwrap fallbackArtist String.toLower - - -lowercaseTitle : Track -> String -lowercaseTitle = - .tags >> .title >> String.toLower diff --git a/src/Library/Tracks/Sorting.elm b/src/Library/Tracks/Sorting.elm deleted file mode 100644 index dd3c80e41..000000000 --- a/src/Library/Tracks/Sorting.elm +++ /dev/null @@ -1,158 +0,0 @@ -module Tracks.Sorting exposing (sort) - -import Maybe.Extra as Maybe -import Tracks exposing (..) - - - --- 🔱 - - -sort : SortBy -> SortDirection -> List IdentifiedTrack -> List IdentifiedTrack -sort property direction list = - let - sortFn = - case property of - Album -> - sortByAlbum - - Artist -> - sortByArtist - - PlaylistIndex -> - sortByPlaylistIndex - - Title -> - sortByTitle - - dirFn = - if direction == Desc then - List.reverse - - else - identity - in - list - |> List.sortWith sortFn - |> dirFn - - - --- BY - - -sortByAlbum : IdentifiedTrack -> IdentifiedTrack -> Order -sortByAlbum ( x, a ) ( y, b ) = - EQ - |> andThenCompareBools isMissing x y - |> andThenCompare album a b - |> andThenCompare parentDir x y - |> andThenCompare disc a b - |> andThenCompare nr a b - |> andThenCompare artist a b - |> andThenCompare title a b - - -sortByArtist : IdentifiedTrack -> IdentifiedTrack -> Order -sortByArtist ( x, a ) ( y, b ) = - EQ - |> andThenCompareBools isMissing x y - |> andThenCompare artist a b - |> andThenCompare album a b - |> andThenCompare parentDir x y - |> andThenCompare disc a b - |> andThenCompare nr a b - |> andThenCompare title a b - - -sortByTitle : IdentifiedTrack -> IdentifiedTrack -> Order -sortByTitle ( _, a ) ( _, b ) = - EQ - |> andThenCompare title a b - |> andThenCompare artist a b - |> andThenCompare album a b - - -sortByPlaylistIndex : IdentifiedTrack -> IdentifiedTrack -> Order -sortByPlaylistIndex ( a, _ ) ( b, _ ) = - andThenCompare (.indexInPlaylist >> Maybe.withDefault 0) a b EQ - - - --- TAGS - - -album : Track -> String -album = - .tags >> .album >> Maybe.unwrap fallbackAlbum low - - -artist : Track -> String -artist = - .tags >> .artist >> Maybe.unwrap fallbackArtist low - - -title : Track -> String -title = - .tags >> .title >> low - - -disc : Track -> Int -disc = - .tags >> .disc - - -nr : Track -> Int -nr = - .tags >> .nr - - -isMissing : Identifiers -> Bool -isMissing = - .isMissing - - -parentDir : Identifiers -> String -parentDir = - .parentDirectory >> low - - - --- COMMON - - -andThenCompare : (ctx -> comparable) -> ctx -> ctx -> Order -> Order -andThenCompare fn a b order = - if order == EQ then - compare (fn a) (fn b) - - else - order - - -andThenCompareBools : (ctx -> Bool) -> ctx -> ctx -> Order -> Order -andThenCompareBools fn a b order = - if order == EQ then - let - af = - fn a - - bf = - fn b - in - if af == bf then - EQ - - else if af == False then - GT - - else - LT - - else - order - - -low : String -> String -low = - String.trim >> String.toLower diff --git a/src/Library/Tuple/Ext.elm b/src/Library/Tuple/Ext.elm deleted file mode 100644 index f52a67774..000000000 --- a/src/Library/Tuple/Ext.elm +++ /dev/null @@ -1,8 +0,0 @@ -module Tuple.Ext exposing (uncurry) - --- 🔱 - - -uncurry : (a -> b -> c) -> ( a, b ) -> c -uncurry fn ( a, b ) = - fn a b diff --git a/src/Library/Url/Ext.elm b/src/Library/Url/Ext.elm deleted file mode 100644 index 48501651e..000000000 --- a/src/Library/Url/Ext.elm +++ /dev/null @@ -1,46 +0,0 @@ -module Url.Ext exposing (action, extractQueryParam, queryDictionary) - -import Dict exposing (Dict) -import Maybe.Extra as Maybe -import Url exposing (Url) -import Url.Parser as Url -import Url.Parser.Query as Query - - - --- 🔱 - - -action : Url -> List String -action url = - url - |> extractQueryParam "action" - |> Maybe.map (String.split "/") - |> Maybe.withDefault [] - - -extractQueryParam : String -> Url -> Maybe String -extractQueryParam key url = - { url | path = "" } - |> Url.parse (Url.query (Query.string key)) - |> Maybe.join - - -queryDictionary : Url -> Dict String String -queryDictionary url = - url.query - |> Maybe.map (String.split "&") - |> Maybe.withDefault [] - |> List.filterMap - (\s -> - case String.split "=" s of - [ k, v ] -> - Just ( k, v ) - - k :: v -> - Just ( k, String.join "=" v ) - - _ -> - Nothing - ) - |> Dict.fromList diff --git a/src/Library/User/Layer.elm b/src/Library/User/Layer.elm deleted file mode 100644 index f3aee3b8f..000000000 --- a/src/Library/User/Layer.elm +++ /dev/null @@ -1,527 +0,0 @@ -module User.Layer exposing (..) - -{-| User Layer. - -This concerns data that relates to the app, -controlled by the user and stored by the user. - -**Enclosed** data is data like, the enable-shuffle setting, -equalizer settings, or the currently-active-search term. -Which is stored in the browser. - -**Hypaethral** data is data like, the user's favourites, -processed tracks, or the user's sources. -Which is stored in the location chosen by the user. - --} - -import Dict exposing (Dict) -import Enum exposing (Enum) -import Equalizer -import Json.Decode as Json -import Json.Decode.Ext as Json -import Json.Decode.Pipeline exposing (optional) -import Json.Encode -import List.Extra as List -import Maybe.Extra as Maybe -import Playlists -import Playlists.Encoding as Playlists -import Settings -import Sources -import Sources.Encoding as Sources -import Task exposing (Task) -import Theme -import Time -import Time.Ext as Time -import Tracks -import Tracks.Encoding as Tracks - - - --- 🌳 - - -type Method - = Dropbox { accessToken : String, expiresAt : Int, refreshToken : String } - | Ipfs { apiOrigin : String } - | RemoteStorage { userAddress : String, token : String } - - -dropboxMethod : Method -dropboxMethod = - Dropbox { accessToken = "", expiresAt = 0, refreshToken = "" } - - -ipfsMethod : Method -ipfsMethod = - Ipfs { apiOrigin = "https://ipfs.io" } - - -remoteStorageMethod : Method -remoteStorageMethod = - RemoteStorage { userAddress = "", token = "" } - - - --- 🌳 ░░ ENCLOSED - - -type alias EnclosedData = - { cachedTracks : List String - , equalizerSettings : Equalizer.Settings - , grouping : Maybe Tracks.Grouping - , onlyShowCachedTracks : Bool - , onlyShowFavourites : Bool - , repeat : Bool - , scene : Tracks.Scene - , searchTerm : Maybe String - , selectedPlaylist : Maybe String - , shuffle : Bool - , sortBy : Tracks.SortBy - , sortDirection : Tracks.SortDirection - , theme : Maybe Theme.Id - } - - - --- 🌳 ░░ HYPAETHRAL - - -type HypaethralBit - = Favourites - | Playlists - | Progress - | Settings - | Sources - | Tracks - -- - | ModifiedAt - - -type HypaethralBaggage - = BaggageClaimed - -- - | PlaylistsBaggage PlaylistsBaggageAttributes - - -type alias HypaethralData = - { favourites : List Tracks.Favourite - , playlists : List Playlists.Playlist - , progress : Dict String Float - , settings : Maybe Settings.Settings - , sources : List Sources.Source - , tracks : List Tracks.Track - - -- - , modifiedAt : Maybe Time.Posix - } - - -type alias PlaylistsBaggageAttributes = - { publicPlaylistsRead : List Json.Value - , publicPlaylistsTodo : List String - , privatePlaylistsRead : List Json.Value - , privatePlaylistsTodo : List String - } - - - --- 🔱 ░░ METHOD - - -decodeMethod : Json.Value -> Maybe Method -decodeMethod = - Json.decodeValue (Json.map methodFromString Json.string) >> Result.toMaybe >> Maybe.join - - -encodeMethod : Method -> Json.Value -encodeMethod = - methodToString >> Json.Encode.string - - -methodName : Method -> String -methodName method = - case method of - Dropbox _ -> - "Dropbox" - - Ipfs _ -> - "IPFS (using MFS)" - - RemoteStorage _ -> - "Remote Storage" - - -methodFromString : String -> Maybe Method -methodFromString string = - case String.split methodSeparator string of - [ "DROPBOX", a, e, r ] -> - Just - (Dropbox - { accessToken = a - , expiresAt = Maybe.withDefault 0 (String.toInt e) - , refreshToken = r - } - ) - - [ "IPFS", a ] -> - Just (Ipfs { apiOrigin = a }) - - [ "REMOTE_STORAGE", u, t ] -> - Just (RemoteStorage { userAddress = u, token = t }) - - _ -> - Nothing - - -methodToString : Method -> String -methodToString method = - case method of - Dropbox { accessToken, expiresAt, refreshToken } -> - String.join - methodSeparator - [ "DROPBOX" - , accessToken - , String.fromInt expiresAt - , refreshToken - ] - - Ipfs { apiOrigin } -> - String.join - methodSeparator - [ "IPFS" - , apiOrigin - ] - - RemoteStorage { userAddress, token } -> - String.join - methodSeparator - [ "REMOTE_STORAGE" - , userAddress - , token - ] - - -methodSeparator : String -methodSeparator = - "___" - - -methodSupportsPublicData : Method -> Bool -methodSupportsPublicData method = - case method of - Dropbox _ -> - False - - Ipfs _ -> - False - - RemoteStorage _ -> - False - - - --- 🔱 ░░ ENCLOSED - - -decodeEnclosedData : Json.Value -> Result Json.Error EnclosedData -decodeEnclosedData = - Json.decodeValue enclosedDataDecoder - - -enclosedDataDecoder : Json.Decoder EnclosedData -enclosedDataDecoder = - Json.succeed EnclosedData - |> optional "cachedTracks" (Json.list Json.string) [] - |> optional "equalizerSettings" Equalizer.settingsDecoder Equalizer.defaultSettings - |> optional "grouping" (Json.maybe Tracks.groupingDecoder) Nothing - |> optional "onlyShowCachedTracks" Json.bool False - |> optional "onlyShowFavourites" Json.bool False - |> optional "repeat" Json.bool False - |> optional "scene" Tracks.sceneDecoder Tracks.Covers - |> optional "searchTerm" (Json.maybe Json.string) Nothing - |> optional "selectedPlaylist" (Json.maybe Json.string) Nothing - |> optional "shuffle" Json.bool False - |> optional "sortBy" Tracks.sortByDecoder Tracks.Album - |> optional "sortDirection" Tracks.sortDirectionDecoder Tracks.Asc - |> optional "theme" (Json.maybe Theme.idDecoder) Nothing - - -encodeEnclosedData : EnclosedData -> Json.Value -encodeEnclosedData { cachedTracks, equalizerSettings, grouping, onlyShowCachedTracks, onlyShowFavourites, repeat, scene, searchTerm, selectedPlaylist, shuffle, sortBy, sortDirection, theme } = - Json.Encode.object - [ ( "cachedTracks", Json.Encode.list Json.Encode.string cachedTracks ) - , ( "equalizerSettings", Equalizer.encodeSettings equalizerSettings ) - , ( "grouping", Maybe.unwrap Json.Encode.null Tracks.encodeGrouping grouping ) - , ( "onlyShowCachedTracks", Json.Encode.bool onlyShowCachedTracks ) - , ( "onlyShowFavourites", Json.Encode.bool onlyShowFavourites ) - , ( "repeat", Json.Encode.bool repeat ) - , ( "scene", Tracks.encodeScene scene ) - , ( "searchTerm", Maybe.unwrap Json.Encode.null Json.Encode.string searchTerm ) - , ( "selectedPlaylist", Maybe.unwrap Json.Encode.null Json.Encode.string selectedPlaylist ) - , ( "shuffle", Json.Encode.bool shuffle ) - , ( "sortBy", Tracks.encodeSortBy sortBy ) - , ( "sortDirection", Tracks.encodeSortDirection sortDirection ) - , ( "theme", Maybe.unwrap Json.Encode.null Theme.encodeId theme ) - ] - - - --- 🔱 ░░ HYPAETHRAL - - -allHypaethralBits : List HypaethralBit -allHypaethralBits = - [ Favourites - , Playlists - , Progress - , Settings - , Sources - , Tracks - ] - - -decodeHypaethralData : Json.Value -> Result Json.Error HypaethralData -decodeHypaethralData = - Json.decodeValue hypaethralDataDecoder - - -emptyHypaethralData : HypaethralData -emptyHypaethralData = - { favourites = [] - , playlists = [] - , progress = Dict.empty - , settings = Nothing - , sources = [] - , tracks = [] - - -- - , modifiedAt = Nothing - } - - -encodeHypaethralBit : HypaethralBit -> HypaethralData -> Json.Value -encodeHypaethralBit bit { favourites, playlists, progress, settings, sources, tracks, modifiedAt } = - case bit of - ModifiedAt -> - Maybe.unwrap Json.Encode.null Time.encode modifiedAt - - _ -> - Json.Encode.object - [ ( "data" - , case bit of - Favourites -> - Json.Encode.list Tracks.encodeFavourite favourites - - ModifiedAt -> - Maybe.unwrap Json.Encode.null Time.encode modifiedAt - - Playlists -> - Json.Encode.list Playlists.encode playlists - - Progress -> - Json.Encode.dict identity Json.Encode.float progress - - Settings -> - Maybe.unwrap Json.Encode.null Settings.encode settings - - Sources -> - Json.Encode.list Sources.encode sources - - Tracks -> - Json.Encode.list Tracks.encodeTrack tracks - ) - , ( "modifiedAt" - , Maybe.unwrap Json.Encode.null Time.encode modifiedAt - ) - ] - - -encodeHypaethralData : HypaethralData -> Json.Value -encodeHypaethralData data = - data - |> encodedHypaethralDataList - |> List.map (Tuple.mapFirst hypaethralBitKey) - |> Json.Encode.object - - -encodedHypaethralDataList : HypaethralData -> List ( HypaethralBit, Json.Value ) -encodedHypaethralDataList data = - List.map - (\bit -> ( bit, encodeHypaethralBit bit data )) - allHypaethralBits - - -hypaethralBit : Enum HypaethralBit -hypaethralBit = - allHypaethralBits - |> List.map (\bit -> ( hypaethralBitKey bit, bit )) - |> Enum.create - - -hypaethralBitFileName : HypaethralBit -> String -hypaethralBitFileName bit = - hypaethralBitKey bit ++ ".json" - - -hypaethralBitKey : HypaethralBit -> String -hypaethralBitKey bit = - case bit of - Favourites -> - "favourites" - - ModifiedAt -> - "modified" - - Playlists -> - "playlists" - - Progress -> - "progress" - - Settings -> - "settings" - - Sources -> - "sources" - - Tracks -> - "tracks" - - -hypaethralDataDecoder : Json.Decoder HypaethralData -hypaethralDataDecoder = - let - optionalWithPossiblyData key dec def a = - optional - (hypaethralBitKey key) - (Json.oneOf [ modifiedAtDecoder dec, noModifiedAt dec ]) - { data = def, modifiedAt = Nothing } - a - in - (\fav pla pro set sor tra mod -> - { favourites = fav.data - , playlists = pla.data - , progress = pro.data - , settings = set.data - , sources = sor.data - , tracks = tra.data - - -- - , modifiedAt = - case mod of - Just m -> - Just (Time.millisToPosix m) - - Nothing -> - [ fav.modifiedAt - , pla.modifiedAt - , pro.modifiedAt - , set.modifiedAt - , sor.modifiedAt - , tra.modifiedAt - ] - |> List.filterMap (Maybe.map Time.posixToMillis) - |> List.sort - |> List.last - |> Maybe.map Time.millisToPosix - } - ) - |> Json.succeed - |> optionalWithPossiblyData Favourites (Json.listIgnore Tracks.favouriteDecoder) [] - |> optionalWithPossiblyData Playlists (Json.listIgnore Playlists.decoder) [] - |> optionalWithPossiblyData Progress (Json.dict Json.float) Dict.empty - |> optionalWithPossiblyData Settings (Json.maybe Settings.decoder) Nothing - |> optionalWithPossiblyData Sources (Json.listIgnore Sources.decoder) [] - |> optionalWithPossiblyData Tracks (Json.listIgnore Tracks.trackDecoder) [] - |> optional (hypaethralBitKey ModifiedAt) (Json.maybe Json.int) Nothing - - - --- merge : HypaethralData -> HypaethralData -> HypaethralData --- merge a b = --- { favourites = List.unique (a.favourites ++ b.favourites) --- , playlists = List.unique (a.playlists ++ b.playlists) --- , progress = List.unique (a.progress ++ b.progress) --- , settings = List.unique (a.settings ++ b.settings) --- , sources = List.unique (a.sources ++ b.sources) --- , tracks = List.unique (a.tracks ++ b.tracks) --- -- --- , modifiedAt = --- case ( a.modifiedAt, b.modifiedAt ) of --- ( Just am, Just bm ) -> --- if Time.posixToMillis am > Time.posixToMillis bm then --- Just am --- else --- Just bm --- ( Just am, Nothing ) -> --- Just am --- ( Nothing, Just bm ) -> --- Just bm --- ( Nothing, Nothing ) -> --- Nothing --- } - - -modifiedAtDecoder : Json.Decoder a -> Json.Decoder { data : a, modifiedAt : Maybe Time.Posix } -modifiedAtDecoder decoder = - Json.map2 - (\d m -> { data = d, modifiedAt = m }) - (Json.field "data" decoder) - (Json.maybe <| Json.field "modifiedAt" Time.decoder) - - -noModifiedAt : Json.Decoder a -> Json.Decoder { data : a, modifiedAt : Maybe Time.Posix } -noModifiedAt = - Json.map - (\data -> - { data = data - , modifiedAt = Nothing - } - ) - - -putHypaethralJsonBitsTogether : List ( HypaethralBit, Json.Value, HypaethralBaggage ) -> Json.Value -putHypaethralJsonBitsTogether bits = - bits - |> List.map (\( a, b, _ ) -> ( hypaethralBitKey a, b )) - |> Json.Encode.object - - -retrieveHypaethralData : (HypaethralBit -> Task x (Maybe Json.Value)) -> Task x (List ( HypaethralBit, Maybe Json.Encode.Value )) -retrieveHypaethralData retrievalFn = - hypaethralBit.list - |> List.map - (\( _, bit ) -> - bit - |> retrievalFn - |> Task.map (\value -> ( bit, value )) - ) - |> Task.sequence - - -saveHypaethralData : (HypaethralBit -> Json.Value -> Task x ()) -> HypaethralData -> Task x () -saveHypaethralData saveFn data = - hypaethralBit.list - |> List.map - (\( _, bit ) -> - data - |> encodeHypaethralBit bit - |> saveFn bit - ) - |> Task.sequence - |> Task.map (always ()) - - - --- 🔱 ░░ BAGGAGE - - -mapPlaylistsBaggage : (PlaylistsBaggageAttributes -> PlaylistsBaggageAttributes) -> HypaethralBaggage -> HypaethralBaggage -mapPlaylistsBaggage fn baggage = - case baggage of - PlaylistsBaggage p -> - PlaylistsBaggage (fn p) - - b -> - b diff --git a/src/Library/User/Layer/Methods/Dropbox.elm b/src/Library/User/Layer/Methods/Dropbox.elm deleted file mode 100644 index 272421695..000000000 --- a/src/Library/User/Layer/Methods/Dropbox.elm +++ /dev/null @@ -1,114 +0,0 @@ -module User.Layer.Methods.Dropbox exposing (..) - -import Common -import Http -import Http.Ext as Http -import Http.Extras as Http -import Json.Decode as Json -import Task exposing (Task) -import Url exposing (Url) - - - --- 🌳 - - -type alias Tokens = - { accessToken : String - , expiresIn : Int -- Time in seconds the access token expires in - , refreshToken : Maybe String - } - - - --- 🏔 - - -clientId : String -clientId = - "te0c9pbeii8f8bw" - - -clientSecret : String -clientSecret = - "kxmlfdsw8k9e0ot" - - -redirectUri : Url -> String -redirectUri url = - Common.urlOrigin url ++ "?action=authenticate/dropbox" - - - --- ENCODING - - -tokensDecoder : Json.Decoder Tokens -tokensDecoder = - Json.map3 - (\a e r -> - { accessToken = a - , expiresIn = e - , refreshToken = r - } - ) - (Json.field "access_token" Json.string) - (Json.field "expires_in" Json.int) - (Json.string - |> Json.field "refresh_token" - |> Json.maybe - ) - - - --- 🛠 - - -exchangeAuthCode : (Result Http.Error Tokens -> msg) -> Url -> String -> Cmd msg -exchangeAuthCode msg url code = - [ ( "client_id", clientId ) - , ( "client_secret", clientSecret ) - , ( "code", code ) - , ( "grant_type", "authorization_code" ) - , ( "redirect_uri", redirectUri url ) - ] - |> Common.queryString - |> String.append "https://api.dropboxapi.com/oauth2/token" - |> (\u -> - { url = u - , body = Http.emptyBody - , expect = Http.expectJson msg tokensDecoder - } - ) - |> Http.post - - -refreshAccessToken : String -> Task String Tokens -refreshAccessToken refreshToken = - [ ( "client_id", clientId ) - , ( "client_secret", clientSecret ) - , ( "refresh_token", refreshToken ) - , ( "grant_type", "refresh_token" ) - ] - |> Common.queryString - |> String.append "https://api.dropboxapi.com/oauth2/token" - |> (\u -> - { method = "POST" - , headers = [] - , url = u - , body = Http.emptyBody - , resolver = - Http.stringResolver - (\resp -> - resp - |> Http.responseToString - |> Result.mapError Http.errorToString - |> Result.andThen - (Json.decodeString tokensDecoder - >> Result.mapError Json.errorToString - ) - ) - , timeout = Nothing - } - ) - |> Http.task diff --git a/src/Library/User/Layer/Methods/RemoteStorage.elm b/src/Library/User/Layer/Methods/RemoteStorage.elm deleted file mode 100644 index 62f9f9d1d..000000000 --- a/src/Library/User/Layer/Methods/RemoteStorage.elm +++ /dev/null @@ -1,113 +0,0 @@ -module User.Layer.Methods.RemoteStorage exposing (Attributes, oauthAddress, parseUserAddress, userAddressError, webfingerAddress, webfingerDecoder, webfingerError, webfingerRequest) - -import Base64 -import Http -import Json.Decode as Decode exposing (Decoder) -import Url -import UrlBase64 - - - --- 🌳 - - -type alias Attributes = - { host : String - , username : String - } - - -userAddressError = - "Please provide a valid RemoteStorage address, the format is **user@server**" - - -webfingerError = - "**Failed to connect** to the given RemoteStorage server, maybe a typo?" - - - --- 🔱 - - -parseUserAddress : String -> Maybe Attributes -parseUserAddress str = - case String.split "@" str of - [ u, h ] -> - Just { host = h, username = u } - - _ -> - Nothing - - -oauthAddress : { oauthOrigin : String, origin : String } -> Attributes -> String -oauthAddress { oauthOrigin, origin } { host, username } = - let - hostWithoutProtocol = - host - |> String.split "://" - |> List.drop 1 - |> List.head - |> Maybe.withDefault host - - ua = - (username ++ "@" ++ hostWithoutProtocol) - |> UrlBase64.encode (Base64.encode >> Ok) - |> Result.withDefault "BASE64_ENCODING_FAILED" - in - String.concat - [ oauthOrigin - , "?redirect_uri=" ++ Url.percentEncode (origin ++ "?action=authenticate/remotestorage/" ++ ua) - , "&client_id=" ++ Url.percentEncode origin - , "&scope=" ++ Url.percentEncode "diffuse:rw" - , "&response_type=token" - ] - - -webfingerAddress : Url.Protocol -> Attributes -> String -webfingerAddress originProtocol { host, username } = - let - fallbackProtocol = - case originProtocol of - Url.Http -> - "http" - - Url.Https -> - "https" - - protocol = - if String.contains "://" host then - host - |> String.split "://" - |> List.head - |> Maybe.withDefault fallbackProtocol - - else - fallbackProtocol - - hostWithoutProtocol = - host - |> String.split "://" - |> List.drop 1 - |> List.head - |> Maybe.withDefault host - in - protocol ++ "://" ++ hostWithoutProtocol ++ "/.well-known/webfinger?resource=acct:" ++ Url.percentEncode (username ++ "@" ++ hostWithoutProtocol) - - -webfingerDecoder : Decoder String -webfingerDecoder = - Decode.at - [ "links" - , "0" - , "properties" - , "http://tools.ietf.org/html/rfc6749#section-4.2" - ] - Decode.string - - -webfingerRequest : (Attributes -> Result Http.Error String -> msg) -> Url.Protocol -> Attributes -> Cmd msg -webfingerRequest toMsg originProtocol rs = - Http.get - { url = webfingerAddress originProtocol rs - , expect = Http.expectJson (toMsg rs) webfingerDecoder - } diff --git a/src/README.md b/src/README.md deleted file mode 100644 index 17b2300ef..000000000 --- a/src/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Elm code - -Elm directories: -- src/Core -- src/Library - -`UI` is the Elm application that'll be executed on the main thread (ie. the UI thread) and `Brain` is the Elm application that'll live inside a web worker. `UI` will be the main application and `Brain` does the heavy lifting. The code shared between these two applications lives in `Library`. The library also contains the more "generic" code that's not necessarily tied to one or the other. Additionally you have `Themes` which is a layer on top of the UI code. - - - -## Emoji Comments - -``` -⛩ Gates (Flags & Ports) -🌳 Types & Initial State -📣 Updates -📰 Subscriptions -🗺 Views - -🧠 Brain -🛠️ Functions -🖼 Styles -㊙️ Secret -``` diff --git a/src/Static/About/CORS.md b/src/Static/About/CORS.md deleted file mode 100644 index 5c72c453a..000000000 --- a/src/Static/About/CORS.md +++ /dev/null @@ -1,121 +0,0 @@ -> A music player that connects to your cloud & distributed storage - -[Return to the application](../../)
-[About](../) - - - -
- -### CORS - -There's only one thing you need to do yourself so that the service you chose will work with the application, and that's setting up [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (Cross-Origin Resource Sharing). Here are the instructions you'll need for each service: - -
- -#### Amazon S3 - -You can find the CORS configuration editor under the "Permissions" tab, on the S3 AWS Console. - -```json -[ - { - "AllowedHeaders": [ - "Range", - "X-Playback-Session-Id" - ], - "AllowedMethods": [ - "GET", - "HEAD" - ], - "AllowedOrigins": [ - "https://diffuse.sh", - "http://127.0.0.1:44999" - ], - "ExposeHeaders": [ - "Accept-Ranges", - "Content-Length", - "Content-Range" - ], - "MaxAgeSeconds": 31536000 - } -] -``` - -
- -#### Dropbox - -_Not necessary._ - -
- -#### IPFS - -Add the domain of the app, with the protocol, to the __list of allowed origins__. - -```shell -ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["https://diffuse.sh", "http://diffuse.sh.ipns.localhost:8080", "http://127.0.0.1:44999"]' -``` - -You can also make this change in the Web UI, you'll find it under "Settings → IPFS Config". - -```javascript -{ - "API": { - "HTTPHeaders": { - "Access-Control-Allow-Origin": [ - ... // Default IPFS values - - "https://diffuse.sh", // 🎵 Default - "http://diffuse.sh.ipns.localhost:8080", // IPNS through IPFS Companion - "http://127.0.0.1:44999" // Electron app - ] - } - } -} -``` - -
- -#### Microsoft Azure Storage - -You can find the CORS configuration under the "Settings -> CORS". -Then fill in the following in the input boxes (left to right): - -``` -ALLOWED ORIGINS * -ALLOWED METHODS GET, HEAD -ALLOWED HEADERS Range, X-Playback-Session-Id -EXPOSED HEADERS Accept-Ranges, Content-Length, Content-Range -MAX AGE 0 -``` - -
- -#### WebDAV - -__Depends on your WebDAV server.__ -Example setup for Henrique Dias's [WebDAV server](https://github.com/hacdias/webdav): - -```yaml -cors: - enabled: true - credentials: true - - allowed_headers: - - Authorization - - Depth - - X-Playback-Session-Id - allowed_methods: - - GET - - HEAD - - PROPFIND - allowed_hosts: - - https://diffuse.sh - - http://127.0.0.1:44999 - exposed_headers: - - Accept-Ranges - - Content-Length - - Content-Range -``` diff --git a/src/Static/About/Dev.md b/src/Static/About/Dev.md deleted file mode 100644 index c3e002d6c..000000000 --- a/src/Static/About/Dev.md +++ /dev/null @@ -1,111 +0,0 @@ -> A music player that connects to your cloud & distributed storage - -[Return to the application](../../)
-[About](../) - - - -## Adding Sources Using Query Parameter - -```js -JSON = encodeURIComponent(JSON.stringify({ - // Object contents depends on type of source, see below. - kind: "ipfs", - data: { - name: "Music from IPFS", - - // Source type specific - directoryHash: "Qm..." - } -})) - -"https://diffuse.sh?source=JSON" -``` - -You can add multiple "source" query parameters, if you want to add multiple sources. - -### Amazon S3 - -```yaml -kind: - amazon_s3 - -data: - # Required - accessKey - bucketName - name - region - secretKey - - # Optional - directoryPath - host -``` - -### Azure - -```yaml -kind: - "azure_file" # or "azure_blob" - -data: - # Required - accountName - accountKey - container - name - - # Optional - directoryPath -``` - -### Dropbox - -```yaml -kind: - dropbox - -data: - # Required - accessToken - appKey - name - - # Optional - directoryPath -``` - -### IPFS - -```yaml -kind: - ipfs - -data: - # Required - directoryHash - name - - # Optional - gateway - ipns ← boolean, `t` of `f` - local ← boolean, `t` of `f` -``` - -### WebDAV - -```yaml -kind: - webdav - -data: - # Required - name - url - - # Optional - directoryPath - password - username -``` diff --git a/src/Static/About/Index.md b/src/Static/About/Index.md deleted file mode 100644 index f8f15b5a9..000000000 --- a/src/Static/About/Index.md +++ /dev/null @@ -1,141 +0,0 @@ -> A music player that connects to your cloud & distributed storage - -[Return to the application](../)
-[CORS instructions](cors/)
-[Developers](dev/)
-[Privacy policy](../privacy.txt) - - - -## What makes it different? - -Diffuse is a decentralized music player consisting out of two main parts. One part is the music and the other is your data (eg. playlists), both of which are in locations of your choice. Meaning that there's no central server for Diffuse, all of the processing happens on your device and all the data is in your control. You can use the [web version](https://diffuse.sh), the [native version](https://github.com/icidasset/diffuse/releases) or host it yourself by downloading the pre-built packages from [Github](https://github.com/icidasset/diffuse). - - -### Music layer - -This layer connects to the services on which your music is stored, no data is written to these services. You can combine all of the following: - -- [Amazon S3](https://aws.amazon.com/s3/) -- [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/) -- [Azure File Storage](https://azure.microsoft.com/en-us/services/storage/files/) -- [Dropbox](https://dropbox.com/) -- [IPFS](https://ipfs.io/) (supports DNSLink & IPNS) -- [WebDAV](https://en.wikipedia.org/wiki/WebDAV) - - -### User layer - -This (optional) layer will use a single service on which to store your data externally. Your data being your settings, favourites, playlists, etc. You can choose between these services: - -- [Dropbox](https://www.dropbox.com/) -- [IPFS](https://ipfs.io/) (using MFS) -- [RemoteStorage](https://remotestorage.io/) - - - -
- -## How does it work? - -Diffuse locates all the music files on the given services, extracts the metadata and then stores it via the previously-explained user layer. - - -### Supported File Formats - -- MP3 -- MP4/M4A -- FLAC -- OGG -- WAV -- WEBM - -*Note, support may vary depending on your browser.* - - - -
- -## UI - -There are a few "hidden" features: - -- **Tracks have a context menu** which can be opened by either right clicking, - or holding it (ie. a long tap). Use the ALT key whilst right clicking - on a track to show the alternative track-context menu with more specialized options. -- **You can reorder items** in the queue or a playlist with drag-and-drop. - Select the item you want to move by tapping on it, then tap and hold to move it around. -- You can select multiple tracks using the SHIFT key and then add that selection - to the queue or a playlist using the context menu. -- Click on the now-playing bit to scroll to that track. -- Double tap on a EQ setting to reset it to its default value. - -### Playlists - -To add something to a playlist, and create that playlist if it doesn't exist yet, you open the context menu of a track. To move tracks around in a playlist, first select the track, then drag it. - -### Search - -```shell -# Show me every track where the title, artist or album contains the term 'Parkway' and the term 'Drive'. -Parkway Drive - -# Show me every track of which the artist's name starts with 'park'. -artist:park - -# Show me every track from Parkway Drive of which the album starts with "Deep Blue". -artist:Parkway Drive album:Deep Blue - -# Show me every track from Parkway Drive but not their "Atlas" album. -artist:Parkway Drive - album:Atlas -``` - -### Keyboard - -The app should be usable with only the keyboard, there are various keyboard shortcuts: - -```js -CTRL + K or CMD + K // Show command palette - -CTRL + L // Select playlist using autocompletion -CTRL + N // Scroll to currently-playing track -CTRL + P // Play / Pause -CTRL + R // Toggle Repeat -CTRL + S // Toggle Shuffle - -CTRL + [ or ] // Previous / Next -CTRL + { or } // Seek forwards / Seek backwards - -Alternatively you can use the media-control keys, -if your browser supports it. - -ESC // Close overlay, close context-menu, deselect album cover, etc. - -CTRL + 1 // Tracks -CTRL + 2 // Playlists -CTRL + 3 // Queue -CTRL + 4 // EQ - -CTRL + 8 // Sources -CTRL + 9 // Settings -``` - - - -
- -## Q&A - -### I used version one, where's my data? - -There's a small link, or button if you will, on the "Settings → Import / Export" -page that will allow you to import data from version 1 of the app. Note that this -will need to reflect the authentication/storage method you chose in version 1. - - - -
- -## 🪐 - -_Part of the App Ring_ diff --git a/src/Static/About/Layout.gren b/src/Static/About/Layout.gren deleted file mode 100644 index 8c2f1e48a..000000000 --- a/src/Static/About/Layout.gren +++ /dev/null @@ -1,135 +0,0 @@ -module About.Layout exposing (..) - -import Transmutable.Html as Html exposing ( Html ) -import Transmutable.Html.Attributes as A - - -layout : - { pathToRoot : String - } - -> Array (Html msg) - -> Array (Html msg) -layout { pathToRoot } contents = - [ Html.doctype - , Html.html - [ A.lang "en" - ] - [ Html.head - [] - [ Html.meta - [ A.charset "utf8" - ] - , Html.title - [ Html.text "About | Diffuse" - ] - , Html.meta - [ A.name "description" - , A.content "A music player that connects to your cloud/distributed storage, in the form of a static, serverless, web application." - ] - - , -- Viewport - Html.meta - [ A.name "viewport" - , A.content "width=device-width, initial-scale=1" - ] - - , -- Favicons & Mobile - Html.link - [ A.rel "apple-touch-icon" - , A.href (pathToRoot ++ "apple-touch-icon.png") - , A.attribute "sizes" "180x180" - ] - , Html.link - [ A.rel "icon" - , A.href (pathToRoot ++ "favicon-32x32.png") - , A.attribute "sizes" "32x32" - ] - , Html.link - [ A.rel "icon" - , A.type_ "image/png" - , A.href (pathToRoot ++ "favicon-16x16.png") - , A.attribute "sizes" "16x16" - ] - , Html.link - [ A.rel "manifest" - , A.href (pathToRoot ++ "site.webmanifest") - ] - , Html.link - [ A.rel "mask-icon" - , A.href (pathToRoot ++ "safari-pinned-tab.svg") - , A.attribute "color" "#8a90a9" - ] - , Html.meta - [ A.name "msapplication-TileColor" - , A.content "#8a90a9" - ] - , Html.meta - [ A.name "theme-color" - , A.content "#8a90a9" - ] - - , -- Styles - Html.meta - [ A.name "color-scheme" - , A.content "dark light" - ] - , Html.link - [ A.rel "stylesheet" - , A.href (pathToRoot ++ "about.css") - ] - ] - - , -- - Html.body - [ A.class "font-body text-base01 dark:bg-darkest-hour my-16 bg-white px-4 dark:text-gray-600" - ] - [ Html.main_ - [ A.class "mx-auto max-w-2xl" - ] - [ Html.a - [ A.class "logo inline-block" - , A.href pathToRoot - ] - [ Html.img - [ A.class "block dark:hidden" - , A.src (pathToRoot ++ "images/diffuse-dark.svg") - ] - [] - , Html.img - [ A.class "hidden dark:block" - , A.src (pathToRoot ++ "images/diffuse-light.svg") - ] - [] - , Html.h1 - [] - [ Html.text "Diffuse" - ] - ] - - , -- - Html.article [] contents - ] - - , -- - Html.node - "script" - [ A.src "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.3.2/highlight.min.js" - ] - [] - , Html.node - "script" - [] - [ Html.text "hljs.initHighlightingOnLoad();" - ] - , Html.node - "script" - [] - [ Html.text "if (\"serviceWorker\" in navigator) {\n" - , Html.text " navigator.serviceWorker.register(\"" - , Html.text pathToRoot - , Html.text "service-worker.js\", { type: \"module\" });\n" - , Html.text "}" - ] - ] - ] - ] diff --git a/src/Static/Fonts/Hack/bold-subset.woff2 b/src/Static/Fonts/Hack/bold-subset.woff2 deleted file mode 100644 index 93d425efd..000000000 Binary files a/src/Static/Fonts/Hack/bold-subset.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Hack/bold.woff2 b/src/Static/Fonts/Hack/bold.woff2 deleted file mode 100644 index 1155477e9..000000000 Binary files a/src/Static/Fonts/Hack/bold.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Hack/regular-subset.woff2 b/src/Static/Fonts/Hack/regular-subset.woff2 deleted file mode 100644 index 1e3abb96e..000000000 Binary files a/src/Static/Fonts/Hack/regular-subset.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Hack/regular.woff2 b/src/Static/Fonts/Hack/regular.woff2 deleted file mode 100644 index 524465cf5..000000000 Binary files a/src/Static/Fonts/Hack/regular.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Montserrat/Old/bold.woff2 b/src/Static/Fonts/Montserrat/Old/bold.woff2 deleted file mode 100644 index d9940cd11..000000000 Binary files a/src/Static/Fonts/Montserrat/Old/bold.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Montserrat/Old/extrabold.woff2 b/src/Static/Fonts/Montserrat/Old/extrabold.woff2 deleted file mode 100644 index d5022db1b..000000000 Binary files a/src/Static/Fonts/Montserrat/Old/extrabold.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Montserrat/Old/light.woff2 b/src/Static/Fonts/Montserrat/Old/light.woff2 deleted file mode 100644 index 31b855fe1..000000000 Binary files a/src/Static/Fonts/Montserrat/Old/light.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Montserrat/Old/medium.woff2 b/src/Static/Fonts/Montserrat/Old/medium.woff2 deleted file mode 100644 index 50b35aa92..000000000 Binary files a/src/Static/Fonts/Montserrat/Old/medium.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Montserrat/Old/regular.woff2 b/src/Static/Fonts/Montserrat/Old/regular.woff2 deleted file mode 100644 index 72d13c60b..000000000 Binary files a/src/Static/Fonts/Montserrat/Old/regular.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Montserrat/Old/semibold.woff2 b/src/Static/Fonts/Montserrat/Old/semibold.woff2 deleted file mode 100644 index c9d70ea59..000000000 Binary files a/src/Static/Fonts/Montserrat/Old/semibold.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Montserrat/Variable/variable.woff2 b/src/Static/Fonts/Montserrat/Variable/variable.woff2 deleted file mode 100644 index 2a938cbd6..000000000 Binary files a/src/Static/Fonts/Montserrat/Variable/variable.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Playfair Display/bold.woff2 b/src/Static/Fonts/Playfair Display/bold.woff2 deleted file mode 100644 index 38ae598dd..000000000 Binary files a/src/Static/Fonts/Playfair Display/bold.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/black.woff2 b/src/Static/Fonts/Source Sans Pro/Old/black.woff2 deleted file mode 100755 index a5618c0f6..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/black.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/bold-italic.woff2 b/src/Static/Fonts/Source Sans Pro/Old/bold-italic.woff2 deleted file mode 100755 index 9b659fef5..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/bold-italic.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/bold.woff2 b/src/Static/Fonts/Source Sans Pro/Old/bold.woff2 deleted file mode 100755 index edc11b3ae..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/bold.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/italic.woff2 b/src/Static/Fonts/Source Sans Pro/Old/italic.woff2 deleted file mode 100755 index f384ac680..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/italic.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/light-italic.woff2 b/src/Static/Fonts/Source Sans Pro/Old/light-italic.woff2 deleted file mode 100755 index 8b8e01fc4..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/light-italic.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/light.woff2 b/src/Static/Fonts/Source Sans Pro/Old/light.woff2 deleted file mode 100755 index b53ed655e..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/light.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/regular.woff2 b/src/Static/Fonts/Source Sans Pro/Old/regular.woff2 deleted file mode 100755 index 51a632966..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/regular.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/semibold-italic.woff2 b/src/Static/Fonts/Source Sans Pro/Old/semibold-italic.woff2 deleted file mode 100755 index caa153ed4..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/semibold-italic.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Old/semibold.woff2 b/src/Static/Fonts/Source Sans Pro/Old/semibold.woff2 deleted file mode 100755 index ee29125c6..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Old/semibold.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Variable/italic.woff2 b/src/Static/Fonts/Source Sans Pro/Variable/italic.woff2 deleted file mode 100644 index 13bcc865e..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Variable/italic.woff2 and /dev/null differ diff --git a/src/Static/Fonts/Source Sans Pro/Variable/roman.woff2 b/src/Static/Fonts/Source Sans Pro/Variable/roman.woff2 deleted file mode 100644 index b60988fe2..000000000 Binary files a/src/Static/Fonts/Source Sans Pro/Variable/roman.woff2 and /dev/null differ diff --git a/src/Static/Hosting/CORS b/src/Static/Hosting/CORS deleted file mode 100644 index 72e8ffc0d..000000000 --- a/src/Static/Hosting/CORS +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/src/Static/Hosting/_headers b/src/Static/Hosting/_headers deleted file mode 100644 index f7e3db34b..000000000 --- a/src/Static/Hosting/_headers +++ /dev/null @@ -1,2 +0,0 @@ -/manifest.json - Access-Control-Allow-Origin: * diff --git a/src/Static/Hosting/_redirects b/src/Static/Hosting/_redirects deleted file mode 100644 index 4353d4fcb..000000000 --- a/src/Static/Hosting/_redirects +++ /dev/null @@ -1 +0,0 @@ -/* /index.html 301 diff --git a/src/Static/Hosting/privacy.txt b/src/Static/Hosting/privacy.txt deleted file mode 100644 index 71e9526be..000000000 --- a/src/Static/Hosting/privacy.txt +++ /dev/null @@ -1,20 +0,0 @@ -Privacy Policy --------------- - -Your privacy is important to us. It is Diffuse's policy to respect your privacy regarding any information we may collect from you across our website, https://diffuse.sh, and other sites we own and operate. - -We only ask for personal information when we truly need it to provide a service to you. We collect it by fair and lawful means, with your knowledge and consent. We also let you know why we’re collecting it and how it will be used. - -We only retain collected information for as long as necessary to provide you with your requested service. What data we store, we’ll protect within commercially acceptable means to prevent loss and theft, as well as unauthorised access, disclosure, copying, use or modification. - -We don’t share any personally identifying information publicly or with third-parties, except when required to by law. - -Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and practices of these sites, and cannot accept responsibility or liability for their respective privacy policies. - -You are free to refuse our request for your personal information, with the understanding that we may be unable to provide you with some of your desired services. - -Your continued use of our website will be regarded as acceptance of our practices around privacy and personal information. If you have any questions about how we handle user data and personal information, feel free to contact us. - -This policy is effective as of 11 March 2018. - -Generated by GetTerms.io diff --git a/src/Static/Html/301.html b/src/Static/Html/301.html deleted file mode 100644 index 8f1511c90..000000000 --- a/src/Static/Html/301.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/Static/Html/Application.html b/src/Static/Html/Application.html deleted file mode 100644 index ead34426c..000000000 --- a/src/Static/Html/Application.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - Diffuse - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - diff --git a/src/Static/Images/ep_naturalblack_pattern.jpg b/src/Static/Images/ep_naturalblack_pattern.jpg deleted file mode 100644 index b06aa8ff4..000000000 Binary files a/src/Static/Images/ep_naturalblack_pattern.jpg and /dev/null differ diff --git a/src/Static/Images/ep_naturalblack_pattern.png b/src/Static/Images/ep_naturalblack_pattern.png deleted file mode 100644 index 323409bd7..000000000 Binary files a/src/Static/Images/ep_naturalblack_pattern.png and /dev/null differ diff --git a/src/Static/Images/icon.png b/src/Static/Images/icon.png deleted file mode 100644 index c8a86c342..000000000 Binary files a/src/Static/Images/icon.png and /dev/null differ diff --git a/src/Static/Images/ocean.jpg b/src/Static/Images/ocean.jpg deleted file mode 100644 index 747a64b64..000000000 Binary files a/src/Static/Images/ocean.jpg and /dev/null differ diff --git a/src/Static/Images/zwartevilt.png b/src/Static/Images/zwartevilt.png deleted file mode 100644 index dc1ebab48..000000000 Binary files a/src/Static/Images/zwartevilt.png and /dev/null differ diff --git a/src/Static/Manifests/manifest.json b/src/Static/Manifests/manifest.json deleted file mode 100644 index 9246ea266..000000000 --- a/src/Static/Manifests/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Diffuse", - "short_name": "Diffuse", - "description": "A music player that connects to your cloud/distributed storage", - "version": "3.5.0", - "author": "Steven Vandevelde ", - "icons": [ - { - "src": "images/icon.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "images/icon.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "start_url": ".", - "theme_color": "#8a90a9", - "background_color": "#02070e", - "display": "standalone", - "screenshots": [ - { - "src": "https://icidasset-public.s3.amazonaws.com/diffuse-v3.jpg", - "type": "image/jpg", - "sizes": "2840x1602" - }, - { - "src": "https://icidasset-public.s3.amazonaws.com/diffuse-v3-art.jpg", - "type": "image/jpg", - "sizes": "2840x1602" - } - ] -} diff --git a/src/_agency.vto b/src/_agency.vto new file mode 100644 index 000000000..f18fbcbbe --- /dev/null +++ b/src/_agency.vto @@ -0,0 +1,78 @@ + +
+

Agency

+ +

+ The goal is for every user, no matter their knowledge level, to have agency over their data and their software. One can start with making small changes and gradually progress to making big changes. +

+ +

+ You can store your user-data in various places, and easily export, import and sync it. +

+ +

Take control

+ +
    +
    + +
  • +

    + Level 1: Pick your restaurant, food comes in all shapes and sizes. The equivalent of choosing a Diffuse theme. +

    +
  • + +
  • +

    + Level 2: Take out food from various places, eg. cheese shop + bakery. You choose how you combine the foods and from where you order them. That's facets. +

    +
  • + +
  • +

    + Level 3: Now that you know which food is good and how to make combinations, you might make a slight customization, add a little something of your own (eg. add some spice). However, you're not quite confident enough which spice to pick, so you hire some help. +

    +

    + This can be done using the facets builder which allows you to build on top of a familiar preconfigured foundation and load custom facets. People might share their own, or maybe you use an LLM to generate something for you (eg. an album art gallery). +

    +
  • + +
  • +

    + Level 4: You learned a bit from watching and talking to the help you hired, so you decide to take things in your own hands. +

    +

    + You continue to use the facets builder but learn a bit of HTML, Javascript and CSS; so you're able to write your own facet. +

    +
  • +
    +
    + +
  • +

    + Level 5: At this point you're confident enough to make a meal from scratch. You can start very simple, eg. just throwing a steak in the pan with some butter and some salt to it. Then later add some baked potatoes and go from there. +

    +

    + A similar tool comes into play here, the themes builder. Same concept as the facets builder, but now you need to specify the foundation yourself. You can use the elements listed below to do so. The code for these is available from this website or through the Javascript package. +

    +
  • + +
  • +

    + Level 6: You open your own restaurant. +

    +

    + You can host the theme you made on any web server, it's only HTML after all. Only difference is that you'll have to create the entire HTML tree, not just the body element, as is the case with the theme builder. +

    +
  • + +
  • +

    + Level 7: You got promoted to master chef. Time to open your own restaurant chain. +

    +

    + You can self-host Diffuse, it's open-source! Or you present your own collection of elements. +

    +
  • +
    +
+
diff --git a/src/_components/diffuse/logo.vto b/src/_components/diffuse/logo.vto new file mode 100644 index 000000000..15b15353f --- /dev/null +++ b/src/_components/diffuse/logo.vto @@ -0,0 +1,6 @@ + + Diffuse + + diff --git a/src/_components/diffuse/status.vto b/src/_components/diffuse/status.vto new file mode 100644 index 000000000..343c9251b --- /dev/null +++ b/src/_components/diffuse/status.vto @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/_components/element.vto b/src/_components/element.vto new file mode 100644 index 000000000..fdd364169 --- /dev/null +++ b/src/_components/element.vto @@ -0,0 +1,11 @@ +
+ {{ set slug = title.toLowerCase().replace(/\s+/g, '-') }} + +

{{title}}

+ +

+ {{content}} +

+ + {{ await comp.list({ items }) }} +
diff --git a/src/_components/examples.vto b/src/_components/examples.vto new file mode 100644 index 000000000..8019bab55 --- /dev/null +++ b/src/_components/examples.vto @@ -0,0 +1,20 @@ +
    + {{ for index, item of items }} +
  • +
    + + {{item.title}} + + +
    +
    + {{ item.desc |> md(true) }} +
    +
  • + {{ /for }} +
diff --git a/src/_components/grid-item.vto b/src/_components/grid-item.vto new file mode 100644 index 000000000..e7ed24456 --- /dev/null +++ b/src/_components/grid-item.vto @@ -0,0 +1,75 @@ +{{ set color = (() => { + switch (item.kind) { + case "prelude": return "var(--accent-twist-4)"; + default: return "var(--accent-twist-2)"; + } +})() }} + +{{ set kind = (() => { + // return item.kind ?? "interactive" + switch (item.kind) { + case "prelude": return "feature"; + default: return "interface"; + } +})() }} + +{{ set facetUri = item.url |> facetURI }} + +
  • +
    +
    + {{ if item.kind === "prelude" }} + + {{item.title}} + + {{ else }} + + {{item.title}} + + {{ /if }} + +
    +
    + {{ item.desc |> md(true) }} +
    + {{ if item.author }} +
    + {{ item.author |> md(true) }} +
    + {{ /if }} +
    +
    + +
    + + +
    +
  • diff --git a/src/_components/grid.vto b/src/_components/grid.vto new file mode 100644 index 000000000..2d3cfd3a1 --- /dev/null +++ b/src/_components/grid.vto @@ -0,0 +1,38 @@ +{{ set completeItems = items.filter((i) => !i.incomplete) }} +{{ set incompleteItems = items.filter((i) => i.incomplete) }} + +
    + Filter by + + + + + + + +
    + + Category +
    +
    + +
      + {{ for index, item of completeItems }} + {{ await comp["grid-item"]({ item, index }) }} + {{ /for }} +
    + +{{ if incompleteItems.length > 0 }} +
    + +

    + + +

    + +
      + {{ for index, item of incompleteItems }} + {{ await comp["grid-item"]({ item, index: index + completeItems.length }) }} + {{ /for }} +
    +{{ /if }} diff --git a/src/_components/list.vto b/src/_components/list.vto new file mode 100644 index 000000000..41dcd0de6 --- /dev/null +++ b/src/_components/list.vto @@ -0,0 +1,15 @@ +
      + {{ for item of items }} +
    • + {{ if item.todo }} + [todo] + {{item.title}} + {{ else }} + {{item.title}} + {{ /if }} + {{ if item.desc }} +
      {{item.desc |> md(true)}}
      + {{ /if }} +
    • + {{ /for }} +
    diff --git a/src/_components/nav.vto b/src/_components/nav.vto new file mode 100644 index 000000000..f55b0f0f2 --- /dev/null +++ b/src/_components/nav.vto @@ -0,0 +1,54 @@ +{{ set colorClass = (path) => url === "/" + path ? '' : 'button--transparent' }} +{{ set slug = (cat) => cat.toLowerCase().replace(/\s+/g, '-') }} + + diff --git a/src/_data/elements.js b/src/_data/elements.js new file mode 100644 index 000000000..7a42dea0c --- /dev/null +++ b/src/_data/elements.js @@ -0,0 +1,408 @@ +import { walk } from "@std/fs/walk"; + +const srcDir = new URL("../", import.meta.url).pathname; +/** @type {Record} */ +const sources = {}; + +for await ( + const entry of walk(srcDir + "components", { match: [/element\.js$/] }) +) { + const content = await Deno.readTextFile(entry.path); + const key = entry.path.slice(srcDir.length); + sources[key] = content; +} + +export default { + sources, + + artwork: [ + { + url: "components/artwork/audio-metadata/element.js", + title: "Audio Metadata", + desc: + "Extracts embedded artwork from audio files using the music-metadata library.", + }, + { + url: "components/artwork/input/element.js", + title: "Input", + desc: + "Fetches artwork by delegating to the configured input element's artwork method.", + }, + { + url: "components/artwork/last.fm/element.js", + title: "Last.fm", + desc: + "Fetches cover art from the Last.fm API using track artist and album tags.", + }, + { + url: "components/artwork/musicbrainz/element.js", + title: "MusicBrainz", + desc: + "Fetches cover art from MusicBrainz and the Cover Art Archive using track artist and album tags.", + }, + ], + + configurators: [ + { + url: "components/configurator/artwork/element.js", + title: "Artwork", + desc: + "Takes artwork components as children and tries each in sequence, returning the first non-null result.", + }, + { + url: "components/configurator/input/element.js", + title: "Input", + desc: "Allows for multiple inputs to be used at once.", + }, + { + url: "components/configurator/metadata/element.js", + title: "Metadata", + desc: + "Takes metadata components as children and chains their patches in sequence.", + }, + { + url: "components/configurator/output/element.js", + title: "Output", + desc: + "Enables the user to configure a specific output. If no default output is set, it creates a temporary session by storing everything in memory.", + }, + { + url: "components/configurator/scrobbles/element.js", + title: "Scrobbles", + desc: "Configure multiple scrobblers (music trackers).", + }, + { + url: "components/configurator/upload/element.js", + title: "Upload", + desc: + "Takes upload components as children and enables configuring one or more upload destinations.", + }, + ], + + engines: [ + { + url: "components/engine/audio/element.js", + title: "Audio", + desc: "Plays audio through audio elements.", + }, + { + url: "components/engine/queue/element.js", + title: "Queue", + desc: "A queue for tracks.", + }, + { + url: "components/engine/repeat-shuffle/element.js", + title: "Repeat & Shuffle", + desc: + "Signals synced with local storage (classified by group) that decide if audio should be repeated and if the queue should be shuffled when filling it.", + }, + { + url: "components/engine/scope/element.js", + title: "Scope", + desc: "Signals that could influence the scope of a set of tracks.", + }, + ], + + input: [ + { + url: "components/input/dropbox/element.js", + title: "Dropbox", + desc: "Dropbox, using the Dropbox v2 HTTP API.", + }, + { + url: "components/input/ephemeral-cache/element.js", + title: "Ephemeral Cache", + desc: + "Ephemeral blobs stored in indexedDB, resolving creates temporary Blob URLs. Not responsible for storing blobs.", + }, + { + url: "components/input/https/element.js", + title: "HTTPS", + desc: "HTTPS URLs to audio files or streams.", + }, + { + url: "components/input/https-json/element.js", + title: "HTTPS + JSON", + desc: "HTTPS servers with JSON directory listings.", + }, + { + url: "components/input/icecast/element.js", + title: "Icecast", + desc: + "Icecast internet radio streams. Fetches ICY metadata to populate track information.", + }, + { + url: "components/input/local/element.js", + title: "Local", + desc: + "Audio files or directories from your local device, using the browser's File System Access API.", + }, + { + url: "components/input/opensubsonic/element.js", + title: "Opensubsonic", + desc: "(Open)subsonic audio servers.", + }, + { + url: "components/input/s3/element.js", + title: "S3", + desc: + "AWS S3 and services that provide the same surface API such as Cloudflare R2.", + }, + { + url: "components/input/webdav/element.js", + title: "WebDAV", + desc: + "WebDAV servers. Depends on a service worker handling the `diffuse:basic-auth` query parameter and converting it to a `Authorization` header.", + }, + ], + + metadata: [ + { + url: "components/metadata/audio-file/element.js", + title: "Audio File", + desc: + "Extracts tags and audio stats from audio files using the music-metadata library.", + }, + { + url: "components/metadata/spectrogram/element.js", + title: "Spectrogram", + desc: + "Decodes a track's audio, computes a spectrogram and stores the derived spectral descriptors (centroid, rolloff, spread, flatness, flux) on the track's stats.", + }, + ], + + orchestrators: [ + { + url: "components/orchestrator/artwork/element.js", + title: "Artwork", + desc: + "Fetches cover art for a given set of tracks, stored locally in indexedDB. Uses the artwork configurator to try each configured source in sequence.", + }, + { + url: "components/orchestrator/auto-queue/element.js", + title: "Automatic Queue", + desc: + "Fill the queue automatically with non-manual items (shuffled or regular, based on repeat-shuffle engine).", + }, + { + url: "components/orchestrator/controller/element.js", + title: "Controller", + desc: + "Provides commonly used computed signals derived from the audio engine, queue engine, and output. Exposes currentTrack(), isPlaying(), and references to the underlying engines.", + }, + { + url: "components/orchestrator/cover-groups/element.js", + title: "Cover Groups", + desc: "Groups tracks by cover art to form collections.", + }, + { + url: "components/orchestrator/favourites/element.js", + title: "Favourites", + desc: + "Mark tracks as favourites. Automatically creates an unordered 'Favourites' playlist.", + }, + { + url: "components/orchestrator/media-session/element.js", + title: "Media Session", + desc: + "Keeps the browser/os media session in sync with queue and audio state. Adds handlers for previous, next, seek to, etc.", + }, + { + url: "components/orchestrator/output/element.js", + title: "Output", + desc: + "A default output configuration. Contains all the outputs provided here along with the relevant transformers.", + }, + { + url: "components/orchestrator/path-collections/element.js", + title: "Path Collections", + desc: + "Wraps an output element to generate ephemeral playlists based on the first path segment of each track's URI. Ephemeral items are excluded from storage.", + }, + { + url: "components/orchestrator/process-tracks/element.js", + title: "Process Inputs Into Tracks", + desc: + "Whenever the cached tracks are initially loaded through the passed output element it will list tracks by using the passed input element. Afterwards it loops over all tracks and checks if metadata needs to be fetched. If anything has changed, it'll pass the results to the output element.", + }, + { + url: "components/orchestrator/queue-audio/element.js", + title: "Queue ⭤ Audio", + desc: "Connects the given queue engine to the given audio engine.", + }, + { + url: "components/orchestrator/scoped-tracks/element.js", + title: "Scoped Tracks", + desc: + "Watches the given output's tracks collection and runs them through a built-in search index. Can perform a search and other ways to reduce the scope of tracks based on the given scope engine. Provides a tracks signal similar to output.tracks.collection.", + }, + { + url: "components/orchestrator/scrobble-audio/element.js", + title: "Scrobble ⭤ Audio", + desc: + "Connects the audio engine with a scrobbler element. Calls nowPlaying when a track starts playing and scrobble once the user has listened long enough.", + }, + { + url: "components/orchestrator/sources/element.js", + title: "Sources", + desc: + "Monitor tracks from the given output to form a list of sources based on the input's sources return value.", + }, + { + url: "components/orchestrator/spectrogram-audio/element.js", + title: "Spectrogram ⭤ Audio", + desc: + "When a track starts playing and is missing spectrogram-derived stats, analyses it via the spectrogram metadata element and persists the result to the output. Only the leader tab performs the analysis.", + }, + ], + + output: [ + { + url: "components/output/polymorphic/indexed-db/element.js", + title: "Polymorphic / IndexedDB", + desc: + "Stores output into the local indexedDB. Supports any type of data that indexedDB supports.", + }, + { + url: "components/output/bytes/s3/element.js", + title: "Bytes / S3", + desc: + "Store output data on AWS S3 or compatible services such as Cloudflare R2.", + }, + { + url: "components/output/raw/atproto-passkey/element.js", + title: "Raw / AT Protocol", + desc: + "Store your user data on the storage associated with your ATProtocol identity. Data is lexicon shaped by default so this element takes in that data directly without any transformations.", + }, + { + url: "components/output/bytes/dropbox/element.js", + title: "Bytes / Dropbox", + desc: + "Store output data as raw bytes in the Dropbox app folder.", + }, + ], + + supplements: [ + { + url: "components/supplement/last.fm/element.js", + title: "Last.fm Scrobbler", + desc: "Scrobbles track plays to Last.fm.", + }, + { + url: "components/supplement/listenbrainz/element.js", + title: "ListenBrainz Scrobbler", + desc: "Scrobbles track plays to ListenBrainz.", + }, + { + url: "components/supplement/rocksky/element.js", + title: "Rocksky Scrobbler", + desc: "Scrobbles track plays to Rocksky.", + }, + ], + + transformers: [ + { + url: "components/transformer/output/bytes/automerge/element.js", + title: "Output / Bytes / Automerge", + desc: "Translate data to and from an Automerge CRDT.", + todo: true, + }, + { + url: "components/transformer/output/bytes/dasl-sync/element.js", + title: "Output / Bytes / DASL Sync", + desc: + "Syncs data between local and remote using CID-based diffing and performs union merges with tombstone tracking when both sides have diverged.", + }, + { + url: "components/transformer/output/bytes/json/element.js", + title: "Output / Bytes / JSON", + desc: "Raw data schema output to and from JSON Uint8Array.", + }, + { + url: "components/transformer/output/raw/atproto-sync/element.js", + title: "Output / Raw / AT Protocol Sync", + desc: + "Wraps an AT Protocol output with a local IndexedDB cache. Uses the repo revision to skip unnecessary fetches and performs union merges with tombstone tracking when both local and remote have diverged.", + }, + { + url: "components/transformer/output/refiner/default/element.js", + title: "Output / Refiner / Default", + desc: + "Removes output state that is not meant to be saved to storage, such as ephemeral tracks. Ideally part of every theme.", + }, + { + url: "components/transformer/output/refiner/initial-contents/element.js", + title: "Output / Refiner / Initial Contents", + desc: "Sets the initial contents for an output on first load.", + }, + { + url: + "components/transformer/output/refiner/passkey-encryption/element.js", + title: "Output / Refiner / Track URI Passkey", + desc: + "Encrypts track URIs using a passkey-derived PRF key. On read, decrypts encrypted:// URIs transparently; on write, re-encrypts all URIs before passing downstream.", + }, + { + url: "components/transformer/output/string/json/element.js", + title: "Output / String / JSON", + desc: "Raw data schema output to and from JSON UTF8 string.", + }, + ], + + upload: [ + { + url: "components/upload/dropbox/element.js", + title: "Dropbox", + desc: + "Uploads and deletes files on Dropbox using the Dropbox v2 HTTP API.", + }, + ], + + definitions: [ + { + url: "definitions/output/collaboration.json", + title: "Output / Collaboration", + desc: + "Represents a collaboration between multiple collaborators on a subject, such as a playlist.", + }, + { + url: "definitions/output/facet.json", + title: "Output / Facet", + desc: "Facet pointer or HTML snippet.", + }, + { + url: "definitions/output/playlistItem.json", + title: "Output / Playlist Item", + desc: + "Represents a single item in a playlist. Tracks are matched based on the given criteria. A playlist is formed by grouping items by their playlist property.", + }, + { + url: "definitions/output/playlistItemBundle.json", + title: "Output / Playlist Item Bundle", + desc: "A bundle of playlist items.", + }, + { + title: "Output / Progress", + desc: "Used to track progress of (long) audio playback.", + todo: true, + }, + { + url: "definitions/output/setting.json", + title: "Output / Setting", + desc: + "Represents a single application setting as a key-value pair.", + }, + { + url: "definitions/output/track.json", + title: "Output / Track", + desc: + "Represents audio that can be played, or a placeholder for a source of tracks. Contains a URI that will resolve to the audio.", + }, + { + url: "definitions/output/trackBundle.json", + title: "Output / Track Bundle", + desc: "A bundle of tracks.", + }, + ], +}; diff --git a/src/_data/facets.json b/src/_data/facets.json new file mode 100644 index 000000000..8dc28e8f4 --- /dev/null +++ b/src/_data/facets.json @@ -0,0 +1,331 @@ +[ + { + "url": "facets/playback/auto-queue/prelude/index.html", + "title": "Automatic Queue", + "kind": "prelude", + "category": "Playback", + "featured": true, + "desc": "Automatically put tracks into the queue. **Always on.** These tracks will always be located after the ones you've added manually." + }, + { + "url": "facets/playback/auto-queue/index.html", + "title": "Automatic Queue", + "category": "Playback", + "desc": "Automatically put tracks into the queue when this interface is opened. Also allows for controlling shuffle, repeat, search, sorting and playlist selection." + }, + { + "url": "facets/themes/blur/facet/index.html", + "title": "Blur", + "category": "Themes", + "featured": true, + "author": "A theme by [@tokono.ma](https://bsky.app/profile/tokono.ma)", + "desc": "A combination of the classic Diffuse browser and the Apple Music iOS playback screen." + }, + { + "url": "facets/themes/blur/artwork-controller/facet/index.html", + "title": "Blur / Artwork Controller", + "category": "Playback", + "featured": true, + "desc": "Audio playback controller with an artwork display. Play audio from the queue, add tracks to your favourites, control the queue and volume." + }, + { + "url": "facets/themes/blur/browser/facet/index.html", + "title": "Blur / Browser", + "category": "Browsing", + "desc": "Collection browser and search with favourite toggling, date grouping, and virtual scrolling." + }, + { + "url": "facets/themes/blur-classic/facet/index.html", + "title": "Blur Classic", + "category": "Themes", + "featured": true, + "author": "A theme by [@tokono.ma](https://bsky.app/profile/tokono.ma)", + "desc": "A recreation of the Diffuse v3 theme with the browser of the newer Blur theme." + }, + { + "url": "facets/themes/blur-pocket/facet/index.html", + "title": "Blur Pocket", + "category": "Themes", + "author": "A theme by [@tokono.ma](https://bsky.app/profile/tokono.ma)", + "incomplete": true, + "desc": "A mobile-optimised and minimalised version of the Blur theme. Does not include the background images and has a slightly different color palette." + }, + { + "url": "facets/data/cache-tracks/index.html", + "title": "Cache Tracks", + "kind": "prelude", + "category": "Data", + "featured": true, + "desc": "Store tracks locally for offline usage automatically after they've been playing for a little while." + }, + { + "url": "facets/themes/catalogue-one/facet/index.html", + "title": "Catalogue One", + "category": "Themes", + "author": "A theme by [@tokono.ma](https://bsky.app/profile/tokono.ma)", + "desc": "A visual, cover-art-first browsing experience. Browse your collection as grids of albums and artists, manage the queue, and control playback." + }, + { + "url": "facets/misc/command/index.html", + "title": "Command Menu", + "category": "Miscellaneous", + "desc": "A command palette for various actions." + }, + { + "url": "facets/connect/index.html", + "title": "Connect", + "category": "Data", + "featured": true, + "desc": "A listing of some of the options available to use as an audio source, or as a user-data storage method." + }, + { + "url": "facets/connect/atproto-passkey/index.html", + "title": "Connect / Atmosphere (Public)", + "category": "Data", + "desc": "Use your AT Protocol account for user-data storage. Your data will be stored as public records, but sensitive data can be encrypted using a passkey." + }, + { + "url": "facets/connect/dropbox/index.html", + "title": "Connect / Dropbox", + "category": "Data", + "desc": "Add Dropbox as an audio source or use it as user-data storage." + }, + { + "url": "facets/connect/https/index.html", + "title": "Connect / HTTPS", + "category": "Data", + "desc": "Add HTTPS URLs as audio sources." + }, + { + "url": "facets/connect/https-json/index.html", + "title": "Connect / HTTPS + JSON", + "category": "Data", + "desc": "Add an HTTPS server with JSON directory listing support as an audio source." + }, + { + "url": "facets/connect/icecast/index.html", + "title": "Connect / Icecast", + "category": "Data", + "desc": "Add an Icecast stream as an audio source." + }, + { + "url": "facets/connect/local/index.html", + "title": "Connect / Local", + "category": "Data", + "desc": "Add local directories or files as audio sources." + }, + { + "url": "facets/connect/opensubsonic/index.html", + "title": "Connect / OpenSubsonic", + "category": "Data", + "desc": "Add an OpenSubsonic server as an audio source." + }, + { + "url": "facets/connect/s3/index.html", + "title": "Connect / S3", + "category": "Data", + "desc": "Connect to an S3-compatible storage for audio input or user-data storage." + }, + { + "url": "facets/connect/webdav/index.html", + "title": "Connect / WebDAV", + "category": "Data", + "desc": "Add a WebDAV server as an audio source." + }, + { + "url": "facets/data/artwork-bundle/index.html", + "title": "Default Artwork Bundle", + "kind": "prelude", + "category": "Data", + "tags": ["base"], + "desc": "The default setup for track artwork retrieval. Adds support for: embedded audio metadata, Last.fm, and MusicBrainz." + }, + { + "url": "facets/data/input-bundle/index.html", + "title": "Default Input Bundle", + "kind": "prelude", + "category": "Data", + "tags": ["base"], + "desc": "The default setup for audio input sources. Adds support for: Dropbox, HTTPS, Icecast, the local filesystem, OpenSubsonic, S3-compatible storage, and WebDAV." + }, + { + "url": "facets/data/metadata-bundle/index.html", + "title": "Default Metadata Bundle", + "kind": "prelude", + "category": "Data", + "tags": ["base"], + "desc": "The default setup for track metadata lookups. Reads tags and audio stats from audio files." + }, + { + "url": "facets/data/output-bundle/index.html", + "title": "Default Output Bundle", + "kind": "prelude", + "category": "Data", + "tags": ["base"], + "desc": "The default setup for user-data storage output. Adds support for: AT Protocol, Dropbox, and S3-compatible storage. For these a custom local-first syncing algorithm is used." + }, + { + "url": "facets/data/upload-bundle/index.html", + "title": "Default Upload Bundle", + "kind": "prelude", + "category": "Data", + "tags": ["base"], + "desc": "The default setup for uploading audio files to cloud storage. Adds support for: Dropbox." + }, + { + "url": "facets/data/export-import/index.html", + "title": "Export & Import", + "category": "Data", + "desc": "Export all data as a JSON snapshot, or restore from a previously exported file." + }, + { + "url": "facets/data/file-manager/index.html", + "title": "File Manager", + "category": "Data", + "featured": true, + "desc": "Upload audio files to play them and optionally sync them with your cloud storage. These are automatically added as inputs (aka. sources)." + }, + { + "url": "facets/themes/ipod/facet/index.html", + "title": "iPod", + "category": "Themes", + "author": "A theme by [@tokono.ma](https://bsky.app/profile/tokono.ma)", + "featured": true, + "desc": "A recreation of the click-wheel iPod UI. Spin the wheel to scroll, tap the ring for menu / prev / next / play, and press the centre button to select. Alternatively, use your keyboard arrow keys." + }, + { + "url": "facets/data/playlists/index.html", + "title": "Playlists", + "category": "Data", + "featured": true, + "desc": "Manage your playlists." + }, + { + "url": "facets/playback/preload/prelude/index.html", + "title": "Preload Tracks", + "kind": "prelude", + "category": "Playback", + "tags": ["base"], + "desc": "Automatically preload the next track in the queue." + }, + { + "url": "facets/data/process-tracks/prelude/index.html", + "title": "Process Tracks", + "kind": "prelude", + "category": "Data", + "desc": "Process all your audio inputs into tracks automatically when opening any interface. Only happens once every 10 minutes, if the processing was completed." + }, + { + "url": "facets/data/process-tracks/index.html", + "title": "Process Tracks", + "category": "Data", + "desc": "Process all your audio sources into tracks. Shows a progress bar when processing is occuring." + }, + { + "url": "facets/playback/queue/index.html", + "title": "Queue", + "category": "Playback", + "featured": true, + "desc": "Manage your playback queue. Reorder upcoming tracks, clear the queue, and view playback history." + }, + { + "url": "facets/misc/scrobble/index.html", + "title": "Scrobble", + "kind": "prelude", + "category": "Miscellaneous", + "desc": "Enable scrobbling, keep track of what you're listening to. Adds support for these scrobblers: Last.fm, ListenBrainz, Rocksky" + }, + { + "url": "facets/misc/scrobble/last.fm/index.html", + "title": "Scrobble / Last.fm", + "category": "Miscellaneous", + "desc": "Connect to Last.fm to setup the Last.fm scrobbler." + }, + { + "url": "facets/misc/scrobble/listenbrainz/index.html", + "title": "Scrobble / ListenBrainz", + "category": "Miscellaneous", + "desc": "Connect to ListenBrainz to setup the ListenBrainz scrobbler." + }, + { + "url": "facets/misc/scrobble/rocksky/index.html", + "title": "Scrobble / Rocksky", + "category": "Miscellaneous", + "desc": "Connect to Rocksky to setup the Rocksky scrobbler." + }, + { + "url": "facets/setup/index.html", + "title": "Setup", + "category": "Miscellaneous", + "desc": "An interactive getting-started guide. Shows contextual next steps based on your current progress." + }, + { + "url": "facets/data/sources/index.html", + "title": "Sources", + "category": "Data", + "featured": true, + "desc": "An overview of all audio inputs. Disable a source to hide its tracks without removing them." + }, + { + "url": "facets/data/spectrogram/index.html", + "title": "Spectrogram Analysis", + "kind": "prelude", + "category": "Data", + "featured": true, + "desc": "Analyse the spectrogram of tracks as they play and store the data on their stats. Only runs for tracks that don't already have spectral stats." + }, + { + "url": "facets/misc/split-view/index.html", + "title": "Split View", + "category": "Miscellaneous", + "desc": "Arrange multiple facets side-by-side in a resizable split-panel layout." + }, + { + "url": "facets/themes/tidal/facet/index.html", + "title": "Tidal", + "category": "Themes", + "incomplete": true, + "desc": "A Tidal-inspired browser. Pick a playlist from the sidebar, browse albums and artists, and play tracks." + }, + { + "url": "facets/themes/tidal/browser/facet/index.html", + "title": "Tidal / Browser", + "category": "Browsing", + "incomplete": true, + "desc": "Tidal-style collection browser and search with a playlist sidebar and album / artist grids." + }, + { + "url": "facets/themes/tidal/player/facet/index.html", + "title": "Tidal / Player", + "category": "Playback", + "incomplete": true, + "desc": "Tidal-style now-playing bar with playback controls, progress and volume." + }, + { + "url": "facets/data/v3-import/index.html", + "title": "V3.x Import", + "category": "Data", + "desc": "Import data from Diffuse v3." + }, + { + "url": "facets/themes/winamp/facet/index.html", + "title": "Winamp", + "category": "Themes", + "featured": true, + "author": "A theme by [@tokono.ma](https://bsky.app/profile/tokono.ma) and [@captbaritone](https://github.com/captbaritone)", + "desc": "Windows 98 inspired theme with a classic Winamp skin for the audio player. The Winamp playlist is a combination of the queue's history, the track that's currently playing and the future queue items." + }, + { + "url": "facets/themes/winamp/browser/facet/index.html", + "title": "Winamp / Browser", + "category": "Browsing", + "desc": "Collection browser and search." + }, + { + "url": "facets/data/userdata/index.html", + "title": "Your data", + "category": "Data", + "incomplete": true, + "desc": "Manage where your user-data is stored. See which storage method is active, switch between methods, and migrate data between them." + } +] \ No newline at end of file diff --git a/src/_includes/layouts/diffuse.vto b/src/_includes/layouts/diffuse.vto new file mode 100644 index 000000000..9bfb4e686 --- /dev/null +++ b/src/_includes/layouts/diffuse.vto @@ -0,0 +1,74 @@ +--- +title: "Diffuse" + +base: "./" +--- + + + + + + + + + + + + + + {{title}} + + + + + + + + + + + + {{ for url of (preloads ?? []) }} + + {{ /for }} + + + {{ for url of styles }} + + {{ /for }} + + + + + + {{ content }} + + + + + {{ for url of scripts }} + + {{ /for }} + + diff --git a/src/_includes/layouts/kitchen-category.vto b/src/_includes/layouts/kitchen-category.vto new file mode 100644 index 000000000..edd8dd915 --- /dev/null +++ b/src/_includes/layouts/kitchen-category.vto @@ -0,0 +1,7 @@ +--- +layout: layouts/kitchen.vto +--- + +
    + {{ await comp.grid({ id: slug, items: categoryFacets }) }} +
    diff --git a/src/_includes/layouts/kitchen.vto b/src/_includes/layouts/kitchen.vto new file mode 100644 index 000000000..0321e3e28 --- /dev/null +++ b/src/_includes/layouts/kitchen.vto @@ -0,0 +1,40 @@ +--- +layout: layouts/diffuse.vto +base: ../ +title: Diffuse + +styles: + - styles/base.css + - styles/diffuse/page.css + - styles/diffuse/code-editor.css + - vendor/@phosphor-icons/web/bold/style.css + - vendor/@phosphor-icons/web/fill/style.css + +scripts: + - common/pages/ppr.js +--- + +
    +
    +
    +
    + + {{ await comp.diffuse.logo() }} + + + {{ await comp.diffuse.status() }} + +
    +
    + {{ await comp.nav({ url, facets }) }} +
    +
    + +
    + {{ content }} +
    + + diff --git a/src/catalogue.vto b/src/catalogue.vto new file mode 100644 index 000000000..ac471babe --- /dev/null +++ b/src/catalogue.vto @@ -0,0 +1,11 @@ +--- +layout: layouts/kitchen.vto +base: ../ +title: Catalogue | Diffuse +--- + +

    Featured

    + +
    + {{ await comp.grid({ id: "catalogue", items: facets }) }} +
    diff --git a/src/common/cid.js b/src/common/cid.js new file mode 100644 index 000000000..ba76b8714 --- /dev/null +++ b/src/common/cid.js @@ -0,0 +1,51 @@ +import * as CID from "@atcute/cid"; +import { equals, toSha256 } from "@atcute/uint8array"; + +/** + * @param {0x55 | 0x71} code + * @param {Uint8Array} data + * + * @example Returns a non-empty base32 CID string, consistent for same input, different for different inputs + * ```js + * import { create } from "~/common/cid.js"; + * + * const data = new TextEncoder().encode("hello world"); + * const cid = await create(0x55, data); + * if (typeof cid !== "string" || cid.length === 0) throw new Error("CID should be a non-empty string"); + * if (!/^[a-z2-7]+$/.test(cid)) throw new Error("CID should be base32-encoded"); + * + * const cid2 = await create(0x55, data); + * if (cid !== cid2) throw new Error("same input should produce same CID"); + * + * const cidDiff = await create(0x55, new TextEncoder().encode("world")); + * if (cid === cidDiff) throw new Error("different input should produce different CID"); + * + * const cidCodec = await create(0x71, data); + * if (cid === cidCodec) throw new Error("different codec should produce different CID"); + * ``` + */ +export async function create(code, data) { + const cid = await CID.create(code, data); + return CID.toString(cid); +} + +/** + * @param {Uint8Array} data + * @param {string} expected + * + * @example Returns true for matching data and false for mismatched data + * ```js + * import { create, verify } from "~/common/cid.js"; + * + * const data = new TextEncoder().encode("hello"); + * const cid = await create(0x55, data); + * if (!await verify(data, cid)) throw new Error("should verify matching data"); + * if (await verify(new TextEncoder().encode("world"), cid)) throw new Error("should not verify mismatched data"); + * ``` + */ +export async function verify(data, expected) { + const expectedCid = CID.fromString(expected); + const digest = await toSha256(data); + + return equals(digest, expectedCid.digest.contents); +} diff --git a/src/common/compare.js b/src/common/compare.js new file mode 100644 index 000000000..0af7c18a5 --- /dev/null +++ b/src/common/compare.js @@ -0,0 +1,60 @@ +import deepDiff from "@fry69/deep-diff"; + +/** + * @param {any} a + * @param {any} b + * + * @example Returns true for identical primitives and false for different ones + * ```js + * import { diff } from "~/common/compare.js"; + * + * if (!diff(1, 1)) throw new Error("identical primitives should be equal"); + * if (diff(1, 2)) throw new Error("different primitives should not be equal"); + * ``` + * + * @example Returns true for deeply equal objects and false for different ones + * ```js + * import { diff } from "~/common/compare.js"; + * + * if (!diff({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })) throw new Error("deeply equal objects should be equal"); + * if (diff({ a: 1 }, { a: 2 })) throw new Error("objects with different values should not be equal"); + * if (diff({ a: 1 }, { b: 1 })) throw new Error("objects with different keys should not be equal"); + * ``` + * + * @example Returns true for identical arrays and false for different ones + * ```js + * import { diff } from "~/common/compare.js"; + * + * if (!diff([1, 2, 3], [1, 2, 3])) throw new Error("identical arrays should be equal"); + * if (diff([1, 2], [1, 3])) throw new Error("arrays with different elements should not be equal"); + * ``` + */ +export function diff(a, b) { + return !deepDiff(a, b); +} + +/** + * @param {any} a + * @param {any} b + * + * @example Returns true for same primitive value, false for different + * ```js + * import { strictEquality } from "~/common/compare.js"; + * + * if (!strictEquality(42, 42)) throw new Error("same primitives should be strictly equal"); + * if (strictEquality(42, 43)) throw new Error("different primitives should not be strictly equal"); + * if (strictEquality(null, undefined)) throw new Error("null and undefined should not be strictly equal"); + * ``` + * + * @example Returns false for same-content objects (reference inequality) + * ```js + * import { strictEquality } from "~/common/compare.js"; + * + * if (strictEquality({ a: 1 }, { a: 1 })) throw new Error("different object references should not be strictly equal"); + * const obj = { a: 1 }; + * if (!strictEquality(obj, obj)) throw new Error("same object reference should be strictly equal"); + * ``` + */ +export function strictEquality(a, b) { + return a === b; +} diff --git a/src/common/element.d.ts b/src/common/element.d.ts new file mode 100644 index 000000000..52f40d9e2 --- /dev/null +++ b/src/common/element.d.ts @@ -0,0 +1,22 @@ +import type { Tunnel } from "./worker.d.ts"; + +export type BroadcastingStatus = + | { leader: true; initialLeader: boolean } + | { leader: false }; + +export type HtmlTagFunction = ( + strings: string[] | ArrayLike, + ...values: unknown[] +) => string; + +export type RenderArg = { + html: HtmlTagFunction; + state: State; +}; + +export type WorkerOpts = { + forceNew?: boolean | { + self?: boolean; + dependencies?: Record; + }; +}; diff --git a/src/common/element.js b/src/common/element.js new file mode 100644 index 000000000..dbe2bf9fe --- /dev/null +++ b/src/common/element.js @@ -0,0 +1,579 @@ +import QS from "query-string"; +import { html, render } from "lit-html"; + +import { effect, signal } from "~/common/signal.js"; +import { rpc, workerLink, workerProxy, workerTunnel } from "./worker.js"; +import { RpcChannel } from "./worker/rpc-channel.js"; + +/** + * @import {BroadcastingStatus, WorkerOpts} from "./element.d.ts" + * @import {ProxiedActions, Tunnel} from "./worker.d.ts"; + * @import {Signal} from "./signal.d.ts" + */ + +export { html, nothing } from "lit-html"; +export const DEFAULT_GROUP = "default"; + +/** + * Base for custom elements, provides some utility functionality + * around rendering and managing signals. + */ +export class DiffuseElement extends HTMLElement { + $connected = signal(false); + + #connected = Promise.withResolvers(); + #disposables = /** @type {Array<() => void>} */ ([]); + + /** */ + constructor() { + super(); + + this.worker = this.worker.bind(this); + this.workerLink = this.workerLink.bind(this); + } + + /** + * @param {string} _name + * @param {string} oldValue + * @param {string} newValue + */ + attributeChangedCallback(_name, oldValue, newValue) { + if (oldValue !== newValue) this.#render(); + } + + /** + * Effect helper that automatically is disposes + * when this element is removed from the DOM. + * + * @param {() => void} fn + */ + effect(fn) { + const unregister = effect(fn); + this.#disposables.push(unregister); + return unregister; + } + + /** */ + forceRender() { + return this.#render(); + } + + /** */ + get group() { + return this.getAttribute("group") ?? DEFAULT_GROUP; + } + + /** */ + get identifier() { + const ns = this.namespace; + return `${this.constructor.prototype.constructor.NAME}/${this.group}${ + ns?.length ? "/" + ns : "" + }`; + } + + /** */ + get label() { + return this.getAttribute("label") ?? this.id ?? this.localName; + } + + /** */ + get namespace() { + return this.getAttribute("namespace") + ? this.getAttribute("namespace") + : undefined; + } + + /** */ + root() { + return (this.shadowRoot ?? this); + } + + /** */ + get selector() { + return this.id.length ? `#${this.id}` : this.localName; + } + + /** */ + whenConnected() { + return this.#connected.promise; + } + + /** */ + whenDefined() { + return customElements.whenDefined(this.localName); + } + + /** + * Avoid replacing the whole subtree, + * morph the existing DOM into the new given tree. + */ + #render() { + if (!("render" in this && typeof this.render === "function")) return; + + const tmp = this.render({ + html: html, + state: "state" in this ? this.state : undefined, + }); + + render(tmp, this.root()); + } + + // LIFECYCLE + + connectedCallback() { + this.$connected.value = true; + this.#connected.resolve(null); + + if (!("render" in this && typeof this.render === "function")) return; + + this.effect(() => { + if (!("render" in this && typeof this.render === "function")) return; + this.#render(); + }); + } + + disconnectedCallback() { + this.$connected.value = false; + this.#teardown(); + } + + #teardown() { + this.#disposables.forEach((fn) => fn()); + } + + // WORKERS + + /** @type {undefined | Worker | SharedWorker} */ + #worker; + + createWorker() { + const NAME = this.constructor.prototype.constructor.NAME; + const WORKER_URL = this.constructor.prototype.constructor.WORKER_URL; + + if (!NAME) throw new Error("Missing `NAME` static property"); + if (!WORKER_URL) throw new Error("Missing `WORKER_URL` static property"); + + // Query + const query = QS.stringify( + "workerQuery" in this && typeof this.workerQuery === "function" + ? this.workerQuery() + : {}, + ); + + // Setup worker + const name = this.identifier; + const url = import.meta.resolve("./" + WORKER_URL) + `?${query}`; + + let worker; + + if (this.hasAttribute("group") && typeof SharedWorker !== "undefined") { + worker = new SharedWorker(url, { name, type: "module" }); + } else { + worker = new Worker(url, { name, type: "module" }); + } + + return worker; + } + + /** + * @returns {Record | null} + */ + dependencies() { + return null; + } + + worker() { + this.#worker ??= this.createWorker(); + return this.#worker; + } + + workerLink() { + const worker = this.worker(); + return workerLink(worker); + } + + /** + * @template {Record any>} Actions + * @param {WorkerOpts} [opts] + * @returns {ProxiedActions} + */ + workerProxy(opts) { + return workerProxy( + () => this.workerTunnel(opts).port, + ); + } + + /** + * Creates a MessagePort that is connected to the worker. + * All the dependencies are added automatically. + * + * @param {WorkerOpts} [opts] + */ + workerTunnel({ forceNew } = {}) { + const worker = forceNew === true || + (typeof forceNew === "object" && forceNew.self === true) + ? () => this.createWorker() + : () => this.worker(); + const deps = this.dependencies(); + + let toWorker; + + if (deps) { + toWorker = + /** + * @param {any} msg + */ + async (msg) => { + /** @type {Array<[string, Tunnel]>} */ + const ports = Object.entries(deps).map( + /** @param {[string, DiffuseElement]} _ */ + ([k, v]) => { + const n = typeof forceNew === "object" + ? forceNew.dependencies?.[k] ?? false + : false; + return [k, v.workerTunnel({ forceNew: n })]; + }, + ); + + const data = { + data: Array.isArray(msg.args) ? msg.args[0] : msg.args, + ports: Object.fromEntries(ports.map(([k, v]) => { + return [k, v.port]; + })), + }; + + this.#disposables.push(() => { + ports.forEach(([_k, v]) => v.disconnect()); + }); + + return { + data: { + ...msg, + args: Array.isArray(msg.args) + ? [data, ...msg.args.slice(1)] + : msg.args, + }, + transfer: ports.map(([_k, v]) => v.port), + }; + }; + } + + const tunnel = workerTunnel(worker, { toWorker }); + return tunnel; + } +} + +/** + * Broadcastable version of the base class. + * + * Share the state of an element across multiple tabs + * of the same origin and have one instance be the leader. + */ +export class BroadcastableDiffuseElement extends DiffuseElement { + broadcasted = false; + + /** @type {{ assumeLeadership?: boolean }} */ + #broadcastingOptions = {}; + + #broadcastingStatus; + broadcastingStatus; + + /** @type {PromiseWithResolvers} */ + #lock = Promise.withResolvers(); + + /** @type {PromiseWithResolvers} */ + #status = Promise.withResolvers(); + + constructor() { + super(); + + this.broadcast = this.broadcast.bind(this); + + /** @type {Signal>} */ + this.#broadcastingStatus = signal(this.#status.promise); + this.broadcastingStatus = this.#broadcastingStatus.get; + } + + /** + * @template {Record any }>} ActionsWithStrategy + * @template {{ [K in keyof ActionsWithStrategy]: ActionsWithStrategy[K]["fn"] }} Actions + * @param {string} channelName + * @param {ActionsWithStrategy} actionsWithStrategy + * @param {{ assumeLeadership?: boolean }} [options] + */ + broadcast(channelName, actionsWithStrategy, options) { + if (this.broadcasted) return; + if (options) this.#broadcastingOptions = options; + + const channel = new BroadcastChannel(channelName); + const msg = new MessageChannel(); + + /** + * @typedef {{ [K in keyof ActionsWithStrategy]: ActionsWithStrategy[K]["fn"] }} A + */ + + this.broadcasted = true; + this.channelName = channelName; + + /** @type {RpcChannel<{}, Actions>} */ + const _rpc = rpc( + msg.port2, + /** @type {Actions} */ ( + Object.fromEntries( + Object.entries(actionsWithStrategy).map(([k, v]) => { + return [k, v.fn.bind(this)]; + }), + ) + ), + ); + + channel.addEventListener( + "message", + async (event) => { + if (event.data?.method?.startsWith("leader:")) { + const status = await this.#status.promise; + if (status.leader) { + msg.port1.postMessage({ + ...event.data, + method: event.data.method.slice("leader:".length), + }); + } + } else { + msg.port1.postMessage(event.data); + } + }, + ); + + msg.port1.addEventListener( + "message", + (event) => channel.postMessage(event.data), + ); + + msg.port1.start(); + msg.port2.start(); + + async function anyoneWaiting() { + if (typeof navigator.locks?.query !== "function") return false; + const state = await navigator.locks.query(); + return !!state.pending?.length; + } + + /** @type {RpcChannel<{}, Actions>} */ + const proxyChannel = new RpcChannel(msg.port2); + + /** @type {ProxiedActions} */ + const proxy = proxyChannel.getAPI(); + + /** @type {any} */ + const actions = {}; + + Object.entries(actionsWithStrategy).forEach( + ([action, { fn, strategy }]) => { + const ogFn = fn.bind(this); + let wrapFn = ogFn; + + switch (strategy) { + case "leaderOnly": + /** @param {Parameters} args */ + wrapFn = async (...args) => { + const status = await this.#status.promise; + return status.leader + ? ogFn(...args) + : proxyChannel.callMethod(`leader:${action}`, args); + }; + break; + + case "replicate": + /** @param {Parameters} args */ + wrapFn = async (...args) => { + anyoneWaiting().then((bool) => { + if (bool) proxy[action](...args); + }); + return ogFn(...args); + }; + break; + } + + actions[action] = wrapFn; + }, + ); + + return /** @type {ProxiedActions} */ (actions); + } + + async isLeader() { + if (this.broadcasted) { + const status = await this.broadcastingStatus(); + return status.leader; + } else { + return true; + } + } + + // LIFECYCLE + + /** + * @override + */ + connectedCallback() { + super.connectedCallback(); + + if (!this.broadcasted) return; + + // Grab a lock if it isn't acquired yet and if needed, + // and hold it until `this.lock.promise` resolves. + const assumeLeadership = this.#broadcastingOptions?.assumeLeadership; + + if (assumeLeadership === undefined || assumeLeadership === true) { + if (typeof navigator.locks?.request === "function") { + navigator.locks.request( + `${this.channelName}/lock`, + assumeLeadership === true ? { steal: true } : { ifAvailable: true }, + (lock) => { + this.#status.resolve( + lock ? { leader: true, initialLeader: true } : { leader: false }, + ); + if (lock) return this.#lock.promise; + }, + ); + } else { + this.#status.resolve( + { leader: true, initialLeader: true }, + ); + } + } else { + this.#status.resolve( + { leader: false }, + ); + } + + // When the lock status is initially determined, log its status. + // Additionally, wait for lock if needed. + this.#status.promise.then((status) => { + if (status.leader) { + console.log(`🧙 Elected leader for: ${this.channelName}`); + } else { + console.log(`🔮 Watching leader: ${this.channelName}`); + } + + // Wait for leadership + if (status.leader === false) { + navigator.locks.request( + `${this.channelName}/lock`, + () => { + this.#status = Promise.withResolvers(); + this.#status.resolve({ leader: true, initialLeader: false }); + + this.#broadcastingStatus.value = this.#status.promise; + + return this.#lock.promise; + }, + ); + } + }); + } + + /** + * @override + */ + disconnectedCallback() { + super.disconnectedCallback(); + this.#lock.resolve(); + } +} + +/** + * Component DOM selector. + * + * Basically `document.querySelector` but returns the element + * with the correct type based on the element module given. + * + * ``` + * import * as QueryEngine from "~/components/engine/query/element.js" + * + * const instance = component(QueryEngine) + * ``` + * + * @template {abstract new (...args: any[]) => any} C + * @param {{ CLASS: C; NAME: string }} elementModule + * @param {string} [id] Optional id to select + */ +export function component(elementModule, id) { + const el = document.querySelector( + id ? `${elementModule.NAME}#${id}` : elementModule.NAME, + ); + if (!el) { + throw new Error(`Element for selector '${elementModule.NAME}' not found.`); + } + return /** @type {InstanceType} */ (el); +} + +/** + * Defines a custom element, guarding against double-registration. + * + * @param {string} name + * @param {CustomElementConstructor} constructor + */ +export function defineElement(name, constructor) { + if (!customElements.get(name)) customElements.define(name, constructor); + else {console.warn( + `The '${name}' element was asked to be defined again, this should be avoided. The code may have been executed multiple times.`, + );} +} + +/** + * @template {HTMLElement} T + * @param {DiffuseElement} parent + * @param {string} attribute + * @returns {T} + */ +export function query(parent, attribute) { + const selector = parent.getAttribute(attribute); + + if (!selector) { + throw new Error(`Missing required '${attribute}' attribute`); + } + + /** @type {T | null} */ + const element = document.querySelector(selector); + if (!element) throw new Error(`Missing required '${selector}' element`); + return element; +} + +/** + * @template {HTMLElement} T + * @param {DiffuseElement} parent + * @param {string} attribute + */ +export function queryOptional(parent, attribute) { + const selector = parent.getAttribute(attribute); + + if (!selector) { + return null; + } + + /** @type {T | null} */ + const elementOrNull = document.querySelector(selector); + return elementOrNull; +} + +/** + * @param {Record} workers + */ +export function terminateWorkers(workers) { + Object.values(workers).forEach((worker) => { + if (worker instanceof Worker) worker.terminate(); + }); +} + +/** + * @template {Record} T + * @param {T} elements + */ +export async function whenElementsDefined(elements) { + await Promise.all( + Object.values(elements).map((element) => + customElements.whenDefined(element.localName) + ), + ); +} diff --git a/src/common/facets/category.js b/src/common/facets/category.js new file mode 100644 index 000000000..511fca9dc --- /dev/null +++ b/src/common/facets/category.js @@ -0,0 +1,53 @@ +/** + * @import { Facet } from "~/definitions/types.d.ts"; + */ + +/** + * @param {Facet} facet + * @returns {string} + * + * @example Returns accent-twist-4 for prelude kind and accent-twist-2 for others + * ```js + * import { color } from "~/common/facets/category.js"; + * + * const prelude = color({ $type: "sh.diffuse.output.facet", id: "1", name: "x", kind: "prelude" }); + * if (prelude !== "var(--accent-twist-4)") throw new Error("prelude should return accent-twist-4"); + * + * const interactive = color({ $type: "sh.diffuse.output.facet", id: "1", name: "x", kind: "interactive" }); + * if (interactive !== "var(--accent-twist-2)") throw new Error("interactive should return accent-twist-2"); + * + * const undef = color({ $type: "sh.diffuse.output.facet", id: "1", name: "x" }); + * if (undef !== "var(--accent-twist-2)") throw new Error("undefined kind should return accent-twist-2"); + * ``` + */ +export function color(facet) { + switch (facet.kind) { + case "prelude": + return "var(--accent-twist-4)"; + default: + return "var(--accent-twist-2)"; + } +} + +/** + * @param {Facet} facet + * @returns {string} + * + * @example Returns 'feature' for prelude kind and 'interface' for others + * ```js + * import { name } from "~/common/facets/category.js"; + * + * if (name({ $type: "sh.diffuse.output.facet", id: "1", name: "x", kind: "prelude" }) !== "feature") throw new Error("prelude should return 'feature'"); + * if (name({ $type: "sh.diffuse.output.facet", id: "1", name: "x", kind: "interactive" }) !== "interface") throw new Error("interactive should return 'interface'"); + * if (name({ $type: "sh.diffuse.output.facet", id: "1", name: "x" }) !== "interface") throw new Error("undefined kind should return 'interface'"); + * ``` + */ +export function name(facet) { + // return facet.kind ?? "interactive"; + switch (facet.kind) { + case "prelude": + return "feature"; + default: + return "interface"; + } +} diff --git a/src/common/facets/constants.js b/src/common/facets/constants.js new file mode 100644 index 000000000..9519ffe92 --- /dev/null +++ b/src/common/facets/constants.js @@ -0,0 +1,43 @@ +/** + * @example TYPE is the ATProto type string for facets + * ```js + * import { TYPE } from "~/common/facets/constants.js"; + * + * if (TYPE !== "sh.diffuse.output.facet") throw new Error(`expected "sh.diffuse.output.facet", got "${TYPE}"`); + * ``` + */ +export const TYPE = /** @type {const} */ ("sh.diffuse.output.facet"); + +export const INTERACTIVE = [ + "facets/connect/index.html", + "facets/data/sources/index.html", + "facets/data/file-manager/index.html", + "facets/themes/blur/artwork-controller/facet/index.html", + "facets/themes/blur/facet/index.html", + "facets/themes/winamp/facet/index.html", +] + +export const PRELUDE_BASE = [ + "facets/data/metadata-bundle/index.html", + "facets/data/artwork-bundle/index.html", + "facets/data/input-bundle/index.html", + "facets/data/output-bundle/index.html", + "facets/data/upload-bundle/index.html", + "facets/playback/preload/prelude/index.html", +]; + +export const PRELUDE_OTHER = [ + "facets/data/process-tracks/prelude/index.html", + "facets/misc/scrobble/index.html", + "facets/playback/auto-queue/prelude/index.html", +] + +export const STARTING_SET_DISABLED = [ + "facets/misc/scrobble/index.html", +]; + +export const STARTING_SET_URIS = [ + ...PRELUDE_BASE, + ...PRELUDE_OTHER, + ...INTERACTIVE, +]; diff --git a/src/common/facets/prelude.js b/src/common/facets/prelude.js new file mode 100644 index 000000000..2a3e67c19 --- /dev/null +++ b/src/common/facets/prelude.js @@ -0,0 +1,28 @@ +/** + * @import {Facet} from "~/definitions/types.d.ts" + */ + +import { loadURI } from "../loader.js"; + +/** + * @param {Facet[]} facets + * @param {HTMLElement} [container] + */ +export async function insertPreludes(facets, container) { + container ??= document.body; + + const range = document.createRange(); + range.selectNode(container); + + const preludes = facets + .filter((f) => f.kind === "prelude" && f.enabled !== false) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const prelude of preludes) { + const html = prelude.html ?? + (prelude.uri ? await loadURI(prelude.uri) : ""); + if (!html) continue; + const preludeFragment = range.createContextualFragment(html); + container.append(preludeFragment); + } +} diff --git a/src/common/facets/utils.js b/src/common/facets/utils.js new file mode 100644 index 000000000..4952e4156 --- /dev/null +++ b/src/common/facets/utils.js @@ -0,0 +1,129 @@ +import * as TID from "@atcute/tid"; +import { xxh32r } from "xxh32/dist/raw.js"; + +import { loadURI } from "../loader.js"; +import * as CID from "../cid.js"; + +import { STARTING_SET_DISABLED, TYPE } from "./constants.js"; + +import facetsData from "~/_data/facets.json" with { type: "json" }; + +/** + * @import {Facet} from "~/definitions/types.d.ts" + */ + +/** + * Builds facet objects from the bundled catalogue for the given URIs. + * Available offline — no output needs to be reachable. + * + * @param {readonly string[]} uris + * @returns {Facet[]} + * + * @example Builds a facet for each known URI, skipping unknown ones + * ```js + * import { buildFacets } from "~/common/facets/utils.js"; + * import { PRELUDE_BASE } from "~/common/facets/constants.js"; + * + * const facets = buildFacets([...PRELUDE_BASE, "facets/does/not/exist.html"]); + * if (facets.length !== PRELUDE_BASE.length) throw new Error("should skip unknown URIs"); + * if (!facets.every((f) => f.kind === "prelude")) throw new Error("base preludes should be preludes"); + * if (facets.some((f) => !f.id)) throw new Error("every facet needs an id"); + * ``` + */ +export function buildFacets(uris) { + return facetsData.flatMap((facet) => { + if (!uris.includes(facet.url)) return []; + + return [{ + $type: TYPE, + id: uriToRkey("diffuse://" + facet.url), + description: facet.desc, + enabled: STARTING_SET_DISABLED.includes(facet.url) ? false : true, + kind: facet.kind === "prelude" ? "prelude" : "interactive", + name: facet.title, + tags: facet.tags?.length ? facet.tags : undefined, + uri: "diffuse://" + facet.url, + }]; + }); +} + +/** + * @param {{ description?: string; kind: string | undefined; name: string; tags?: string[]; uri: string }} _args + * @param {{ fetchHTML: boolean }} options + * + * @example Creates a facet with correct $type, name, uri, id, and timestamps + * ```js + * import { facetFromURI } from "~/common/facets/utils.js"; + * + * const facet = await facetFromURI({ name: "My Facet", uri: "facets/test/index.html", kind: undefined, description: undefined }, { fetchHTML: false }); + * + * if (facet.$type !== "sh.diffuse.output.facet") throw new Error("$type should be sh.diffuse.output.facet"); + * if (facet.name !== "My Facet") throw new Error("name should be preserved"); + * if (facet.uri !== "facets/test/index.html") throw new Error("uri should be preserved"); + * if (typeof facet.id !== "string" || facet.id.length === 0) throw new Error("id should be a non-empty string"); + * if (!facet.createdAt) throw new Error("createdAt should be set"); + * if (facet.createdAt !== facet.updatedAt) throw new Error("createdAt and updatedAt should match"); + * if (new Date(facet.createdAt).toISOString() !== facet.createdAt) throw new Error("createdAt should be a valid ISO string"); + * ``` + * + * @example fetchHTML false leaves html and cid undefined; kind is validated + * ```js + * import { facetFromURI } from "~/common/facets/utils.js"; + * + * const base = { name: "Test", uri: "test.html", description: undefined }; + * + * const noHtml = await facetFromURI({ ...base, kind: undefined }, { fetchHTML: false }); + * if (noHtml.html !== undefined) throw new Error("html should be undefined when fetchHTML is false"); + * if (noHtml.cid !== undefined) throw new Error("cid should be undefined when fetchHTML is false"); + * + * const prelude = await facetFromURI({ ...base, kind: "prelude" }, { fetchHTML: false }); + * if (prelude.kind !== "prelude") throw new Error("prelude kind should be preserved"); + * + * const interactive = await facetFromURI({ ...base, kind: "interactive" }, { fetchHTML: false }); + * if (interactive.kind !== "interactive") throw new Error("interactive kind should be preserved"); + * + * const unknown = await facetFromURI({ ...base, kind: "unknown" }, { fetchHTML: false }); + * if (unknown.kind !== undefined) throw new Error("unrecognised kind should be set to undefined"); + * ``` + * + * @example Generates unique ids across calls + * ```js + * import { facetFromURI } from "~/common/facets/utils.js"; + * + * const a = await facetFromURI({ name: "A", uri: "a.html", kind: undefined, description: undefined }, { fetchHTML: false }); + * const b = await facetFromURI({ name: "B", uri: "b.html", kind: undefined, description: undefined }, { fetchHTML: false }); + * if (a.id === b.id) throw new Error("ids should be unique across calls"); + * ``` + */ +export async function facetFromURI( + { description, kind, name, tags, uri }, + { fetchHTML }, +) { + const html = fetchHTML ? await loadURI(uri) : undefined; + const cid = html + ? await CID.create(0x55, new TextEncoder().encode(html)) + : undefined; + const timestamp = new Date().toISOString(); + + /** @type {Facet} */ + const facet = { + $type: "sh.diffuse.output.facet", + createdAt: timestamp, + id: TID.now(), + cid, + description, + html, + name, + kind: kind === "interactive" || kind === "prelude" ? kind : undefined, + tags: tags?.length ? tags : undefined, + updatedAt: timestamp, + uri, + }; + + return facet; +} + +/** @param {string} uri */ +function uriToRkey(uri) { + return xxh32r(new TextEncoder().encode(uri)).toString(16).padStart(8, "0"); +} diff --git a/src/common/fast-uri.js b/src/common/fast-uri.js new file mode 100644 index 000000000..cf40b655c --- /dev/null +++ b/src/common/fast-uri.js @@ -0,0 +1,16 @@ +// ESM shim for fast-uri. +// +// Deno's npm:CJS interop puts `module.exports` on the `default` export but +// does not hoist properties to namespace members — so `import * as URI from +// "fast-uri"` gives `URI.parse === undefined` in Deno. In esbuild (browser +// build) the hoisting works, but we use this shim so both runtimes see the +// same named-export interface. +// +// Mapped via `deno.jsonc` → `"fast-uri": "./src/common/fast-uri.js"`. + +import fastUri from "npm:fast-uri@^3.1.0"; + +export const parse = fastUri.parse; +export const serialize = fastUri.serialize; + +export default fastUri; diff --git a/src/common/foundation.js b/src/common/foundation.js new file mode 100644 index 000000000..5c95dd2c5 --- /dev/null +++ b/src/common/foundation.js @@ -0,0 +1,599 @@ +import { signal } from "~/common/signal.js"; + +/** + * @import { DiffuseElement } from "~/common/element.js"; + * @import { Signal } from "~/common/signal.d.ts"; + * @import { ScrobbleElement } from "@specs/components/supplement/types.d.ts"; + */ + +const url = new URL(document.location.href); +export const GROUP = url.searchParams.get("group") ?? "facets"; + +/** + * [PRIVATE] Signals. + */ +const signals = { + configurator: { + artwork: signal( + /** @type {import("~/components/configurator/artwork/element.js").CLASS | null} */ (null), + ), + metadata: signal( + /** @type {import("~/components/configurator/metadata/element.js").CLASS | null} */ (null), + ), + input: signal( + /** @type {import("~/components/configurator/input/element.js").CLASS | null} */ (null), + ), + scrobbles: signal( + /** @type {import("~/components/configurator/scrobbles/element.js").CLASS | null} */ (null), + ), + upload: signal( + /** @type {import("~/components/configurator/upload/element.js").CLASS | null} */ (null), + ), + }, + + engine: { + audio: signal( + /** @type {import("~/components/engine/audio/element.js").CLASS | null} */ (null), + ), + queue: signal( + /** @type {import("~/components/engine/queue/element.js").CLASS | null} */ (null), + ), + repeatShuffle: signal( + /** @type {import("~/components/engine/repeat-shuffle/element.js").CLASS | null} */ (null), + ), + scope: signal( + /** @type {import("~/components/engine/scope/element.js").CLASS | null} */ (null), + ), + }, + + orchestrator: { + artwork: signal( + /** @type {import("~/components/orchestrator/artwork/element.js").CLASS | null} */ (null), + ), + controller: signal( + /** @type {import("~/components/orchestrator/controller/element.js").CLASS | null} */ (null), + ), + coverGroups: signal( + /** @type {import("~/components/orchestrator/cover-groups/element.js").CLASS | null} */ (null), + ), + autoQueue: signal( + /** @type {import("~/components/orchestrator/auto-queue/element.js").CLASS | null} */ (null), + ), + favourites: signal( + /** @type {import("~/components/orchestrator/favourites/element.js").CLASS | null} */ (null), + ), + mediaSession: signal( + /** @type {import("~/components/orchestrator/media-session/element.js").CLASS | null} */ (null), + ), + output: signal( + /** @type {import("~/components/orchestrator/output/element.js").CLASS | null} */ (null), + ), + pathCollections: signal( + /** @type {import("~/components/orchestrator/path-collections/element.js").CLASS | null} */ (null), + ), + processTracks: signal( + /** @type {import("~/components/orchestrator/process-tracks/element.js").CLASS | null} */ (null), + ), + queueAudio: signal( + /** @type {import("~/components/orchestrator/queue-audio/element.js").CLASS | null} */ (null), + ), + scopedTracks: signal( + /** @type {import("~/components/orchestrator/scoped-tracks/element.js").CLASS | null} */ (null), + ), + scrobbleAudio: signal( + /** @type {import("~/components/orchestrator/scrobble-audio/element.js").CLASS | null} */ (null), + ), + sources: signal( + /** @type {import("~/components/orchestrator/sources/element.js").CLASS | null} */ (null), + ), + spectrogramAudio: signal( + /** @type {import("~/components/orchestrator/spectrogram-audio/element.js").CLASS | null} */ (null), + ), + }, +}; + +/** + * Default config for facets. + */ +export const config = { + GROUP, + + // Elements + configurator: { + artwork: configuratorArtwork, + metadata: configuratorMetadata, + input, + scrobbles, + upload, + }, + + engine: { + audio, + queue, + repeatShuffle, + scope, + }, + + orchestrator: { + artwork, + controller, + coverGroups, + autoQueue, + favourites, + mediaSession, + output, + pathCollections, + processTracks, + queueAudio, + scopedTracks, + scrobbleAudio, + sources, + spectrogramAudio, + }, + + /** + * Element signals + */ + signals: { + configurator: { + artwork: signals.configurator.artwork.get, + metadata: signals.configurator.metadata.get, + input: signals.configurator.input.get, + scrobbles: signals.configurator.scrobbles.get, + upload: signals.configurator.upload.get, + }, + + engine: { + audio: signals.engine.audio.get, + queue: signals.engine.queue.get, + repeatShuffle: signals.engine.repeatShuffle.get, + scope: signals.engine.scope.get, + }, + + orchestrator: { + artwork: signals.orchestrator.artwork.get, + controller: signals.orchestrator.controller.get, + coverGroups: signals.orchestrator.coverGroups.get, + autoQueue: signals.orchestrator.autoQueue.get, + favourites: signals.orchestrator.favourites.get, + mediaSession: signals.orchestrator.mediaSession.get, + output: signals.orchestrator.output.get, + pathCollections: signals.orchestrator.pathCollections.get, + processTracks: signals.orchestrator.processTracks.get, + queueAudio: signals.orchestrator.queueAudio.get, + scopedTracks: signals.orchestrator.scopedTracks.get, + scrobbleAudio: signals.orchestrator.scrobbleAudio.get, + sources: signals.orchestrator.sources.get, + spectrogramAudio: signals.orchestrator.spectrogramAudio.get, + }, + }, + + // Utilities + + container: () => { + return document.body.querySelector("#container"); + }, + + hideLoader: () => { + const loader = document.querySelector("#diffuse-loader"); + + if (loader) { + loader.classList.add("loaded"); + setTimeout(() => { + loader.remove(); + loader.parentElement?.remove(); + }, 750); + } + }, + + /** + * @param {{ title: string }} options + */ + setup: ({ title }) => { + document.title = title; + }, + + /** + * Hide the loader and fade in the facet content by adding the `has-loaded` + * class to `#container` after two animation frames (so the initial opacity:0 + * is painted first and the CSS transition actually runs). + */ + ready: () => { + config.hideLoader(); + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + document.querySelector("#container")?.classList.add("has-loaded"); + }); + }); + }, +}; + +export default config; + +// 🥡 + +// Configurators + +async function configuratorArtwork() { + const { CLASS: ArtworkConfigurator } = await import( + "~/components/configurator/artwork/element.js" + ); + + const ac = new ArtworkConfigurator(); + ac.setAttribute("group", GROUP); + + return findExistingOrAdd(ac, signals.configurator.artwork); +} + +async function configuratorMetadata() { + const { CLASS: MetadataConfigurator } = await import( + "~/components/configurator/metadata/element.js" + ); + + const mc = new MetadataConfigurator(); + mc.setAttribute("group", GROUP); + + return findExistingOrAdd(mc, signals.configurator.metadata); +} + +async function input() { + const { CLASS: InputConfigurator } = await import( + "~/components/configurator/input/element.js" + ); + + const i = new InputConfigurator(); + i.setAttribute("group", GROUP); + + return findExistingOrAdd(i, signals.configurator.input); +} + +async function upload() { + const { CLASS: UploadConfigurator } = await import( + "~/components/configurator/upload/element.js" + ); + + const u = new UploadConfigurator(); + u.setAttribute("group", GROUP); + + return findExistingOrAdd(u, signals.configurator.upload); +} + +/** + * @returns {Promise} + */ +async function scrobbles() { + const { CLASS: ScrobblesConfigurator } = await import( + "~/components/configurator/scrobbles/element.js" + ); + + const sc = new ScrobblesConfigurator(); + sc.setAttribute("group", GROUP); + + const existing = document.body.querySelector(sc.selector); + + if (existing) { + return /** @type {ScrobbleElement} */ (existing); + } + + document.body.append(sc); + return /** @type {ScrobbleElement} */ (/** @type {unknown} */ (sc)); +} + +// Engines +async function audio() { + const { CLASS: AudioEngine } = await import( + "~/components/engine/audio/element.js" + ); + + const a = new AudioEngine(); + a.setAttribute("group", GROUP); + + return findExistingOrAdd(a, signals.engine.audio); +} + +async function queue() { + const { CLASS: Queue } = await import( + "~/components/engine/queue/element.js" + ); + + const q = new Queue(); + q.setAttribute("group", GROUP); + + return findExistingOrAdd(q, signals.engine.queue); +} + +async function repeatShuffle() { + const { CLASS: RepeatShuffleEngine } = await import( + "~/components/engine/repeat-shuffle/element.js" + ); + + const r = new RepeatShuffleEngine(); + r.setAttribute("group", GROUP); + + return findExistingOrAdd(r, signals.engine.repeatShuffle); +} + +async function scope() { + const { CLASS: ScopeEngine } = await import( + "~/components/engine/scope/element.js" + ); + + const s = new ScopeEngine(); + s.setAttribute("group", GROUP); + + return findExistingOrAdd(s, signals.engine.scope); +} + +// Orchestrators + +async function artwork() { + const [{ CLASS: ArtworkOrchestrator }, ac] = await Promise.all([ + import("~/components/orchestrator/artwork/element.js"), + configuratorArtwork(), + ]); + + const a = new ArtworkOrchestrator(); + a.setAttribute("group", GROUP); + a.setAttribute("artwork-selector", ac.selector); + + return findExistingOrAdd(a, signals.orchestrator.artwork); +} + +async function autoQueue() { + const [{ CLASS: AutoQueueOrchestrator }, q, r, t] = await Promise.all([ + import("~/components/orchestrator/auto-queue/element.js"), + queue(), + repeatShuffle(), + scopedTracks(), + ]); + + const aqo = new AutoQueueOrchestrator(); + aqo.setAttribute("group", GROUP); + aqo.setAttribute("queue-engine-selector", q.selector); + aqo.setAttribute("repeat-shuffle-engine-selector", r.selector); + aqo.setAttribute("tracks-selector", t.selector); + + return findExistingOrAdd(aqo, signals.orchestrator.autoQueue); +} + +async function controller() { + const [{ CLASS: ControllerOrchestrator }, a, o, q] = await Promise.all([ + import("~/components/orchestrator/controller/element.js"), + audio(), + output(), + queue(), + ]); + + const co = new ControllerOrchestrator(); + co.setAttribute("audio-engine-selector", a.selector); + co.setAttribute("output-selector", o.selector); + co.setAttribute("queue-engine-selector", q.selector); + + return findExistingOrAdd(co, signals.orchestrator.controller); +} + +async function coverGroups() { + const [{ CLASS: CoverGroupsOrchestrator }, t] = await Promise.all([ + import("~/components/orchestrator/cover-groups/element.js"), + scopedTracks(), + ]); + + const cgo = new CoverGroupsOrchestrator(); + cgo.setAttribute("tracks-selector", t.selector); + + return findExistingOrAdd(cgo, signals.orchestrator.coverGroups); +} + +async function favourites() { + const [{ CLASS: FavouritesOrchestrator }, o] = await Promise.all([ + import("~/components/orchestrator/favourites/element.js"), + output(), + ]); + + const fo = new FavouritesOrchestrator(); + fo.setAttribute("group", GROUP); + fo.setAttribute("output-selector", o.selector); + + return findExistingOrAdd(fo, signals.orchestrator.favourites); +} + +async function mediaSession() { + const [{ CLASS: MediaSessionOrchestrator }, a, aw, o, q] = await Promise + .all([ + import("~/components/orchestrator/media-session/element.js"), + audio(), + artwork(), + output(), + queue(), + ]); + + const mso = new MediaSessionOrchestrator(); + mso.setAttribute("group", GROUP); + mso.setAttribute("audio-engine-selector", a.selector); + mso.setAttribute("artwork-selector", aw.selector); + mso.setAttribute("output-selector", o.selector); + mso.setAttribute("queue-engine-selector", q.selector); + + return findExistingOrAdd(mso, signals.orchestrator.mediaSession); +} + +/** + * @param {Object} [options] - Options + * @param {string} [options.namespace] - The namespace to use for the output. + */ +async function output(options) { + const { CLASS: OutputOrchestrator } = await import( + "~/components/orchestrator/output/element.js" + ); + + const o = new OutputOrchestrator(); + o.setAttribute("group", GROUP); + + if (options?.namespace) o.setAttribute("namespace", options.namespace); + + return findExistingOrAdd(o, signals.orchestrator.output); +} + +/** + * @param {Object} opts - Options + * @param {boolean} [opts.disableWhenReady] - Whether to disable processing when ready. + */ +async function processTracks(opts = { disableWhenReady: false }) { + const [{ CLASS: ProcessTracksOrchestrator }, i, o, m] = await Promise.all([ + import("~/components/orchestrator/process-tracks/element.js"), + input(), + output(), + configuratorMetadata(), + ]); + + const opt = new ProcessTracksOrchestrator(); + opt.setAttribute("group", GROUP); + opt.setAttribute("input-selector", i.selector); + opt.setAttribute("output-selector", o.selector); + opt.setAttribute("metadata-selector", m.selector); + + if (!opts.disableWhenReady) { + opt.toggleAttribute("process-when-ready"); + } + + return findExistingOrAdd(opt, signals.orchestrator.processTracks); +} + +async function queueAudio() { + const [{ CLASS: QueueAudioOrchestrator }, a, i, o, q, r] = await Promise + .all([ + import("~/components/orchestrator/queue-audio/element.js"), + audio(), + input(), + output(), + queue(), + repeatShuffle(), + ]); + + const oqa = new QueueAudioOrchestrator(); + oqa.setAttribute("group", GROUP); + oqa.setAttribute("audio-engine-selector", a.selector); + oqa.setAttribute("input-selector", i.selector); + oqa.setAttribute("output-selector", o.selector); + oqa.setAttribute("queue-engine-selector", q.selector); + oqa.setAttribute("repeat-shuffle-engine-selector", r.selector); + + return findExistingOrAdd(oqa, signals.orchestrator.queueAudio); +} + +async function scopedTracks() { + const [{ CLASS: ScopedTracksOrchestrator }, i, o, e] = await Promise.all([ + import("~/components/orchestrator/scoped-tracks/element.js"), + input(), + output(), + scope(), + ]); + + const sto = new ScopedTracksOrchestrator(); + sto.setAttribute("group", GROUP); + sto.setAttribute("input-selector", i.selector); + sto.setAttribute("output-selector", o.selector); + sto.setAttribute("scope-engine-selector", e.selector); + + return findExistingOrAdd(sto, signals.orchestrator.scopedTracks); +} + +async function scrobbleAudio() { + const [{ CLASS: ScrobbleAudioOrchestrator }, a, sc] = await Promise.all([ + import("~/components/orchestrator/scrobble-audio/element.js"), + audio(), + scrobbles(), + ]); + + const sao = new ScrobbleAudioOrchestrator(); + sao.setAttribute("group", GROUP); + sao.setAttribute("audio-engine-selector", a.selector); + sao.setAttribute("scrobble-selector", sc.selector); + + return findExistingOrAdd(sao, signals.orchestrator.scrobbleAudio); +} + +async function spectrogramAudio() { + const [ + { CLASS: SpectrogramAudioOrchestrator }, + { CLASS: SpectrogramMetadata }, + a, + i, + o, + ] = await Promise.all([ + import("~/components/orchestrator/spectrogram-audio/element.js"), + import("~/components/metadata/spectrogram/element.js"), + audio(), + input(), + output(), + ]); + + // A standalone spectrogram metadata element the orchestrator can call + // directly. It's given the input configurator so its worker can fetch audio. + const sm = new SpectrogramMetadata(); + sm.setAttribute("group", GROUP); + sm.setAttribute("input-selector", i.selector); + + if (!document.body.querySelector(sm.selector)) { + document.body.append(sm); + } + + const sao = new SpectrogramAudioOrchestrator(); + sao.setAttribute("group", GROUP); + sao.setAttribute("audio-engine-selector", a.selector); + sao.setAttribute("spectrogram-selector", sm.selector); + sao.setAttribute("output-selector", o.selector); + + return findExistingOrAdd(sao, signals.orchestrator.spectrogramAudio); +} + +async function pathCollections() { + const [{ CLASS: PathCollectionsOrchestrator }, o] = await Promise.all([ + import("~/components/orchestrator/path-collections/element.js"), + output(), + ]); + + const pco = new PathCollectionsOrchestrator(); + pco.setAttribute("group", GROUP); + pco.setAttribute("output-selector", o.selector); + + return findExistingOrAdd(pco, signals.orchestrator.pathCollections); +} + +async function sources() { + const [{ CLASS: SourcesOrchestrator }, i, o] = await Promise.all([ + import("~/components/orchestrator/sources/element.js"), + input(), + output(), + ]); + + const so = new SourcesOrchestrator(); + so.setAttribute("group", GROUP); + so.setAttribute("input-selector", i.selector); + so.setAttribute("output-selector", o.selector); + + return findExistingOrAdd(so, signals.orchestrator.sources); +} + +// 🛠️ + +/** + * @template {DiffuseElement} T + * @param {T} element + * @param {Signal} signal + * @returns {T} + */ +export function findExistingOrAdd(element, signal) { + /** @type {T | null} */ + const alreadyAdded = document.body.querySelector(element.selector); + + if (!alreadyAdded) { + document.body.append(element); + signal.value = element; + return element; + } + + signal.value = alreadyAdded; + return alreadyAdded; +} diff --git a/src/common/loader.js b/src/common/loader.js new file mode 100644 index 000000000..69d8f7ce7 --- /dev/null +++ b/src/common/loader.js @@ -0,0 +1,252 @@ +import * as URI from "fast-uri"; +import * as TID from "@atcute/tid"; +import { Client, ok, simpleFetchHandler } from "@atcute/client"; +import { + CompositeDidDocumentResolver, + LocalActorResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + XrpcHandleResolver, +} from "@atcute/identity-resolver"; + +import * as CID from "~/common/cid.js"; +import { effect } from "~/common/signal.js"; + +// When the service worker takes control (clients.claim()), the page is about +// to reload (see default-layout.js sw-activated handler). Any fetch() calls +// in flight at that moment will be cancelled by the navigation and throw a +// NetworkError. We detect the controller change here so we can suppress those +// spurious errors rather than flashing an error UI before the reload. +let swControllerChanging = false; + +if ("serviceWorker" in navigator) { + navigator.serviceWorker.addEventListener("controllerchange", () => { + swControllerChanging = true; + }); +} + +/** + * @import {SignalReader} from "~/common/signal.d.ts" + */ + +/** + * @typedef {{ html?: string; uri?: string; cid?: string; id: string; name: string; $type: string }} LoadableItem + */ + +/** + * @typedef {object} LoaderConfig + * @property {string} $type - The atproto $type + * @property {string} label - Human-readable label for error messages (e.g. "Facet", "Theme") + * @property {() => { collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: LoadableItem[] } | { state: "error" }> }} source - The collection source + * @property {(item: LoadableItem) => void} render - Renders the loaded item + */ + +/** + * Sets up the full loader effect: reads URL params, resolves the item + * from the collection or creates a temporary one, ensures HTML is loaded, + * and calls the render callback. + * + * @param {LoaderConfig} config + */ +export function createLoader(config) { + const docUrl = new URL(document.location.href); + + const id = docUrl.searchParams.get("id"); + const cid = docUrl.searchParams.get("cid"); + const name = docUrl.searchParams.get("name"); + const uri = docUrl.searchParams.get("uri"); + const path = docUrl.searchParams.get("path"); + + const containerNull = document.querySelector("#container"); + if (!containerNull) throw new Error("Container not found"); + + const container = /** @type {HTMLDivElement} */ (containerNull); + + /** @type {string | null} */ + let loadedCid = null; + + /** @type {string | null} */ + let loader = null; + + effect(() => { + /** @type {LoadableItem | undefined} */ + let item = undefined; + + if (path) { + item = { + $type: config.$type, + id: TID.now(), + name: "temporary", + uri: `diffuse://${path}`, + }; + + loader = "path"; + } else if (uri) { + item = { + $type: config.$type, + id: TID.now(), + name: "temporary", + uri, + }; + + loader = "uri"; + } else { + const source = config.source(); + const col = source.collection(); + if (col.state === "error") { + return renderError(container, `Failed to load ${config.label.toLowerCase()}`); + } + if (col.state !== "loaded") return; + const collection = col.data; + + if (id) { + item = collection.find((c) => c.id === id); + loader = "id"; + } else if (cid) { + item = collection.find((c) => c.cid === cid); + loader = "cid"; + } else if (name) { + item = collection.find((c) => c.name === name); + loader = "name"; + } + } + + if (!loader) { + return renderError(container, "No loader specified"); + } else if (!item) { + return renderError(container, `${config.label} not found`); + } + + // Make sure HTML is loaded when a URI is specified + ensureHTML(item).catch((err) => { + if (swControllerChanging) return; + renderError(container, `Failed to load URI: ${item.uri}`, { + context: err, + throw: true, + }); + }).then(() => { + if (item.cid === loadedCid) return; + loadedCid = item.cid ?? null; + config.render(item); + }) + }); +} + +/** + * @param {string} uri + */ +export async function loadURI(uri) { + const u = URI.parse(uri); + + switch (u.scheme) { + case "at": + return atprotoLoader(uri); + case "diffuse": + return httpLoader(uri.replace(/^diffuse:\/\//, "")); + case "http": + case "https": + return httpLoader(uri); + default: + throw new Error(`Unsupported scheme: ${u.scheme}`); + } +} + +/** + * Ensures the item has HTML loaded. If it has a URI but no HTML, + * fetches the HTML and computes the CID. + * + * @template {{ html?: string; uri?: string; cid?: string }} T + * @param {T} item + * @returns {Promise} + */ +export async function ensureHTML(item) { + if (!item.html && item.uri) { + const html = await loadURI(item.uri); + const cid = await CID.create(0x55, new TextEncoder().encode(html)); + + item.html = html; + item.cid = cid; + } + + return item; +} + +/** + * @param {HTMLElement} container + * @param {string} error + * @param {{ context?: Error; throw?: boolean }} [options] + */ +export function renderError(container, error, options) { + document.querySelector("#diffuse-loader")?.classList.add("loaded"); + container.classList.add("has-loaded"); + container.innerHTML = ` + + `; + + if (options?.throw) { + throw options.context ?? new Error(error); + } +} + +//////////////////////////////////////////// +// 🛠️ | LOADERS +//////////////////////////////////////////// + +/** + * @param {string} uri + * @returns {Promise} + */ +async function atprotoLoader(uri) { + const parts = uri.replace(/at:\/\//, "").split("/"); + const [repo, collection, rkey] = parts; + + const resolver = new LocalActorResolver({ + handleResolver: new XrpcHandleResolver({ + serviceUrl: "https://public.api.bsky.app", + }), + didDocumentResolver: new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver(), + }, + }), + }); + + const identity = await resolver.resolve( + /** @type {import("@atcute/lexicons/syntax").ActorIdentifier} */ (repo), + ); + + const rpc = new Client({ + handler: simpleFetchHandler({ service: identity.pds }), + }); + + /** @type {any} */ + const { value } = await ok( + /** @type {any} */ (rpc).get("com.atproto.repo.getRecord", { + params: { repo: identity.did, collection, rkey }, + }), + ); + + if (value.html) { + return value.html; + } + + if (value.uri) { + return loadURI(value.uri); + } + + return ""; +} + +/** + * @param {string} url + * @returns {Promise} + */ +async function httpLoader(url) { + return fetch(url).then((res) => res.text()); +} diff --git a/src/common/output.js b/src/common/output.js new file mode 100644 index 000000000..6880649a3 --- /dev/null +++ b/src/common/output.js @@ -0,0 +1,138 @@ +import { effect } from "~/common/signal.js"; + +/** + * @import {SignalReader} from "~/common/signal.d.ts"; + */ + +/** + * Merges two arrays by `id`. Items from `incoming` replace any matching + * items in `existing`; unmatched existing items are preserved. + * + * @template {{ id: string }} T + * @param {T[]} existing + * @param {T[]} incoming + * @returns {T[]} + * + * @example Returns incoming items when existing is empty + * ```js + * import { mergeById } from "~/common/output.js"; + * + * const result = mergeById([], [{ id: "a" }, { id: "b" }]); + * if (result.map(t => t.id).join(",") !== "a,b") throw new Error("unexpected result"); + * ``` + * + * @example Returns existing items when incoming is empty + * ```js + * import { mergeById } from "~/common/output.js"; + * + * const result = mergeById([{ id: "a" }, { id: "b" }], []); + * if (result.map(t => t.id).join(",") !== "a,b") throw new Error("unexpected result"); + * ``` + * + * @example Preserves items not present in incoming + * ```js + * import { mergeById } from "~/common/output.js"; + * + * const result = mergeById([{ id: "a" }, { id: "b" }], [{ id: "c" }]); + * if (result.map(t => t.id).join(",") !== "a,b,c") throw new Error("unexpected result"); + * ``` + * + * @example Replaces existing item with incoming version when ids match + * ```js + * import { mergeById } from "~/common/output.js"; + * + * const result = mergeById([{ id: "a", uri: "old://a" }], [{ id: "a", uri: "new://a" }]); + * if (result.length !== 1) throw new Error("expected length 1"); + * if (result[0].uri !== "new://a") throw new Error("expected new uri"); + * ``` + * + * @example Preserves other-source items when incoming covers one source + * ```js + * import { mergeById } from "~/common/output.js"; + * + * const existing = [ + * { id: "s3-1", uri: "s3://bucket/a.flac" }, + * { id: "s3-2", uri: "s3://bucket/b.flac" }, + * { id: "wd-1", uri: "webdav://server/c.flac" }, + * ]; + * const incoming = [ + * { id: "s3-1", uri: "s3://bucket/a.flac" }, + * { id: "s3-3", uri: "s3://bucket/d.flac" }, + * ]; + * const result = mergeById(existing, incoming); + * const sorted = result.map(t => t.id).sort().join(","); + * if (sorted !== "s3-1,s3-2,s3-3,wd-1") throw new Error("unexpected result: " + sorted); + * ``` + * + * @example Incoming items appear after preserved existing items + * ```js + * import { mergeById } from "~/common/output.js"; + * + * const result = mergeById([{ id: "x" }], [{ id: "y" }, { id: "z" }]); + * if (result.map(t => t.id).join(",") !== "x,y,z") throw new Error("unexpected result"); + * ``` + * + * @example Handles both arrays empty + * ```ts + * import { mergeById } from "~/common/output.js"; + * import type { Track } from "~/definitions/types.d.ts"; + * + * const result = mergeById([], []); + * if (result.length !== 0) throw new Error("expected empty result"); + * ``` + */ +export function mergeById(existing, incoming) { + const ids = new Set(incoming.map((t) => t.id)); + const preserved = existing.filter((t) => !ids.has(t.id)); + return [...preserved, ...incoming]; +} + +/** + * @template T + * @param {{ collection: SignalReader<{ state: "loading" } | { state: "loaded"; data: T } | { state: "error" }> }} output + * @returns {Promise} + * + * @example Resolves immediately when collection is already loaded + * ```js + * import { data } from "~/common/output.js"; + * import { signal } from "~/common/signal.js"; + * + * const col = signal(JSON.parse('{"state":"loaded","data":["a","b"]}')); + * const result = await data({ collection: col.get }); + * if (result.join(",") !== "a,b") throw new Error("expected ['a', 'b']"); + * ``` + * + * @example Waits for collection to transition to loaded + * ```js + * import { data } from "~/common/output.js"; + * import { signal } from "~/common/signal.js"; + * + * const col = signal(JSON.parse('{"state":"loading"}')); + * const promise = data({ collection: col.get }); + * + * await Promise.resolve(); + * col.set({ state: "loaded", data: [1, 2, 3] }); + * + * const result = await promise; + * if (result.join(",") !== "1,2,3") throw new Error("expected [1, 2, 3]"); + * ``` + */ +export async function data(output) { + return await new Promise((resolve) => { + let resolved = false; + + const stop = effect(() => { + if (resolved) { + stop(); + return; + } + + const col = output.collection(); + + if (col.state === "loaded") { + resolved = true; + resolve(col.data); + } + }); + }); +} diff --git a/src/common/pages/code.js b/src/common/pages/code.js new file mode 100644 index 000000000..57bed300d --- /dev/null +++ b/src/common/pages/code.js @@ -0,0 +1,318 @@ +import { basicSetup, EditorView } from "codemirror"; +import { css as langCss } from "@codemirror/lang-css"; +import { html as langHtml } from "@codemirror/lang-html"; +import { javascript as langJs } from "@codemirror/lang-javascript"; +import { autocompletion } from "@codemirror/autocomplete"; + +import * as TID from "@atcute/tid"; + +import * as CID from "~/common/cid.js"; +import * as Output from "~/common/output.js"; +import { facetFromURI } from "~/common/facets/utils.js"; +import { loadURI } from "~/common/loader.js"; +import { signal } from "~/common/signal.js"; + +import { saveFacet } from "./crud.js"; +import { output } from "./output.js"; + +/** + * @import {Facet} from "~/definitions/types.d.ts" + */ + +const $editor = signal(/** @type {EditorView | null} */ (null)); +const $editingFacet = signal(/** @type {Facet | null} */ (null)); + +//////////////////////////////////////////// +// LOADING +//////////////////////////////////////////// + +const LOADING_EL_ID = "editor-loading"; + +/** + * @param {boolean} loading + */ +function setEditorLoading(loading) { + const container = /** @type {HTMLElement | null} */ ( + document.querySelector("#html-input-container") + ); + if (!container) return; + + if (loading) { + if (document.getElementById(LOADING_EL_ID)) return; + const el = document.createElement("div"); + el.id = LOADING_EL_ID; + el.className = "with-icon"; + el.style.fontSize = "var(--fs-sm)"; + el.innerHTML = ' Loading…'; + container.before(el); + container.hidden = true; + } else { + document.getElementById(LOADING_EL_ID)?.remove(); + container.hidden = false; + } +} + +//////////////////////////////////////////// +// EDITOR +//////////////////////////////////////////// + +export function renderEditor() { + // Code editor + const editorContainer = document.body.querySelector("#html-input-container"); + if (!editorContainer) throw new Error("Editor container not found"); + + const editor = new EditorView({ + parent: editorContainer, + doc: ` + + + + `.trim(), + extensions: [ + basicSetup, + langHtml(), + langCss(), + langJs(), + autocompletion(), + ], + }); + + $editor.value = editor; + return editor; +} + +//////////////////////////////////////////// +// FORM +//////////////////////////////////////////// + +/** + * @param {EditorView} editor + */ +const onBuildSubmit = (editor) => +/** + * @param {Event} event + */ +async (event) => { + event.preventDefault(); + + const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( + "#name-input", + )); + + const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( + document.querySelector("#description-input") + ); + + const kindEl = /** @type {HTMLSelectElement | null} */ ( + document.querySelector("#kind-input") + ); + + const html = editor.state.doc.toString(); + const cid = await CID.create(0x55, new TextEncoder().encode(html)); + const name = nameEl?.value ?? "nameless"; + const description = descriptionEl?.value ?? ""; + const kind = + /** @type {"interactive" | "prelude"} */ (kindEl?.value ?? "interactive"); + + /** @type {Facet} */ + const facet = $editingFacet.value + ? { + ...$editingFacet.value, + cid, + description, + html, + kind, + name, + } + : { + $type: "sh.diffuse.output.facet", + id: TID.now(), + cid, + description, + html, + kind, + name, + }; + + $editingFacet.value = facet; + + switch (/** @type {any} */ (event).submitter.name) { + case "save": + await saveFacet(facet); + break; + case "save+open": + await saveFacet(facet); + globalThis.open(`./l/?id=${facet.id}`, "blank"); + break; + } +}; + +/** + * @param {Facet} ogFacet + */ +async function editFacet(ogFacet) { + const facet = { ...ogFacet }; + const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( + "#name-input", + )); + + const descriptionEl = /** @type {HTMLTextAreaElement | null} */ ( + document.querySelector("#description-input") + ); + + const kindEl = /** @type {HTMLSelectElement | null} */ ( + document.querySelector("#kind-input") + ); + + if (!nameEl) return; + + // Reset url — remove `id` param if not matching the facet + const url = new URL(location.href); + const id = url.searchParams.get("id"); + + if (id && facet.id !== id) { + url.searchParams.delete("id"); + history.replaceState(null, "", url); + } + + // Scroll to builder + document.querySelector("#code")?.scrollIntoView(); + + // Make sure HTML is loaded + if (!facet.html && facet.uri) { + setEditorLoading(true); + const html = await loadURI(facet.uri); + const cid = await CID.create(0x55, new TextEncoder().encode(html)); + setEditorLoading(false); + + facet.html = html; + facet.cid = cid; + } + + $editingFacet.value = facet; + nameEl.value = facet.name; + + if (kindEl) { + kindEl.value = facet.kind ?? "interactive"; + } + + if (descriptionEl) { + descriptionEl.value = facet.description ?? ""; + } + + const editor = $editor.value; + editor?.dispatch({ + changes: { from: 0, to: editor.state.doc.length, insert: facet.html }, + }); +} + +export function handleBuildFormSubmit() { + const editor = $editor.value; + if (!editor) return; + + document.querySelector("#code-form")?.addEventListener( + "submit", + onBuildSubmit(editor), + ); + + const importBtn = document.querySelector("#import-button"); + const importInput = document.querySelector("#import-input"); + + importBtn?.addEventListener("click", () => /** @type {HTMLElement} */ (importInput)?.click()); + + importInput?.addEventListener("change", async (event) => { + const file = /** @type {HTMLInputElement} */ (event.target).files?.[0]; + if (!file) return; + + const html = await file.text(); + const cid = await CID.create(0x55, new TextEncoder().encode(html)); + + editor.dispatch({ + changes: { from: 0, to: editor.state.doc.length, insert: html }, + }); + }); +} + +//////////////////////////////////////////// +// EDIT EXAMPLES +//////////////////////////////////////////// + +let isListening = false; + +export function listenForExamplesEdit() { + if (isListening) return; + isListening = true; + + document.body.addEventListener( + "click", + /** + * @param {MouseEvent} event + */ + async (event) => { + const target = /** @type {HTMLElement} */ (event.target); + const rel = target.getAttribute("rel"); + if (!rel) return; + + const uri = target.closest("li")?.getAttribute("data-uri"); + if (!uri) return; + + const name = target.closest("li")?.getAttribute("data-name"); + if (!name) return; + + const kind = target.closest("li")?.getAttribute("data-kind") ?? undefined; + + switch (rel) { + case "edit": { + setEditorLoading(true); + const facet = await facetFromURI({ kind, name, uri }, { + fetchHTML: true, + }); + setEditorLoading(false); + editFacet(facet); + document.querySelector("#code")?.scrollIntoView(); + break; + } + } + }, + ); +} + +//////////////////////////////////////////// +// EDIT FACET FROM URL +//////////////////////////////////////////// + +export async function editFacetFromURL() { + const params = new URLSearchParams(location.search); + const idParam = params.get("id"); + const uriParam = params.get("uri"); + + // Reset editing state; if we're loading a specific facet, editFacet() will + // set it again. Without this, $editingFacet persists across SPA navigations + // and a subsequent "save" will reuse the stale id, silently overwriting the + // previously edited facet instead of creating a new entry. + $editingFacet.value = null; + + setEditorLoading(true); + try { + if (idParam) { + const out = await output(); + const col = await Output.data(out.facets); + const facet = col.find((f) => f.id === idParam); + if (facet) await editFacet(facet); + } else if (uriParam) { + const facet = await facetFromURI({ + uri: uriParam, + name: params.get("name") ?? "", + kind: /** @type {any} */ (params.get("kind") ?? undefined), + description: params.get("description") ?? undefined, + }, { fetchHTML: true }); + await editFacet(facet); + } + } finally { + setEditorLoading(false); + } +} diff --git a/src/common/pages/crud.js b/src/common/pages/crud.js new file mode 100644 index 000000000..21c1db7c5 --- /dev/null +++ b/src/common/pages/crud.js @@ -0,0 +1,66 @@ +import * as Output from "~/common/output.js"; +import { output } from "./output.js"; + +/** + * @import {Facet} from "~/definitions/types.d.ts" + */ + +/** + * @param {{ id: string }} _ + */ +export function deleteFacet({ id }) { + return async () => { + const c = confirm("Are you sure you want to delete this facet?"); + if (!c) return; + + const out = await output(); + const col = await Output.data(out.facets); + + out.facets.save(col.filter((c) => !(c.id === id))); + }; +} + +/** + * @param {{ id: string }} _ + */ +export function toggleFacetEnabled({ id }) { + return async () => { + const out = await output(); + const col = await Output.data(out.facets); + const facet = col.find((c) => c.id === id); + if (!facet) return; + await out.facets.save([ + ...col.filter((c) => c.id !== id), + { ...facet, enabled: !(facet.enabled ?? true), updatedAt: new Date().toISOString() }, + ]); + }; +} + +/** + * @param {{ id: string }} _ + */ +export function toggleFacetFavourite({ id }) { + return async () => { + const out = await output(); + const col = await Output.data(out.facets); + const facet = col.find((c) => c.id === id); + if (!facet) return; + await out.facets.save([ + ...col.filter((c) => c.id !== id), + { ...facet, favourite: !(facet.favourite ?? false), updatedAt: new Date().toISOString() }, + ]); + }; +} + +/** + * @param {Facet} facet + */ +export async function saveFacet(facet) { + const out = await output(); + const col = await Output.data(out.facets); + const colWithoutId = col.filter((c) => c.id !== facet.id); + await out.facets.save([...colWithoutId, { + ...facet, + updatedAt: new Date().toISOString(), + }]); +} diff --git a/src/common/pages/dashboard.js b/src/common/pages/dashboard.js new file mode 100644 index 000000000..466941b9c --- /dev/null +++ b/src/common/pages/dashboard.js @@ -0,0 +1,383 @@ +import { html, render } from "lit-html"; +import { classMap } from "lit-html/directives/class-map.js"; +import { keyed } from "lit-html/directives/keyed.js"; +import { marked } from "marked"; +import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; + +import * as FacetCategory from "~/common/facets/category.js"; +import { batch, effect, signal } from "~/common/signal.js"; + +import { nothing } from "~/common/element.js"; + +import { deleteFacet, toggleFacetEnabled, toggleFacetFavourite } from "./crud.js"; +import { output } from "./output.js"; +import { openAddFromURIModal } from "./from-uri.js"; + +// Signals +const FILTER_STORAGE_KEY = "diffuse/dashboard/filter"; +const storedFilter = localStorage.getItem(FILTER_STORAGE_KEY); +const activeFilter = signal( + storedFilter === "prelude" || storedFilter === "interface" || + storedFilter === "base" + ? storedFilter + : "all", +); + +effect(() => { + localStorage.setItem(FILTER_STORAGE_KEY, activeFilter.get()); +}); + +const UNFAVOURITE_COLLAPSED_KEY = "diffuse/dashboard/unfavourite-collapsed"; +const unfavouriteCollapsed = signal( + localStorage.getItem(UNFAVOURITE_COLLAPSED_KEY) === "true", +); + +effect(() => { + localStorage.setItem( + UNFAVOURITE_COLLAPSED_KEY, + String(unfavouriteCollapsed.get()), + ); +}); + +/** @param {string} filter */ +function setFilter(filter) { + const url = new URL(location.href); + url.searchParams.delete("filter"); + history.replaceState(null, "", url); + batch(() => { + activeFilter.set(filter); + unfavouriteCollapsed.set(false); + }); +} + +/** + * @import OutputOrchestrator from "~/components/orchestrator/output/element.js"; + * @import {Facet} from "~/definitions/types.d.ts"; + */ + +const emptyFacetsList = () => + html` +

    + + You haven't saved anything yet. Add a facet by browsing the featured ones or any of the other categories. You can click the toggle + to quickly add or remove from your collection. + +

    + `; + +//////////////////////////////////////////// +// LIST +//////////////////////////////////////////// + +/** @type {() => void | undefined} */ +let stopMonitor; + +/** */ +export async function renderList() { + if (stopMonitor) stopMonitor(); + activeFilter.set((() => { + const param = new URL(location.href).searchParams.get("filter"); + if ( + param === "prelude" || param === "interface" || param === "base" || + param === "all" + ) return param; + const stored = localStorage.getItem(FILTER_STORAGE_KEY); + return stored === "prelude" || stored === "interface" || stored === "base" + ? stored + : "all"; + })()); + + /** @type {HTMLElement | null} */ + const listEl = document.querySelector("#list"); + if (!listEl) throw new Error("List element not found"); + + if (listEl.getAttribute("data-rendered") === "f") { + listEl.innerHTML = ""; + listEl.removeAttribute("data-rendered"); + } + + const out = await output(); + + stopMonitor = effect(() => { + _renderList(out, listEl); + }); +} + +/** + * @param {OutputOrchestrator} output + * @param {HTMLElement} listEl + */ +function _renderList(output, listEl) { + const facetsCol = output.facets.collection(); + + if (facetsCol.state !== "loaded") { + const loading = html` +
    + + Loading your software +
    + `; + + render(loading, listEl); + return; + } + + const filter = activeFilter.get(); + + /** @param {Facet} a @param {Facet} b */ + const sortByName = (a, b) => + a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()) || + a.id.localeCompare(b.id); + + const filtered = facetsCol.state === "loaded" + ? [...facetsCol.data].filter((c) => + filter === "base" ? !!c.tags?.includes("base") : (filter === "all" || + (filter === "prelude" ? c.kind === "prelude" : c.kind !== "prelude")) && + !c.tags?.includes("base") + ) + : []; + + const favouriteCol = filtered.filter((c) => c.favourite).sort(sortByName); + const unfavouriteCol = filtered.filter((c) => !c.favourite).sort(sortByName); + const col = favouriteCol.concat(unfavouriteCol); + + const selected = output.selected(); + const outputLabel = selected?.label ?? selected?.getAttribute?.("label") ?? + "Local storage"; + + const filterBar = html` +
    + Filter by + + + + + + + + + + +
    + + Userdata from + ${outputLabel} +
    + `; + + /** @param {Facet[]} items */ + const renderItems = (items) => + items.map((c) => { + const color = FacetCategory.color(c); + + const title = c.kind === "prelude" + ? html` + + ${c.name} + + ` + : html` + + ${c.name} + + `; + + return keyed( + c.id, + html` +
  • +
    +
    + ${title} +
    +
    +
    + ${c.description?.trim().length + ? unsafeHTML( + marked.parse(c.description, { async: false }), + ) + : nothing} +
    +
    + ${c.uri && !c.html + ? html` + + + Tracking the original URI + + ` + : html` + + + Custom code + + `} +
    +
    +
    + + +
  • + `, + ); + }); + + const collapsed = unfavouriteCollapsed.get() && favouriteCol.length > 0; + + const h = col.length || filter !== "all" + ? html` + ${filterBar} ${favouriteCol.length + ? html` +
      ${renderItems(favouriteCol)}
    + ` + : nothing} ${favouriteCol.length && unfavouriteCol.length + ? html` +
    + +
    +
    + ` + : nothing} ${unfavouriteCol.length && !collapsed + ? html` +
      ${renderItems(unfavouriteCol)}
    + ` + : nothing} + ` + : html` + ${filterBar} ${emptyFacetsList()} + `; + + render(h, listEl); + + setTimeout(() => { + /** @type {HTMLElement | null} */ + const l = listEl.querySelector(".grid-filter--label-output"); + + /** @type {HTMLElement | null} */ + const o = listEl.querySelector(".grid-filter--output"); + + if (o && l) { + l.style.opacity = "0.4"; + o.style.opacity = "1"; + } + }, 250); +} diff --git a/src/common/pages/from-uri.js b/src/common/pages/from-uri.js new file mode 100644 index 000000000..c4f3b75f1 --- /dev/null +++ b/src/common/pages/from-uri.js @@ -0,0 +1,131 @@ +import { html, render } from "lit-html"; + +import { facetFromURI } from "~/common/facets/utils.js"; +import { saveFacet } from "./crud.js"; + +//////////////////////////////////////////// +// DIALOG +//////////////////////////////////////////// + +export function openAddFromURIModal() { + let dialog = /** @type {HTMLDialogElement | null} */ ( + document.getElementById("add-from-uri-dialog") + ); + + if (!dialog) { + dialog = /** @type {HTMLDialogElement} */ ( + document.createElement("dialog") + ); + + dialog.id = "add-from-uri-dialog"; + dialog.style.cssText = + "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0;"; + + render( + html` +
    +

    + Load a facet from a URI. Currently supported URI schemes: + https, at (AT Protocol) and diffuse + (references internal facets). +

    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + `, + dialog, + ); + + document.body.appendChild(dialog); + + dialog.querySelector("#add-uri-cancel")?.addEventListener("click", () => { + /** @type {HTMLDialogElement} */ (dialog).close(); + }); + + dialog.querySelector("#add-from-uri-form")?.addEventListener( + "submit", + async (e) => { + e.preventDefault(); + + const nameEl = /** @type {HTMLInputElement} */ ( + dialog?.querySelector("#add-uri-name") + ); + + const kindEl = /** @type {HTMLSelectElement} */ ( + dialog?.querySelector("#add-uri-kind") + ); + + const uriEl = /** @type {HTMLInputElement} */ ( + dialog?.querySelector("#add-uri-uri") + ); + + const name = nameEl?.value.trim() ?? ""; + const kind = kindEl?.value ?? "interactive"; + const uri = uriEl?.value.trim() ?? ""; + if (!name || !uri) return; + + const facet = await facetFromURI({ kind, name, uri }, { + fetchHTML: false, + }); + + await saveFacet(facet); + + /** @type {HTMLDialogElement} */ (dialog).close(); + }, + ); + } + + const nameEl = /** @type {HTMLInputElement} */ ( + dialog.querySelector("#add-uri-name") + ); + + const kindEl = /** @type {HTMLSelectElement} */ ( + dialog.querySelector("#add-uri-kind") + ); + + const uriEl = /** @type {HTMLInputElement} */ ( + dialog.querySelector("#add-uri-uri") + ); + + if (nameEl) nameEl.value = ""; + if (kindEl) kindEl.value = "interactive"; + if (uriEl) uriEl.value = ""; + + dialog.showModal(); +} diff --git a/src/common/pages/grid.js b/src/common/pages/grid.js new file mode 100644 index 000000000..2815d6d15 --- /dev/null +++ b/src/common/pages/grid.js @@ -0,0 +1,265 @@ +import * as Output from "~/common/output.js"; +import { facetFromURI } from "~/common/facets/utils.js"; +import { effect } from "~/common/signal.js"; + +import { output } from "./output.js"; + +//////////////////////////////////////////// +// FILTER +//////////////////////////////////////////// + +export function setupFilter() { + /** @type {NodeListOf} */ + const kindButtons = document.querySelectorAll( + ".grid-filter button[data-filter]", + ); + + /** @type {NodeListOf} */ + const items = document.querySelectorAll(".grid-item"); + + // Build category buttons from the categories present in the current grid + const categoriesEl = document.querySelector(".grid-filter--categories"); + const categories = /** @type {string[]} */ ( + [...new Set([...items].map((i) => i.dataset.category).filter(Boolean))] + .sort() + ); + + /** @type {HTMLElement | null} */ + let categoryLabelEl = null; + /** @type {HTMLElement | null} */ + let categoryMenuEl = null; + + if (categoriesEl && categories.length > 1) { + categoryLabelEl = document.createElement("span"); + categoryLabelEl.textContent = "All"; + + const triggerBtn = document.createElement("button"); + triggerBtn.className = "button--border button--tiny button--transparent"; + triggerBtn.setAttribute("popovertarget", "grid-category-menu"); + const span = document.createElement("span"); + span.className = "with-icon"; + span.appendChild(categoryLabelEl); + const caret = document.createElement("i"); + caret.className = "ph-bold ph-caret-down"; + span.appendChild(caret); + triggerBtn.appendChild(span); + + categoryMenuEl = document.createElement("div"); + categoryMenuEl.id = "grid-category-menu"; + categoryMenuEl.className = "dropdown"; + categoryMenuEl.setAttribute("popover", ""); + + for (const cat of ["all", ...categories]) { + const item = document.createElement("button"); + item.dataset.category = cat; + item.textContent = cat === "all" ? "All" : cat; + item.addEventListener("click", () => { + activeCategory = cat; + const url = new URL(location.href); + if (cat === "all") url.searchParams.delete("category"); + else url.searchParams.set("category", cat); + history.replaceState(null, "", url); + categoryMenuEl?.hidePopover(); + applyFilter(activeKind, activeCategory); + }); + categoryMenuEl.appendChild(item); + } + + categoriesEl.appendChild(triggerBtn); + categoriesEl.appendChild(categoryMenuEl); + } + + const FILTER_KIND_STORAGE_KEY = "diffuse/dashboard/filter"; + + let activeKind = "all"; + let activeCategory = "all"; + + /** + * @param {string} kind + * @param {string} category + */ + function applyFilter(kind, category) { + kindButtons.forEach((b) => { + const transparent = b.dataset.filter !== kind; + if (b.classList.contains("button--transparent") !== transparent) { + b.classList.toggle("button--transparent", transparent); + } + }); + if (categoryLabelEl) { + categoryLabelEl.textContent = category === "all" ? "All" : category; + } + items.forEach((item) => { + const isBase = (item.dataset.tags ?? "").split(",").includes("base"); + if (kind === "base") { + item.hidden = !isBase; + } else { + const kindMatch = kind === "all" || item.dataset.kind === kind; + const catMatch = category === "all" || + item.dataset.category === category; + item.hidden = !(kindMatch && catMatch && !isBase); + } + }); + } + + kindButtons.forEach((b) => { + b.addEventListener("click", () => { + activeKind = b.dataset.filter ?? "all"; + localStorage.setItem(FILTER_KIND_STORAGE_KEY, activeKind); + const url = new URL(location.href); + url.searchParams.delete("filter"); + history.replaceState(null, "", url); + applyFilter(activeKind, activeCategory); + }); + }); + + const url = new URL(location.href); + const filterParam = url.searchParams.get("filter"); + const storedKind = localStorage.getItem(FILTER_KIND_STORAGE_KEY); + activeKind = filterParam === "prelude" || filterParam === "interface" || + filterParam === "base" || filterParam === "all" + ? filterParam + : storedKind === "prelude" || storedKind === "interface" || + storedKind === "base" + ? storedKind + : "all"; + activeCategory = url.searchParams.get("category") ?? "all"; + applyFilter(activeKind, activeCategory); +} + +//////////////////////////////////////////// +// TOGGLE BUTTONS +//////////////////////////////////////////// + +export function insertToggleButtons() { + const gridItems = /** @type {NodeListOf} */ ( + document.querySelectorAll(".grid li") + ); + + for (const li of gridItems) { + const button = li.querySelector("button[data-action='toggle']"); + if (!button) continue; + + button.addEventListener("click", async () => { + const uri = li.getAttribute("data-uri"); + const name = li.getAttribute("data-name"); + const kind = li.getAttribute("data-kind") ?? undefined; + const description = li.getAttribute("data-description") ?? undefined; + const tagsRaw = li.getAttribute("data-tags"); + const tags = tagsRaw ? tagsRaw.split(",").filter(Boolean) : undefined; + + if (!uri || !name) return; + + const out = await output(); + const collection = await Output.data(out.facets); + const isActive = collection.some((f) => + f.uri === uri && f.html === undefined + ); + + if (isActive) { + out.facets.save(collection.filter((f) => f.uri !== uri)); + } else { + const facet = await facetFromURI( + { description, kind, name, tags, uri }, + { + fetchHTML: false, + }, + ); + out.facets.save([...collection, facet]); + } + }); + } +} + +//////////////////////////////////////////// +// SYNC ACTIVE STATES +//////////////////////////////////////////// + +/** @type {() => void | undefined} */ +let stopMonitor; + +export async function monitorToggleButtonStates() { + if (stopMonitor) stopMonitor(); + const out = await output(); + + stopMonitor = effect(() => { + const gridItems = /** @type {NodeListOf} */ ( + document.querySelectorAll(".grid li") + ); + + const col = out.facets.collection(); + const collection = col.state === "loaded" ? col.data : []; + const colMap = new Map(collection.map((f) => [f.uri, f])); + + for (const li of gridItems) { + const uri = li.getAttribute("data-uri"); + const menu = /** @type {HTMLElement | null} */ ( + li.querySelector(".grid-item__menu") + ); + + const button = /** @type {HTMLElement | null} */ ( + li.querySelector("button[data-action='toggle']") + ); + + const icon = button?.querySelector("i"); + + if (!menu || !button || !icon || !uri) continue; + + const item = colMap.get(uri); + const isActive = item && item.html === undefined; + const isPrelude = li.dataset.kind === "prelude"; + + menu.classList.toggle("grid-item__menu--active", isActive ?? false); + + button.style.opacity = "revert-layer"; + button.title = isActive + ? (isPrelude ? "Remove feature" : "Unpin interface") + : (isPrelude ? "Add feature" : "Pin interface"); + + icon.className = isActive + ? isPrelude ? "ph-bold ph-check" : "ph-fill ph-push-pin" + : isPrelude + ? "ph-bold ph-plus" + : "ph-bold ph-push-pin"; + + /** @type {HTMLElement} */ (icon).style.transform = isActive && !isPrelude + ? "rotate(-45deg)" + : ""; + } + }); +} + +//////////////////////////////////////////// +// OUTPUT INDICATOR +//////////////////////////////////////////// + +/** @type {() => void | undefined} */ +let stopOutputIndicator; + +export async function setupOutputIndicator() { + if (stopOutputIndicator) stopOutputIndicator(); + + const filterEl = document.querySelector(".grid-filter"); + if (!filterEl) return; + + const out = await output(); + + /** @type {HTMLElement | null} */ + const indicator = filterEl.querySelector(".grid-filter--output"); + if (!indicator) return; + + /** @type {HTMLElement | null} */ + const label = filterEl.querySelector(".grid-filter--label-output"); + if (!label) return; + + setTimeout(() => { + indicator.style.opacity = "1"; + label.style.opacity = "0.4"; + }, 250); + + stopOutputIndicator = effect(() => { + const selected = out.selected(); + const label = selected?.label ?? selected?.getAttribute?.("label") ?? + "Local storage"; + indicator.textContent = label; + }); +} diff --git a/src/common/pages/guide.js b/src/common/pages/guide.js new file mode 100644 index 000000000..cb96b2fd6 --- /dev/null +++ b/src/common/pages/guide.js @@ -0,0 +1,65 @@ +import { insertPreludes } from "../facets/prelude.js"; + +//////////////////////////////////////////// +// DEMO +//////////////////////////////////////////// + +async function addSampleContent() { + const addDemoBtn = document.querySelector("#add-sample-content"); + if (!addDemoBtn) return; + + addDemoBtn.setAttribute("disabled", ""); + addDemoBtn.innerHTML = ` + + Loading dependencies + `; + + const { default: foundation } = await import("~/common/foundation.js"); + const Output = await import("~/common/output.js"); + + const input = await foundation.configurator.input(); + const output = await foundation.orchestrator.output(); + const pto = await foundation.orchestrator.processTracks({ + disableWhenReady: true, + }); + + // Execute prelude + await insertPreludes( + await Output.data(output.facets), + ); + + /** @type {import("~/components/input/s3/element.js").CLASS | null} */ + const s3 = input.querySelector("di-s3"); + + if (!s3) { + throw new Error("S3 input not found"); + } + + addDemoBtn.innerHTML = ` + + Adding source + `; + + const demo = await s3.demo(); + + await output.tracks.save([ + ...(await Output.data(output.tracks)), + demo.track, + ]); + + addDemoBtn.innerHTML = ` + + Processing source + `; + + await pto.process(); + + addDemoBtn.innerHTML = ` + Added + `; +} + +export function setupSampleButton() { + const addDemoBtn = document.querySelector("#add-sample-content"); + addDemoBtn?.addEventListener("click", addSampleContent); +} diff --git a/src/common/pages/nav.js b/src/common/pages/nav.js new file mode 100644 index 000000000..3b93c0795 --- /dev/null +++ b/src/common/pages/nav.js @@ -0,0 +1,108 @@ +/** + * Updates the active state of nav links based on the current URL. + * Required because the nav now lives in
    and is not replaced by PPR. + */ +export function updateActiveLinks() { + const nav = document.getElementById("diffuse-nav"); + const menu = document.getElementById("nav-overflow-menu"); + + for (const container of [nav, menu]) { + if (!container) continue; + for (const link of container.querySelectorAll("a[href]")) { + const a = /** @type {HTMLAnchorElement} */ (link); + const isActive = new URL(a.href).pathname === location.pathname; + a.classList.toggle("button--transparent", !isActive); + } + } +} + +export function update() { + const nav = document.getElementById("diffuse-nav"); + const btn = document.getElementById("nav-overflow-btn"); + const menu = document.getElementById("nav-overflow-menu"); + + if (!nav || !btn || !menu) return; + + const items = /** @type {HTMLElement[]} */ ([...nav.children]); + + // Reset: show all items, hide button, restore default icon + for (const item of items) item.style.display = ""; + btn.style.display = "none"; + const span = btn.querySelector("span"); + if (span) span.innerHTML = ``; + + // Available width is the nav-container's width (nav may be content-sized in column layouts) + const availableWidth = (nav.parentElement ?? nav).clientWidth; + + // Measure total content width directly — scrollWidth is unreliable with + // justify-content: flex-end because overflow spills left (negative direction) + // and isn't reflected in scrollWidth. + const gap = parseFloat(getComputedStyle(nav).columnGap) || 0; + const contentWidth = () => { + const visible = items.filter((el) => el.style.display !== "none"); + return visible.reduce((acc, el) => acc + el.offsetWidth, 0) + + gap * Math.max(0, visible.length - 1); + }; + + // No overflow — nothing to do + if (contentWidth() <= availableWidth) return; + + // Show button (takes up space; subtract its width + container gap from available) + btn.style.display = ""; + const containerGap = parseFloat(getComputedStyle(nav.parentElement ?? nav).columnGap) || 0; + + // Hide items from right until content fits + const hidden = []; + for (let i = items.length - 1; i >= 0; i--) { + if (contentWidth() <= availableWidth - btn.offsetWidth - containerGap) break; + items[i].style.display = "none"; + hidden.unshift(items[i]); + } + + // Update button label: show "Menu" when all items are hidden + const allHidden = hidden.length === items.length; + if (span) { + span.innerHTML = allHidden + ? ` Menu` + : ``; + } + + // Populate dropdown with clones (stripped of button styling) + menu.innerHTML = ""; + for (const el of hidden) { + if (el.classList.contains("divider")) continue; + + const clone = /** @type {HTMLElement} */ (el.cloneNode(true)); + clone.style.display = ""; + clone.classList.remove( + "button", + "button--transparent", + "button--border", + "button--bg-twist-2", + ); + + menu.appendChild(clone); + } +} + +export function setupOverflowMenuClose() { + const menu = document.getElementById("nav-overflow-menu"); + if (!menu) return; + menu.addEventListener("click", (e) => { + if (/** @type {HTMLElement} */ (e.target).closest("a")) { + /** @type {any} */ (menu).hidePopover?.(); + } + }); +} + +let _observer = /** @type {ResizeObserver | undefined} */ (undefined); + +export function watchResize() { + const nav = document.getElementById("diffuse-nav"); + if (!nav) return; + _observer?.disconnect(); + _observer = new ResizeObserver(update); + // Observe the container — in column layouts the nav is content-sized and + // won't resize, but the container does as the header width changes. + _observer.observe(nav.parentElement ?? nav); +} diff --git a/src/common/pages/output.js b/src/common/pages/output.js new file mode 100644 index 000000000..d4761fb57 --- /dev/null +++ b/src/common/pages/output.js @@ -0,0 +1,51 @@ +import { effect } from "~/common/signal.js"; +import { insertPreludes } from "~/common/facets/prelude.js"; +import foundation from "~/common/foundation.js"; + +const elements = document.createElement("div"); +elements.id = "elements-container"; +document.body.appendChild(elements); + +let preludeInserted = false; +let preludeLoaded = false; + +effect(() => { + const out = foundation.signals.orchestrator.output(); + if (!out) return; + + const col = out.facets.collection(); + if (col.state !== "loaded") return; + + if (preludeInserted === false) { + preludeInserted = true; + insertPreludes(col.data, elements).then(() => { + preludeLoaded = true; + }); + } +}); + +export async function output() { + const out = await foundation.orchestrator.output(); + + if (out.hasSelected() && !out.selected() && !preludeLoaded) { + await new Promise((resolve) => { + let resolved = false; + + const stop = effect(() => { + if (resolved) { + stop(); + return; + } + + const s = out.selected(); + + if (s) { + resolved = true; + resolve(null); + } + }); + }); + } + + return out; +} diff --git a/src/common/pages/ppr.js b/src/common/pages/ppr.js new file mode 100644 index 000000000..7740483df --- /dev/null +++ b/src/common/pages/ppr.js @@ -0,0 +1,164 @@ +import * as Build from "./code.js"; +import * as Dashboard from "./dashboard.js"; +import * as Grid from "./grid.js"; +import * as Guide from "./guide.js"; +import * as Nav from "./nav.js"; + +/** Base pathname of the app (e.g. "/" at root, "/diffuse/" in a subdirectory). */ +const BASE_PATHNAME = new URL(document.baseURI).pathname; + +/** + * Strips the app's base path prefix from an absolute pathname, + * returning a root-relative path like "/code". + * + * @param {string} pathname + */ +function relativePathname(pathname) { + const stripped = pathname.replace(/\/$/, ""); + const base = BASE_PATHNAME.replace(/\/$/, ""); + return base.length > 0 && stripped.startsWith(base) + ? stripped.slice(base.length) + : stripped; +} + +/** + * @param {URL} url + */ +async function initJsBasedOnPage(url) { + const path = relativePathname(url.pathname); + + Nav.update(); + Nav.updateActiveLinks(); + Nav.watchResize(); + Nav.setupOverflowMenuClose(); + + Grid.setupFilter(); + Grid.insertToggleButtons(); + await Grid.monitorToggleButtonStates(); + await Grid.setupOutputIndicator(); + + switch (path) { + case "/create": + Build.renderEditor(); + Build.handleBuildFormSubmit(); + Build.listenForExamplesEdit(); + await Build.editFacetFromURL(); + break; + case "/dashboard": + await Dashboard.renderList(); + break; + case "/guide": + Guide.setupSampleButton(); + break; + default: + break; + } +} + +initJsBasedOnPage(new URL(location.href)); + +// Partial page updates for kitchen navigation using the Navigation API. +// Intercepts nav link clicks, fetches the new page, and swaps
    content +// instead of doing a full page load. + +if ("navigation" in globalThis) { + /** @type {any} */ (globalThis).navigation.addEventListener( + "navigate", + navigateHandler, + ); +} + +/** @param {any} event */ +function navigateHandler(event) { + if (!event.canIntercept) return; + + const url = new URL(event.destination.url); + if (url.origin !== location.origin) return; + if (url.pathname === location.pathname) return; + + // Only intercept paths one level deep + const relative = relativePathname(url.pathname); + const parts = relative.split("/").filter(Boolean); + if (parts.length === 0) return; + if (parts.length > 2) return; + + // Skip the loader page + if (parts[0] === "l") return; + if (parts.includes("chronicle")) return; + + event.intercept({ + scroll: "manual", + async handler() { + const navLinks = /** @type {HTMLAnchorElement[]} */ ([ + ...document.querySelectorAll("#diffuse-nav a, #nav-overflow-menu a"), + ]); + const stripSlash = (/** @type {string} */ p) => p.replace(/^\//, ""); + const navLink = navLinks.find( + (a) => + stripSlash(new URL(a.href).pathname) === stripSlash(url.pathname), + ); + + const icon = navLink?.querySelector("i"); + const originalIconClass = icon?.className; + let addedSpinner = /** @type {HTMLElement | undefined} */ (undefined); + + const loadingTimer = navLink + ? setTimeout(() => { + if (icon) { + icon.className = "ph-bold ph-spinner animate-spin"; + } else { + addedSpinner = document.createElement("i"); + addedSpinner.className = "ph-bold ph-spinner animate-spin"; + const span = navLink.querySelector("span"); + (span ?? navLink).prepend(addedSpinner); + } + }, 250) + : undefined; + + let html; + + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`${response.status}`); + html = await response.text(); + } catch { + clearTimeout(loadingTimer); + if (icon && originalIconClass !== undefined) icon.className = originalIconClass; + addedSpinner?.remove(); + location.href = url.href; + return; + } finally { + clearTimeout(loadingTimer); + if (icon && originalIconClass !== undefined) icon.className = originalIconClass; + addedSpinner?.remove(); + } + + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + + const newMain = doc.querySelector("main"); + const currentMain = document.querySelector("main"); + + if (!newMain || !currentMain) { + location.href = url.href; + return; + } + + document.title = doc.title; + + // Replace
    content + const range = document.createRange(); + range.selectNode(currentMain); + const documentFragment = range.createContextualFragment( + newMain.innerHTML ?? "", + ); + + currentMain.innerHTML = ""; + currentMain.append(documentFragment); + + initJsBasedOnPage(url); + + window.scrollTo({ top: 0, behavior: "instant" }); + }, + }); +} diff --git a/src/common/pages/version-upgrade.js b/src/common/pages/version-upgrade.js new file mode 100644 index 000000000..0d5e79bf4 --- /dev/null +++ b/src/common/pages/version-upgrade.js @@ -0,0 +1,270 @@ +import { + canParse, + greaterThan, + parse as parseSemver, + satisfies, + tryParseRange, +} from "@std/semver"; + +/** + * Given the current URL segment and the latest known artifact, returns whether + * the user is already on the latest version. + * + * @param {string} versionOrCid - The first path segment of the current URL + * @param {{ cid: string, version: string } | null} lastArtifact - The latest artifact + * @returns {boolean} + * + * @example No artifact means always latest + * ```js + * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; + * + * if (!checkIsLatest("4.0.0", null)) throw new Error("no artifact should be latest"); + * ``` + * + * @example CID comparison + * ```js + * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; + * + * const artifact = { cid: "bafyabc", version: "4.0.0" }; + * if (!checkIsLatest("bafyabc", artifact)) throw new Error("matching CID should be latest"); + * if (checkIsLatest("bafyxyz", artifact)) throw new Error("different CID should not be latest"); + * ``` + * + * @example Exact version comparison + * ```js + * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; + * + * const artifact = { cid: "bafyabc", version: "4.0.0" }; + * if (!checkIsLatest("4.0.0", artifact)) throw new Error("matching version should be latest"); + * if (checkIsLatest("3.9.0", artifact)) throw new Error("older version should not be latest"); + * ``` + * + * @example Version range (e.g. 4.0.x) + * ```js + * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; + * + * const artifact = { cid: "bafyabc", version: "4.0.5" }; + * if (!checkIsLatest("4.0.x", artifact)) throw new Error("latest within range should be latest"); + * if (checkIsLatest("3.x", artifact)) throw new Error("latest outside range should not be latest"); + * ``` + * + * @example Caret and tilde ranges + * ```js + * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; + * + * const artifact = { cid: "bafyabc", version: "4.1.0" }; + * if (!checkIsLatest("^4.0.1", artifact)) throw new Error("^4.0.1 should match 4.1.0"); + * if (checkIsLatest("~4.0.1", artifact)) throw new Error("~4.0.1 should not match 4.1.0"); + * ``` + * + * @example Partial versions are filled in with zeros (>=4 is equivalent to >=4.0.0) + * ```js + * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; + * + * const artifact = { cid: "bafyabc", version: "4.1.0" }; + * if (!checkIsLatest(">=4", artifact)) throw new Error(">=4 should match 4.1.0"); + * if (checkIsLatest(">=5", artifact)) throw new Error(">=5 should not match 4.1.0"); + * ``` + * + * @example Non-semver, non-range slugs are always latest + * ```js + * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; + * + * const artifact = { cid: "bafyabc", version: "4.0.0" }; + * if (!checkIsLatest("some-branch", artifact)) throw new Error("non-semver slug should be latest"); + * ``` + */ +export function checkIsLatest(versionOrCid, lastArtifact) { + if (!lastArtifact) return true; + const usesCid = versionOrCid.startsWith("bafy"); + if (usesCid) return versionOrCid === lastArtifact.cid; + if (canParse(versionOrCid)) return versionOrCid === lastArtifact.version; + const versionRange = tryParseRange(versionOrCid); + if (versionRange) { + return satisfies(parseSemver(lastArtifact.version), versionRange); + } + return true; +} + +/** + * Coerce a version string that may contain `x` wildcards (e.g. `4.x-nightly`) + * into a parseable semver string (e.g. `4.99.0-nightly`). Returns the original + * string if it's already parseable, or `null` if coercion fails. + * + * @param {string} version + * @returns {string | null} + */ +function tryCoerceVersion(version) { + if (canParse(version)) return version; + const coerced = version.replace(/[xX]/g, "99"); + if (canParse(coerced)) return coerced; + // Fill in missing patch segment (e.g., "4.99-nightly" -> "4.99.0-nightly") + const withPatch = coerced.replace( + /^(\d+\.\d+)([+-]|$)/, + "$1.0$2", + ); + if (canParse(withPatch)) return withPatch; + return null; +} + +/** + * @param {Record} artifacts + * @param {{ includePrerelease?: boolean }} [options] + * @returns {{ version: string, cid: string } | null} + * + * @example Returns null for an empty artifact list + * ```js + * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; + * + * if (getLatestArtifact({}) !== null) throw new Error("empty artifacts should return null"); + * ``` + * + * @example Returns the highest semver artifact + * ```js + * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; + * + * const artifacts = { + * a: { cid: "a", version: "4.0.0" }, + * b: { cid: "b", version: "4.1.0" }, + * c: { cid: "c", version: "3.9.0" }, + * }; + * if (getLatestArtifact(artifacts)?.cid !== "b") throw new Error("should return highest version"); + * ``` + * + * @example Ignores non-semver versions + * ```js + * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; + * + * const artifacts = { + * a: { cid: "a", version: "4.0.0" }, + * b: { cid: "b", version: "some-branch" }, + * }; + * if (getLatestArtifact(artifacts)?.cid !== "a") throw new Error("should ignore non-semver versions"); + * ``` + * + * @example Coerces x-wildcard prerelease (4.x-nightly beats 3.5.0) + * ```js + * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; + * + * const artifacts = { + * a: { cid: "a", version: "3.5.0" }, + * b: { cid: "b", version: "4.x-nightly" }, + * }; + * if (getLatestArtifact(artifacts)?.cid !== "b") { + * throw new Error("4.x-nightly should be latest"); + * } + * ``` + * + * @example Coerced x-wildcard with prerelease beats higher stable of same major + * ```js + * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; + * + * const artifacts = { + * a: { cid: "a", version: "4.5.1" }, + * b: { cid: "b", version: "4.x-nightly" }, + * }; + * if (getLatestArtifact(artifacts, { includePrerelease: true })?.cid !== "b") { + * throw new Error("4.x-nightly should beat 4.5.1 when includePrerelease is true"); + * } + * ``` + * + * @example Excludes prerelease artifacts when includePrerelease is false + * ```js + * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; + * + * const artifacts = { + * a: { cid: "a", version: "4.1.0" }, + * b: { cid: "b", version: "4.2.0-nightly.1" }, + * }; + * if (getLatestArtifact(artifacts, { includePrerelease: false })?.cid !== "a") { + * throw new Error("should exclude prerelease artifacts"); + * } + * if (getLatestArtifact(artifacts, { includePrerelease: true })?.cid !== "b") { + * throw new Error("should include prerelease artifacts when opted in"); + * } + * ``` + */ + +export function getLatestArtifact( + artifacts, + { includePrerelease = true } = {}, +) { + return Object.values(artifacts).reduce( + /** @param {{ version: string, cid: string } | null} max */ + (max, artifact) => { + const comparable = tryCoerceVersion(artifact.version); + if (!comparable) return max; + if ( + !includePrerelease && parseSemver(comparable).prerelease?.length + ) return max; + if (!max) return artifact; + return greaterThan( + parseSemver(comparable), + parseSemver(tryCoerceVersion(max.version) ?? max.version), + ) + ? artifact + : max; + }, + /** @type {{ version: string, cid: string } | null} */ (null), + ); +} + +/** @param {Element} status */ +function removeLoadingAnimation(status) { + status.querySelectorAll(".ph-spinner").forEach((icon) => { + icon.parentElement?.classList.add("hidden"); + + setTimeout(() => { + icon.parentElement?.classList.remove("animate-spin"); + icon.classList.remove("ph-spinner"); + icon.classList.add("ph-arrow-fat-lines-up"); + }, 500); + }); +} + +/** + * @param {Element} status + * @param {{ usesCid: boolean, isLatest: boolean }} options + */ +function updateUpgradeLink(status, { usesCid, isLatest }) { + status.querySelectorAll(`[href="/latest/"]`).forEach((a) => { + if (usesCid) a.setAttribute("href", "/latest/hash/"); + if (!isLatest) setTimeout(() => a.classList.remove("hidden"), 750); + }); +} + +/** + * Setup version upgrade (only works with `diffuse-artifacts` deployments) + */ +export async function versionUpgrade() { + const isDiffuseDomain = document.location.hostname.endsWith("diffuse.sh"); + + if (!isDiffuseDomain) { + document.querySelectorAll("#status a").forEach((el) => { + el.classList.add("hidden"); + }); + + return; + } + + const versionOrCid = + document.location.pathname.slice(1).split("/")[0]?.toLowerCase() ?? ""; + const usesCid = versionOrCid.startsWith("bafy"); + + const { default: artifacts } = await import( + `${document.location.origin}/artifacts.json`, + { with: { type: "json" } } + ).catch(() => ({ default: {} })); + + const currentIsStable = canParse(versionOrCid) && + !parseSemver(versionOrCid).prerelease?.length; + const lastArtifact = getLatestArtifact(artifacts, { + includePrerelease: !currentIsStable, + }); + const isLatest = checkIsLatest(versionOrCid, lastArtifact); + + document.querySelectorAll("#status").forEach((status) => { + removeLoadingAnimation(status); + updateUpgradeLink(status, { usesCid, isLatest }); + }); +} diff --git a/src/common/playlist.js b/src/common/playlist.js new file mode 100644 index 000000000..d5ff3459f --- /dev/null +++ b/src/common/playlist.js @@ -0,0 +1,441 @@ +/** + * @import {PlaylistItem, Track} from "~/definitions/types.d.ts" + */ + +import { compareTimestamps } from "~/common/temporal.js"; + +/** + * Filter tracks by playlist membership using an indexed lookup. + * + * @param {Track[]} tracks + * @param {PlaylistItem[]} playlistItems + * + * @example Returns only tracks matching playlist criteria + * ```js + * import { filterByPlaylist } from "~/common/playlist.js"; + * + * const tracks = [ + * { $type: "sh.diffuse.output.track", id: "a", uri: "http://x.com/a.mp3", tags: { artist: "A", title: "T1" } }, + * { $type: "sh.diffuse.output.track", id: "b", uri: "http://x.com/b.mp3", tags: { artist: "B", title: "T2" } }, + * ]; + * const items = [ + * { $type: "sh.diffuse.output.playlistItem", id: "i1", playlist: "p", criteria: [{ field: "tags.artist", value: "A" }] }, + * ]; + * // @ts-ignore + * const result = filterByPlaylist(tracks, items); + * if (result.length !== 1 || result[0].id !== "a") throw new Error("expected only track 'a'"); + * ``` + * + * @example Returns empty array when no tracks match or no items given + * ```js + * import { filterByPlaylist } from "~/common/playlist.js"; + * + * const tracks = [ + * { $type: "sh.diffuse.output.track", id: "a", uri: "http://x.com/a.mp3", tags: { artist: "A" } }, + * ]; + * const noMatchItems = [ + * { $type: "sh.diffuse.output.playlistItem", id: "i1", playlist: "p", criteria: [{ field: "tags.artist", value: "Z" }] }, + * ]; + * // @ts-ignore + * if (filterByPlaylist(tracks, noMatchItems).length !== 0) throw new Error("expected no matches"); + * // @ts-ignore + * if (filterByPlaylist(tracks, []).length !== 0) throw new Error("expected empty for no items"); + * ``` + * + * @example Applies transformations before comparing + * ```js + * import { filterByPlaylist } from "~/common/playlist.js"; + * + * const tracks = [{ $type: "sh.diffuse.output.track", id: "a", uri: "http://x.com/a.mp3", tags: { artist: "Artist" } }]; + * const items = [{ + * $type: "sh.diffuse.output.playlistItem", + * id: "i1", + * playlist: "p", + * criteria: [{ field: "tags.artist", value: "ARTIST", transformations: ["toLowerCase"] }], + * }]; + * // @ts-ignore + * if (filterByPlaylist(tracks, items).length !== 1) throw new Error("transformation should match"); + * ``` + */ +export function filterByPlaylist(tracks, playlistItems) { + // Group playlist items by criteria shape, building a Set index per shape. + const shapes = playlistItems + .reduce( + (acc, playlistItem) => { + const shapeKey = playlistItem.criteria + .map((c) => `${c.field}\0${(c.transformations ?? []).join(",")}`) + .join("\0\0"); + + const group = acc.get(shapeKey) ?? acc + .set(shapeKey, { criteria: playlistItem.criteria, keys: new Set() }) + .get(shapeKey); + + group?.keys.add( + playlistItem.criteria.map((c) => + transform(c.value, c.transformations) + ).join( + "\0", + ), + ); + + return acc; + }, + /** @type {Map }>} */ (new Map()), + ) + .values() + .map((group) => ({ + fields: group.criteria.map((c) => ({ + parts: c.field.split("."), + transformations: c.transformations, + })), + keys: group.keys, + })) + .toArray(); + + return tracks.filter((track) => + shapes.some((shape) => + shape.keys.has( + shape.fields + .map(({ parts, transformations }) => + transform( + parts.reduce((v, f) => v?.[f], /** @type {any} */ (track)), + transformations, + ) + ) + .join("\0"), + ) + ) + ); +} + +/** + * Order already-playlist-member tracks by their playlist's `positionedAfter` + * chain. Tracks are bucketed to the first playlist item (in chain order) that + * they match; each track appears only once. Tracks not matched by any item are + * dropped (the caller is expected to pre-filter with `filterByPlaylist`). + * + * When the playlist is unordered (or empty), the input tracks are returned + * unchanged so the caller can fall back to its own sort. + * + * @param {Track[]} tracks Tracks already known to belong to the playlist. + * @param {PlaylistItem[]} items Items of a single playlist. + * @returns {Track[]} + * + * @example Orders tracks by their playlist item chain + * ```js + * import { orderByPlaylist } from "~/common/playlist.js"; + * + * const tracks = [ + * { $type: "sh.diffuse.output.track", id: "t-b", uri: "http://x/b.mp3", tags: { title: "B" } }, + * { $type: "sh.diffuse.output.track", id: "t-a", uri: "http://x/a.mp3", tags: { title: "A" } }, + * { $type: "sh.diffuse.output.track", id: "t-c", uri: "http://x/c.mp3", tags: { title: "C" } }, + * ]; + * const items = [ + * { $type: "sh.diffuse.output.playlistItem", id: "i-b", playlist: "p", criteria: [{ field: "tags.title", value: "B" }], positionedAfter: "i-a" }, + * { $type: "sh.diffuse.output.playlistItem", id: "i-a", playlist: "p", criteria: [{ field: "tags.title", value: "A" }], positionedAfter: undefined }, + * { $type: "sh.diffuse.output.playlistItem", id: "i-c", playlist: "p", criteria: [{ field: "tags.title", value: "C" }], positionedAfter: "i-b" }, + * ]; + * // @ts-ignore + * const result = orderByPlaylist(tracks, items); + * if (result.map((t) => t.id).join(",") !== "t-a,t-b,t-c") throw new Error("should follow chain order"); + * ``` + * + * @example Returns tracks unchanged for unordered playlists + * ```js + * import { orderByPlaylist } from "~/common/playlist.js"; + * + * const tracks = [{ $type: "sh.diffuse.output.track", id: "t-a", uri: "http://x/a.mp3", tags: { title: "A" } }]; + * const items = [{ $type: "sh.diffuse.output.playlistItem", id: "i-a", playlist: "p", criteria: [{ field: "tags.title", value: "A" }] }]; + * // @ts-ignore + * const result = orderByPlaylist(tracks, items); + * if (result !== tracks) throw new Error("unordered playlists should return input untouched"); + * ``` + * + * @example A track matching multiple items appears only under the first + * ```js + * import { orderByPlaylist } from "~/common/playlist.js"; + * + * const tracks = [ + * { $type: "sh.diffuse.output.track", id: "t-a", uri: "http://x/a.mp3", tags: { artist: "X", title: "A" } }, + * ]; + * const items = [ + * { $type: "sh.diffuse.output.playlistItem", id: "i-a", playlist: "p", criteria: [{ field: "tags.artist", value: "X" }], positionedAfter: undefined }, + * { $type: "sh.diffuse.output.playlistItem", id: "i-b", playlist: "p", criteria: [{ field: "tags.title", value: "A" }], positionedAfter: "i-a" }, + * ]; + * // @ts-ignore + * const result = orderByPlaylist(tracks, items); + * if (result.length !== 1) throw new Error("track should appear only once"); + * ``` + */ +export function orderByPlaylist(tracks, items) { + if (!items.length) return tracks; + + // Detect ordered state for the (single) playlist the items belong to. + const grouped = gather(items); + const meta = grouped.values().next().value; + if (!meta || meta.unordered) return tracks; + + const orderedItems = sort(items); + const claimed = new Set(); + /** @type {Track[]} */ + const out = []; + + for (const item of orderedItems) { + let matchedAny = false; + for (const track of tracks) { + if (claimed.has(track.id)) continue; + if (match(track, item)) { + claimed.add(track.id); + out.push(track); + matchedAny = true; + } + } + // Items with no matching tracks don't contribute to the output but the + // chain still proceeds so following items can claim what they can. + void matchedAny; + } + + return out; +} + +/** + * Bundle playlist items into their respective playlists. + * + * @param {PlaylistItem[]} items + * + * @example Groups items by playlist name and tracks ordered/unordered state + * ```js + * import { gather } from "~/common/playlist.js"; + * + * const items = [ + * { $type: "sh.diffuse.output.playlistItem", id: "1", playlist: "Rock", criteria: [], positionedAfter: "prev" }, + * { $type: "sh.diffuse.output.playlistItem", id: "2", playlist: "Pop", criteria: [], positionedAfter: "prev" }, + * { $type: "sh.diffuse.output.playlistItem", id: "3", playlist: "Rock", criteria: [], positionedAfter: "prev" }, + * ]; + * // @ts-ignore + * const map = gather(items); + * const rock = map.get("Rock"); + * const pop = map.get("Pop"); + * if (!rock || !pop) throw new Error("expected Rock and Pop playlists"); + * if (rock.items.length !== 2) throw new Error("expected 2 rock items"); + * if (pop.items.length !== 1) throw new Error("expected 1 pop item"); + * + * // @ts-ignore + * const unordered = gather([ + * { $type: "sh.diffuse.output.playlistItem", id: "1", playlist: "Mix", criteria: [] }, + * { $type: "sh.diffuse.output.playlistItem", id: "2", playlist: "Mix", criteria: [] }, + * ]); + * const unorderedMix = unordered.get("Mix"); + * if (!unorderedMix) throw new Error("expected Mix playlist"); + * if (!unorderedMix.unordered) throw new Error("playlist without positionedAfter should be unordered"); + * + * // @ts-ignore + * const ordered = gather([ + * { $type: "sh.diffuse.output.playlistItem", id: "1", playlist: "Mix", criteria: [], positionedAfter: undefined }, + * { $type: "sh.diffuse.output.playlistItem", id: "2", playlist: "Mix", criteria: [], positionedAfter: "1" }, + * ]); + * const orderedMix = ordered.get("Mix"); + * if (!orderedMix) throw new Error("expected Mix playlist"); + * if (orderedMix.unordered) throw new Error("playlist with positionedAfter should be ordered"); + * ``` + */ +export function gather(items) { + /** + * @type {Map} + */ + const playlistMap = new Map(); + + for (const item of items) { + const existing = playlistMap.get(item.playlist); + + if (!existing) { + playlistMap.set(item.playlist, { + items: [item], + name: item.playlist, + unordered: item.positionedAfter == null, + }); + } else { + existing.items.push(item); + existing.unordered = existing.unordered === false + ? false + : item.positionedAfter == null; + } + } + + return playlistMap; +} + +/** + * Check if a track matches the criteria of a playlist item. + * + * @param {Track} track + * @param {PlaylistItem} item + * + * @example Returns true when all criteria match, false when any criterion fails + * ```js + * import { match } from "~/common/playlist.js"; + * + * const track = { $type: "sh.diffuse.output.track", id: "t", uri: "http://x.com/t.mp3", tags: { artist: "Artist A", title: "Song A" } }; + * const item = { + * $type: "sh.diffuse.output.playlistItem", id: "i", playlist: "p", + * criteria: [{ field: "tags.artist", value: "Artist A" }, { field: "tags.title", value: "Song A" }], + * }; + * // @ts-ignore + * if (!match(track, item)) throw new Error("should match when all criteria pass"); + * + * const mismatch = { ...item, criteria: [{ field: "tags.artist", value: "Artist A" }, { field: "tags.title", value: "Wrong" }] }; + * // @ts-ignore + * if (match(track, mismatch)) throw new Error("should not match when a criterion fails"); + * ``` + * + * @example Applies transformations before comparing + * ```js + * import { match } from "~/common/playlist.js"; + * + * const track = { $type: "sh.diffuse.output.track", id: "t", uri: "http://x.com/t.mp3", tags: { artist: "Artist A" } }; + * const item = { + * $type: "sh.diffuse.output.playlistItem", id: "i", playlist: "p", + * criteria: [{ field: "tags.artist", value: "ARTIST A", transformations: ["toLowerCase"] }], + * }; + * // @ts-ignore + * if (!match(track, item)) throw new Error("transformation should match lowercase"); + * ``` + */ +export function match(track, item) { + return item.criteria.every((c) => { + /** @type {any} */ + let value = track; + + /** @type {any} */ + let critValue = c.value; + + c.field.split(".").forEach((f) => { + if (value) value = value[f]; + }); + + if (value && c.transformations) { + c.transformations.forEach((t) => { + try { + value = value[t](); + critValue = critValue[t](); + } catch (err) {} + }); + } + + return critValue === value; + }); +} + +/** + * Sort playlist items by their `positionedAfter` linked-list order. + * Items with no `positionedAfter` are placed first. + * + * @param {PlaylistItem[]} items + * @returns {PlaylistItem[]} + * + * @example Sorts a linked list in order and appends orphaned items at end + * ```js + * import { sort } from "~/common/playlist.js"; + * + * const single = [{ $type: "sh.diffuse.output.playlistItem", id: "a", playlist: "p", criteria: [], positionedAfter: undefined }]; + * // @ts-ignore + * if (sort(single).map((i) => i.id).join(",") !== "a") throw new Error("single item should be unchanged"); + * + * const linked = [ + * { $type: "sh.diffuse.output.playlistItem", id: "c", playlist: "p", criteria: [], positionedAfter: "b" }, + * { $type: "sh.diffuse.output.playlistItem", id: "a", playlist: "p", criteria: [], positionedAfter: undefined }, + * { $type: "sh.diffuse.output.playlistItem", id: "b", playlist: "p", criteria: [], positionedAfter: "a" }, + * ]; + * // @ts-ignore + * if (sort(linked).map((i) => i.id).join(",") !== "a,b,c") throw new Error("should sort linked list in order"); + * + * const withOrphan = [ + * { $type: "sh.diffuse.output.playlistItem", id: "a", playlist: "p", criteria: [], positionedAfter: undefined }, + * { $type: "sh.diffuse.output.playlistItem", id: "b", playlist: "p", criteria: [], positionedAfter: "a" }, + * { $type: "sh.diffuse.output.playlistItem", id: "orphan", playlist: "p", criteria: [], positionedAfter: "missing" }, + * ]; + * // @ts-ignore + * const sorted = sort(withOrphan); + * if (sorted[sorted.length - 1].id !== "orphan") throw new Error("orphaned item should be last"); + * ``` + * + * @example Sorts multiple heads by updatedAt ascending + * ```js + * import { sort } from "~/common/playlist.js"; + * + * const items = [ + * { $type: "sh.diffuse.output.playlistItem", id: "b", playlist: "p", criteria: [], positionedAfter: undefined, updatedAt: "2024-06-01T00:00:00.000Z" }, + * { $type: "sh.diffuse.output.playlistItem", id: "a", playlist: "p", criteria: [], positionedAfter: undefined, updatedAt: "2024-01-01T00:00:00.000Z" }, + * ]; + * // @ts-ignore + * const result = sort(items); + * if (result[0].id !== "a" || result[1].id !== "b") throw new Error("heads should be sorted by updatedAt"); + * ``` + */ +export function sort(items) { + if (items.length <= 1) return items; + + /** @type {Map} */ + const afterMap = new Map(); + + for (const item of items) { + const key = item.positionedAfter ?? null; + const group = afterMap.get(key); + if (group) { + group.push(item); + } else { + afterMap.set(key, [item]); + } + } + + // Sort each group by updatedAt so collisions have a deterministic order. + for (const group of afterMap.values()) { + if (group.length > 1) { + group.sort((a, b) => { + if (!a.updatedAt || !b.updatedAt) return a.updatedAt ? 1 : -1; + return compareTimestamps(a.updatedAt, b.updatedAt); + }); + } + } + + /** @type {PlaylistItem[]} */ + const sorted = []; + const visited = new Set(); + + /** @type {PlaylistItem[]} */ + const queue = [...(afterMap.get(null) ?? [])]; + + while (queue.length > 0) { + const current = /** @type {PlaylistItem} */ (queue.shift()); + if (visited.has(current.id)) continue; + visited.add(current.id); + sorted.push(current); + + const next = afterMap.get(current.id); + if (next) queue.unshift(...next); + } + + // Append any items not reachable from a head (e.g. broken chains). + for (const item of items) { + if (!visited.has(item.id)) { + sorted.push(item); + } + } + + return sorted; +} + +/** + * @param {any} val + * @param {string[] | undefined} transformations + */ +export function transform(val, transformations) { + if (!val || !transformations) return val; + return transformations.reduce((v, t) => { + try { + return v[t](); + } catch (_) { + return v; + } + }, val); +} diff --git a/src/common/signal.d.ts b/src/common/signal.d.ts new file mode 100644 index 000000000..1929f7e76 --- /dev/null +++ b/src/common/signal.d.ts @@ -0,0 +1,10 @@ +export type SignalReader = () => T; +export type SignalWriter = (t: T) => void; + +export type Signal = { + get: SignalReader; + set: SignalWriter; + + get value(): T; + set value(t: T); +}; diff --git a/src/common/signal.js b/src/common/signal.js new file mode 100644 index 000000000..f57fe3726 --- /dev/null +++ b/src/common/signal.js @@ -0,0 +1,206 @@ +import { + effect as alienEffect, + endBatch, + setActiveSub, + signal as alienSignal, + startBatch, +} from "alien-signals"; + +export * from "alien-signals"; + +/** + * `effect` wrapper that tolerates async effect bodies. + * + * alien-signals v3.2.0+ stores `e.cleanup = e.fn()` and invokes + * `cleanup()` when the effect re-runs or is disposed. An async effect body + * returns a `Promise`, which is not a function — calling it later throws + * `TypeError: cleanup is not a function`. This wrapper normalises a + * `Promise` (or any thenable) return to `undefined` so async effect bodies + * are safe to use without manual try/catch around the return value. + * + * Sync effect bodies that return a real cleanup function are passed + * through unchanged. + * + * @param {() => (unknown | Promise)} fn + * @returns {() => void} + */ +export function effect(fn) { + return alienEffect(() => { + const cleanup = fn(); + return cleanup && typeof /** @type {any} */ (cleanup).then === "function" + ? /** @type {void} */ (undefined) + : /** @type {(() => void) | void} */ (cleanup); + }); +} + +/** + * @import {Signal, SignalReader, SignalWriter} from "./signal.d.ts" + */ + +/** + * @param {function(): void} fn + * + * @example Defers effect execution until batch completes + * ```js + * import { signal, effect, batch } from "~/common/signal.js"; + * + * const a = signal(0); + * const b = signal(0); + * const values = [0]; values.length = 0; // typed as number[] + * + * effect(() => { values.push(a.get() + b.get()); }); + * + * const before = [...values]; // [0] + * batch(() => { a.set(1); b.set(2); }); + * + * if (before.join(",") !== "0") throw new Error("expected [0] before batch"); + * if (values.join(",") !== "0,3") throw new Error("expected exactly one update after batch, got " + values.join(",")); + * ``` + */ +export const batch = (fn) => { + startBatch(); + try { + fn(); + } finally { + endBatch(); + } +}; + +/** + * @template T + * @param {T} initialValue + * @param {{ compare?: (a: T, b: T) => boolean }} [options] + * @returns {Signal} + * + * @example get/set and value getter/setter return and update the value + * ```js + * import { signal } from "~/common/signal.js"; + * + * const num = signal(42); + * if (num.get() !== 42) throw new Error("get should return initial value"); + * if (num.value !== 42) throw new Error("value getter should return initial value"); + * + * num.set(99); + * if (num.get() !== 99) throw new Error("get should return updated value"); + * + * const str = signal("a"); + * str.value = "b"; + * if (str.value !== "b") throw new Error("value setter should update value"); + * ``` + * + * @example compare option skips update when values are equal by custom comparator + * ```js + * import { signal, effect } from "~/common/signal.js"; + * + * let runCount = 0; + * const s = signal({ x: 1 }, { compare: (a, b) => a.x === b.x }); + * + * effect(() => { s.get(); runCount++; }); + * + * const before = runCount; + * s.set({ x: 1 }); // same by compare + * if (runCount !== before) throw new Error("effect should not re-run when value is equal by compare"); + * + * s.set({ x: 2 }); // different by compare + * if (runCount !== before + 1) throw new Error("effect should re-run when value differs by compare"); + * ``` + */ +export function signal(initialValue, options) { + const s = alienSignal(initialValue); + if (options?.compare) { + const compare = options.compare; + + return _signal({ + get: () => s(), + set: (b) => { + const a = untracked(() => s()); + if (!compare(a, b)) s(b); + }, + }); + } + + return _signal({ + get: () => s(), + set: (v) => s(v), + }); +} + +/** + * @template T + * @param {function(): T} fn + * @returns {T} + * + * @example Reads a signal without tracking it as a dependency + * ```js + * import { signal, effect, untracked } from "~/common/signal.js"; + * + * const a = signal(1); + * const b = signal(10); + * let runCount = 0; + * + * effect(() => { + * a.get(); // tracked + * untracked(() => b.get()); // not tracked + * runCount++; + * }); + * + * const before = runCount; // 1 + * b.set(20); // should NOT re-run effect + * if (runCount !== before) throw new Error("untracked read should not trigger re-run"); + * + * a.set(2); // SHOULD re-run effect + * if (runCount !== before + 1) throw new Error("tracked read should trigger re-run"); + * + * if (untracked(() => a.get()) !== 2) throw new Error("untracked should return the value"); + * ``` + */ +export const untracked = (fn) => { + const sub = setActiveSub(void 0); + try { + return fn(); + } finally { + setActiveSub(sub); + } +}; + +/** + * @template T + * @param {function(): Promise} fn + * @returns {Promise} + * + * @example Returns the resolved value from the async callback + * ```js + * import { untrackedAsync } from "~/common/signal.js"; + * + * const result = await untrackedAsync(async () => 99); + * if (result !== 99) throw new Error("untrackedAsync should return resolved value"); + * ``` + */ +export const untrackedAsync = async (fn) => { + const sub = setActiveSub(void 0); + try { + return await fn(); + } finally { + setActiveSub(sub); + } +}; + +/** + * @template T + * @param {{ get: SignalReader; set: SignalWriter }} _ + * @returns {Signal} + */ +function _signal({ get, set }) { + return { + get, + set, + + get value() { + return get(); + }, + + set value(v) { + set(v); + }, + }; +} diff --git a/src/common/temporal.js b/src/common/temporal.js new file mode 100644 index 000000000..c9fba3d3f --- /dev/null +++ b/src/common/temporal.js @@ -0,0 +1,31 @@ +import { Temporal as _polyfill } from "temporal-polyfill"; + +export const Temporal = /** @type {any} */ (globalThis).Temporal ?? _polyfill; + +/** + * @param {string} a + * @param {string} b + * + * @example Returns 0 for equal timestamps, negative for earlier, positive for later + * ```js + * import { compareTimestamps } from "~/common/temporal.js"; + * + * const eq = compareTimestamps("2024-01-01T00:00:00.000Z", "2024-01-01T00:00:00.000Z"); + * if (eq !== 0) throw new Error("equal timestamps should return 0"); + * + * const earlier = compareTimestamps("2024-01-01T00:00:00.000Z", "2024-06-01T00:00:00.000Z"); + * if (earlier >= 0) throw new Error("earlier timestamp should return negative"); + * + * const later = compareTimestamps("2024-06-01T00:00:00.000Z", "2024-01-01T00:00:00.000Z"); + * if (later <= 0) throw new Error("later timestamp should return positive"); + * + * const ms = compareTimestamps("2024-01-01T00:00:00.001Z", "2024-01-01T00:00:00.000Z"); + * if (ms <= 0) throw new Error("millisecond precision should be respected"); + * ``` + */ +export function compareTimestamps(a, b) { + return Temporal.Instant.compare( + Temporal.Instant.from(a), + Temporal.Instant.from(b), + ); +} diff --git a/src/common/track.js b/src/common/track.js new file mode 100644 index 000000000..b72799af8 --- /dev/null +++ b/src/common/track.js @@ -0,0 +1,30 @@ +import * as URI from "fast-uri"; + +/** + * @import {Track} from "~/definitions/types.d.ts" + */ + +/** + * @param {string} uri + * + */ +export function trackURIBase(uri) { + const p = URI.parse(uri); + p.path = undefined; + p.query = undefined; + return URI.serialize(p); +} + +/** + * @param {Track[]} tracks + * + */ +export function uniqueTrackURIs(tracks) { + const set = new Set(); + + tracks.forEach((t) => { + set.add(trackURIBase(t.uri)); + }); + + return set; +} diff --git a/src/common/utils.js b/src/common/utils.js new file mode 100644 index 000000000..341f9bf0c --- /dev/null +++ b/src/common/utils.js @@ -0,0 +1,288 @@ +import { xxh32r } from "xxh32/dist/raw.js"; + +/** + * @import {Track} from "~/definitions/types.d.ts" + */ + +/** + * @template T + * @param {Array} array + * @returns Array + * + * @example Returns same elements in (possibly different) order without mutating the original + * ```js + * import { arrayShuffle } from "~/common/utils.js"; + * + * if (arrayShuffle([]).length !== 0) throw new Error("empty array should return empty"); + * + * const sorted = arrayShuffle([1, 2, 3, 4, 5]).sort((a, b) => a - b); + * if (sorted.join(",") !== "1,2,3,4,5") throw new Error("shuffled array should contain same elements"); + * + * const original = [1, 2, 3]; + * arrayShuffle(original); + * if (original.join(",") !== "1,2,3") throw new Error("original array should not be mutated"); + * + * if (arrayShuffle([42]).join(",") !== "42") throw new Error("single-element array should be unchanged"); + * ``` + */ +export function arrayShuffle(array) { + if (array.length === 0) { + return []; + } + + array = [...array]; + + for (let index = array.length - 1; index > 0; index--) { + const randArr = crypto.getRandomValues(new Uint32Array(1)); + const randVal = randArr[0] / 2 ** 32; + const newIndex = Math.floor(randVal * (index + 1)); + [array[index], array[newIndex]] = [array[newIndex], array[index]]; + } + + return array; +} + +/** + * @param {string | undefined | null} value + * + * @example Returns true only for empty string (present attribute) + * ```js + * import { boolAttr } from "~/common/utils.js"; + * + * if (!boolAttr("")) throw new Error("empty string should return true"); + * if (boolAttr(null)) throw new Error("null should return false"); + * if (boolAttr(undefined)) throw new Error("undefined should return false"); + * if (boolAttr("true")) throw new Error("non-empty string should return false"); + * ``` + */ +export function boolAttr(value) { + return value === ""; +} + + +/** + * @param {any} object + * + * @example Returns a consistent string hash for the same object + * ```js + * import { hash } from "~/common/utils.js"; + * + * if (typeof hash({ a: 1 }) !== "string") throw new Error("hash should return a string"); + * if (hash({ a: 1, b: 2 }) !== hash({ a: 1, b: 2 })) throw new Error("same objects should produce same hash"); + * if (hash({ a: 1 }) === hash({ a: 2 })) throw new Error("different objects should produce different hashes"); + * ``` + */ +export function hash(object) { + return xxh32r(jsonEncode(object)).toString(); +} + +/** + * @param {Track[]} tracks + * @param {Record} initial + * @returns {Record} + * + * @example Groups tracks by URI scheme + * ```js + * import { groupTracksPerScheme } from "~/common/utils.js"; + * + * const tracks = [ + * { $type: "sh.diffuse.output.track", id: "1", uri: "http://example.com/a.mp3" }, + * { $type: "sh.diffuse.output.track", id: "2", uri: "s3://bucket/b.mp3" }, + * { $type: "sh.diffuse.output.track", id: "3", uri: "http://example.com/c.mp3" }, + * ]; + * // @ts-ignore + * const groups = groupTracksPerScheme(tracks); + * if (groups["http"].length !== 2) throw new Error("expected 2 http tracks"); + * if (groups["s3"].length !== 1) throw new Error("expected 1 s3 track"); + * ``` + * + * @example Merges into a provided initial object + * ```js + * import { groupTracksPerScheme } from "~/common/utils.js"; + * + * const initial = { http: [{ $type: "sh.diffuse.output.track", id: "0", uri: "http://existing.com/x.mp3" }] }; + * const tracks = [{ $type: "sh.diffuse.output.track", id: "1", uri: "http://example.com/a.mp3" }]; + * // @ts-ignore + * const merged = groupTracksPerScheme(tracks, initial); + * if (merged["http"].length !== 2) throw new Error("expected 2 http tracks after merge"); + * ``` + */ +export function groupTracksPerScheme( + tracks, + initial = {}, +) { + /** @type {Record} */ + const acc = initial; + + tracks.forEach((track) => { + const scheme = track.uri.substring(0, track.uri.indexOf(":")); + acc[scheme] ??= []; + acc[scheme].push(track); + }); + + return acc; +} + +/** + * @param {string[]} uris + * @returns {Record} + * + * @example Groups URIs by scheme + * ```js + * import { groupUrisPerScheme } from "~/common/utils.js"; + * + * const groups = groupUrisPerScheme(["http://a.com/t.mp3", "s3://b/t.flac", "http://c.com/t.mp3"]); + * if (groups["http"].length !== 2) throw new Error("expected 2 http URIs"); + * if (groups["s3"].length !== 1) throw new Error("expected 1 s3 URI"); + * + * if (Object.keys(groupUrisPerScheme([])).length !== 0) throw new Error("expected empty object for empty input"); + * ``` + */ +export function groupUrisPerScheme(uris) { + /** @type {Record} */ + const acc = {}; + + uris.forEach((uri) => { + const scheme = uri.substring(0, uri.indexOf(":")); + acc[scheme] ??= []; + acc[scheme].push(uri); + }); + + return acc; +} + +/** + * @param {unknown} test + * + * @example Returns true for primitives and false for objects/arrays + * ```js + * import { isPrimitive } from "~/common/utils.js"; + * + * if (!isPrimitive(42)) throw new Error("number should be primitive"); + * if (!isPrimitive("hello")) throw new Error("string should be primitive"); + * if (!isPrimitive(true)) throw new Error("boolean should be primitive"); + * if (!isPrimitive(null)) throw new Error("null should be primitive"); + * if (isPrimitive({ a: 1 })) throw new Error("object should not be primitive"); + * if (isPrimitive([1, 2])) throw new Error("array should not be primitive"); + * ``` + */ +export function isPrimitive(test) { + return test !== Object(test); +} + +/** + * @template T + * @param {any} a + * @returns {T} + */ +export function jsonDecode(a) { + return JSON.parse(new TextDecoder().decode(a)); +} + +/** + * @template T + * @param {T} a + * @returns Uint8Array + * + * @example jsonEncode returns a Uint8Array that round-trips through jsonDecode + * ```js + * import { jsonEncode, jsonDecode } from "~/common/utils.js"; + * + * const original = { a: 1, b: "hello", c: [1, 2, 3] }; + * if (!(jsonEncode(original) instanceof Uint8Array)) throw new Error("jsonEncode should return a Uint8Array"); + * + * const roundTripped = jsonDecode(jsonEncode(original)); + * if (JSON.stringify(roundTripped) !== JSON.stringify(original)) throw new Error("round-trip should preserve values"); + * ``` + */ +export function jsonEncode(a) { + return new TextEncoder().encode(JSON.stringify(a)); +} + +/** + * @template {Record} T + * @param {T} rec + * + * @example Removes keys with undefined values without mutating the original + * ```js + * import { removeUndefinedValuesFromRecord } from "~/common/utils.js"; + * + * const result = removeUndefinedValuesFromRecord({ a: 1, b: undefined, c: "x" }); + * if ("b" in result) throw new Error("undefined key should be removed"); + * if (result["a"] !== 1 || result["c"] !== "x") throw new Error("defined keys should be preserved"); + * + * const original = { a: 1, b: undefined }; + * removeUndefinedValuesFromRecord(original); + * if (!("b" in original)) throw new Error("original should not be mutated"); + * + * const noUndef = removeUndefinedValuesFromRecord({ a: 1, b: 2 }); + * if (noUndef["a"] !== 1 || noUndef["b"] !== 2) throw new Error("record without undefined values should be unchanged"); + * ``` + */ +export function removeUndefinedValuesFromRecord(rec) { + const recClone = { ...rec }; + + Object.entries(recClone).forEach(([key, value]) => { + if (value === undefined) { + delete recClone[key]; + } + }); + + return recClone; +} + +/** + * @template {Record} T + * @param {T} rec + * + * @example Deep-clones nested records without sharing references + * ```js + * import { recursivelyCloneRecords } from "~/common/utils.js"; + * + * const original = { a: 1, b: "x" }; + * const clone = recursivelyCloneRecords(original); + * if (clone === original) throw new Error("clone should be a different object"); + * if (clone.a !== 1 || clone.b !== "x") throw new Error("values should be preserved"); + * + * const nested = { outer: { inner: 42 } }; + * const nestedClone = recursivelyCloneRecords(nested); + * if (nestedClone.outer === nested.outer) throw new Error("nested objects should not share references"); + * if (nestedClone.outer.inner !== 42) throw new Error("nested values should be preserved"); + * ``` + */ +export function recursivelyCloneRecords(rec) { + const recClone = { ...rec }; + + Object.entries(recClone).forEach(([key, value]) => { + if (typeof value === "object") { + /** @ts-ignore */ + recClone[key] = recursivelyCloneRecords(value); + } + }); + + return recClone; +} + +/** + * @param {string} str + * @returns {string} + * + * @example Decodes percent-encoded and %u unicode escapes, leaves plain strings unchanged + * ```js + * import { safeDecodeURIComponent } from "~/common/utils.js"; + * + * if (safeDecodeURIComponent("hello%20world") !== "hello world") throw new Error("should decode %20 as space"); + * if (safeDecodeURIComponent("%u0041") !== "A") throw new Error("should decode %u unicode escape"); + * if (safeDecodeURIComponent("plain-string") !== "plain-string") throw new Error("plain string should be unchanged"); + * if (safeDecodeURIComponent("hello%2Fworld") !== "hello/world") throw new Error("should decode %2F as slash"); + * ``` + */ +export function safeDecodeURIComponent(str) { + return str.replace( + /%u([0-9A-Fa-f]{4})|%([0-9A-Fa-f]{2})/g, + (_, unicode, byte) => + unicode + ? String.fromCharCode(parseInt(unicode, 16)) + : String.fromCharCode(parseInt(byte, 16)), + ); +} diff --git a/src/common/webawesome/detect-dark.js b/src/common/webawesome/detect-dark.js new file mode 100644 index 000000000..8336a6fb4 --- /dev/null +++ b/src/common/webawesome/detect-dark.js @@ -0,0 +1,13 @@ +const systemDark = window.matchMedia("(prefers-color-scheme: dark)"); + +/** + * @param {MediaQueryListEvent | MediaQueryList} mql + */ +function applyDark(mql) { + const isDark = mql.matches; + document.documentElement.classList.toggle("wa-dark", isDark); +} + +systemDark.addEventListener("change", applyDark); + +applyDark(systemDark); diff --git a/src/common/webawesome/phosphor/_lib.js b/src/common/webawesome/phosphor/_lib.js new file mode 100644 index 000000000..db61945c7 --- /dev/null +++ b/src/common/webawesome/phosphor/_lib.js @@ -0,0 +1,34 @@ +/** + * @param {{ icons: Array<{ icon: { paths: string[], attrs: object[] }, properties: { name: string } }> }} selection + * @returns {Map} + */ +export function buildIconMap(selection) { + const map = new Map(); + for (const icon of selection.icons) { + const { name } = icon.properties; + // Strip weight suffix to get base name (e.g. "gear-bold" → "gear") + // Take only the primary name before any comma-separated aliases + const primaryName = name.split(",")[0].trim(); + const baseName = primaryName.replace(/-(?:bold|fill|light|thin|regular|duotone)$/, ""); + map.set(baseName, { paths: icon.icon.paths, attrs: icon.icon.attrs }); + } + return map; +} + +/** + * @param {string[]} paths + * @param {object[]} attrs + * @returns {string} + */ +export function makeSvgDataUrl(paths, attrs) { + const pathEls = paths + .map((d, i) => { + const a = attrs[i] ?? {}; + const attrStr = Object.entries(a) + .map(([k, v]) => `${k}="${v}"`) + .join(" "); + return attrStr ? `` : ``; + }) + .join(""); + return `data:image/svg+xml,${encodeURIComponent(`${pathEls}`)}`; +} diff --git a/src/common/webawesome/phosphor/bold.js b/src/common/webawesome/phosphor/bold.js new file mode 100644 index 000000000..d2b1118a5 --- /dev/null +++ b/src/common/webawesome/phosphor/bold.js @@ -0,0 +1,15 @@ +import { registerIconLibrary } from "@awesome.me/webawesome/dist/components/icon/library.js"; +import data from "@phosphor-icons/web/bold/selection.json" with { + type: "json", +}; + +import { buildIconMap, makeSvgDataUrl } from "./_lib.js"; + +const map = buildIconMap(data); + +registerIconLibrary("phosphor/bold", { + resolver: (name) => { + const icon = map.get(name); + return icon ? makeSvgDataUrl(icon.paths, icon.attrs) : ""; + }, +}); diff --git a/src/common/webawesome/phosphor/duotone.js b/src/common/webawesome/phosphor/duotone.js new file mode 100644 index 000000000..448b83d6b --- /dev/null +++ b/src/common/webawesome/phosphor/duotone.js @@ -0,0 +1,15 @@ +import { registerIconLibrary } from "@awesome.me/webawesome/dist/components/icon/library.js"; +import data from "@phosphor-icons/web/duotone/selection.json" with { + type: "json", +}; + +import { buildIconMap, makeSvgDataUrl } from "./_lib.js"; + +const map = buildIconMap(data); + +registerIconLibrary("phosphor/duotone", { + resolver: (name) => { + const icon = map.get(name); + return icon ? makeSvgDataUrl(icon.paths, icon.attrs) : ""; + }, +}); diff --git a/src/common/webawesome/phosphor/fill.js b/src/common/webawesome/phosphor/fill.js new file mode 100644 index 000000000..97965f520 --- /dev/null +++ b/src/common/webawesome/phosphor/fill.js @@ -0,0 +1,15 @@ +import { registerIconLibrary } from "@awesome.me/webawesome/dist/components/icon/library.js"; +import data from "@phosphor-icons/web/fill/selection.json" with { + type: "json", +}; + +import { buildIconMap, makeSvgDataUrl } from "./_lib.js"; + +const map = buildIconMap(data); + +registerIconLibrary("phosphor/fill", { + resolver: (name) => { + const icon = map.get(name); + return icon ? makeSvgDataUrl(icon.paths, icon.attrs) : ""; + }, +}); diff --git a/src/common/webawesome/phosphor/light.js b/src/common/webawesome/phosphor/light.js new file mode 100644 index 000000000..d19c52f23 --- /dev/null +++ b/src/common/webawesome/phosphor/light.js @@ -0,0 +1,15 @@ +import { registerIconLibrary } from "@awesome.me/webawesome/dist/components/icon/library.js"; +import data from "@phosphor-icons/web/light/selection.json" with { + type: "json", +}; + +import { buildIconMap, makeSvgDataUrl } from "./_lib.js"; + +const map = buildIconMap(data); + +registerIconLibrary("phosphor/light", { + resolver: (name) => { + const icon = map.get(name); + return icon ? makeSvgDataUrl(icon.paths, icon.attrs) : ""; + }, +}); diff --git a/src/common/webawesome/phosphor/regular.js b/src/common/webawesome/phosphor/regular.js new file mode 100644 index 000000000..f4d6eae59 --- /dev/null +++ b/src/common/webawesome/phosphor/regular.js @@ -0,0 +1,15 @@ +import { registerIconLibrary } from "@awesome.me/webawesome/dist/components/icon/library.js"; +import data from "@phosphor-icons/web/regular/selection.json" with { + type: "json", +}; + +import { buildIconMap, makeSvgDataUrl } from "./_lib.js"; + +const map = buildIconMap(data); + +registerIconLibrary("phosphor/regular", { + resolver: (name) => { + const icon = map.get(name); + return icon ? makeSvgDataUrl(icon.paths, icon.attrs) : ""; + }, +}); diff --git a/src/common/webawesome/phosphor/thin.js b/src/common/webawesome/phosphor/thin.js new file mode 100644 index 000000000..cee273460 --- /dev/null +++ b/src/common/webawesome/phosphor/thin.js @@ -0,0 +1,15 @@ +import { registerIconLibrary } from "@awesome.me/webawesome/dist/components/icon/library.js"; +import data from "@phosphor-icons/web/thin/selection.json" with { + type: "json", +}; + +import { buildIconMap, makeSvgDataUrl } from "./_lib.js"; + +const map = buildIconMap(data); + +registerIconLibrary("phosphor/thin", { + resolver: (name) => { + const icon = map.get(name); + return icon ? makeSvgDataUrl(icon.paths, icon.attrs) : ""; + }, +}); diff --git a/src/common/worker.d.ts b/src/common/worker.d.ts new file mode 100644 index 000000000..b44f8fd71 --- /dev/null +++ b/src/common/worker.d.ts @@ -0,0 +1,60 @@ +export type Announcement = MRpcBaseMsg & { type: "announcement"; args: T }; +export type IncompleteArray = ["Missing required items", T]; + +export type ActionsWithTunnel< + Actions extends Record any>, +> = { + [A in keyof Actions]: WithTunnel; +}; + +export type Dependencies = { + [K in T]: Worker | SharedWorker; +}; + +/** + * Comes from the `@mys/m-rpc` library, + * but it is not exported. Used to identify + * messages sent via `postMessage`. + */ +export type MRpcBaseMsg = { ns: string; name: string; key: number }; + +/** */ +export type ProxiedActions< + Actions extends Record any>, +> = { + [A in keyof Actions]: ProxiedAction; +}; + +export type ProxiedAction< + Action extends (...args: any[]) => any, + PromisedReturn = + (ReturnType extends Promise ? ReturnType + : Promise>), +> = (...args: Parameters) => PromisedReturn; + +/** */ +export interface MessengerRealm { + postMessage: MessagePort["postMessage"]; + addEventListener: MessagePort["addEventListener"]; + removeEventListener: MessagePort["removeEventListener"]; +} + +/** */ +export type Tunnel = { + disconnect: () => void; + port: MessagePort; +}; + +/** */ +export type WithTunnel< + Fn extends (...args: any[]) => any, + PromisedReturn = (ReturnType extends Promise ? ReturnType + : Promise>), +> = ( + _: { data: Parameters[0]; ports: Record }, + ...args: Rest> +) => PromisedReturn; + +// 🛑 + +type Rest = T extends [any, ...(infer R)[]] ? R : never; diff --git a/src/common/worker.js b/src/common/worker.js new file mode 100644 index 000000000..4da58bdb1 --- /dev/null +++ b/src/common/worker.js @@ -0,0 +1,290 @@ +/// +import { getTransferables } from "@okikio/transferables"; +import { debounceMicrotask } from "@vicary/debounce-microtask"; +import { xxh32 } from "xxh32"; + +import { RpcChannel } from "./worker/rpc-channel.js"; + +export { getTransferables } from "@okikio/transferables"; + +/** + * @import {Announcement, MessengerRealm, ProxiedActions, Tunnel} from "./worker.d.ts" + */ + +// Early message buffer for regular Workers. +// +// If a Worker module (or its dependencies) contains a top-level `await`, the +// browser can deliver queued incoming messages to `globalThis` while the module +// evaluation is paused — before `ostiary`/`rpc()` has had a chance to register +// a handler. Those messages would otherwise be silently dropped. +// +// This buffer captures such messages the moment this module is imported (which +// happens before any top-level `await` pause) and replays them once `ostiary` +// sets up the real handler. +// +// Detection: regular Workers are instances of DedicatedWorkerGlobalScope. +// Previously we checked `globalThis.onmessage === null`, but Safari initialises +// that property as `undefined` rather than `null`, causing the check to fail. + +/** @type {MessageEvent[]} */ +const _earlyMessages = []; + +/** @type {null | (() => void)} */ +let _flushEarlyMessages = null; + +if ( + typeof DedicatedWorkerGlobalScope !== "undefined" && + globalThis instanceof DedicatedWorkerGlobalScope +) { + const handler = /** @type {EventListener} */ ((event) => { + _earlyMessages.push(/** @type {MessageEvent} */ (event)); + }); + + globalThis.addEventListener("message", handler); + + _flushEarlyMessages = () => { + globalThis.removeEventListener("message", handler); + }; +} + +//////////////////////////////////////////// +// MISC +//////////////////////////////////////////// + +/** + * Manage incoming connections for a shared worker. + * If a regular worker is used instead, it'll just execute the callback immediately. + * + * @template {MessagePort | Worker | MessengerRealm} T + * @param {(context: MessagePort | T, firstConnection: boolean, connectionId: string) => void} callback + * @param {T} [context] Uses `globalThis` by default. + */ +export function ostiary( + callback, + context = /** @type {T} */ (/** @type {unknown} */ (globalThis)), +) { + if ( + typeof DedicatedWorkerGlobalScope !== "undefined" && + context instanceof DedicatedWorkerGlobalScope + ) { + callback(context, true, crypto.randomUUID()); + + // Replay any messages that arrived before the handler was registered. + if (_flushEarlyMessages) { + _flushEarlyMessages(); + _flushEarlyMessages = null; + const ctx = /** @type {EventTarget} */ (/** @type {unknown} */ (context)); + _earlyMessages.splice(0).forEach((e) => { + ctx.dispatchEvent( + new MessageEvent("message", { data: e.data, ports: [...e.ports] }), + ); + }); + } + + return; + } + + const c = /** @type {any} */ (context); + c.__id ??= crypto.randomUUID(); + + context.addEventListener( + "connect", + /** + * @param {any} event + */ + (event) => { + /** @type {MessagePort} */ + const port = event.ports[0]; + port.start(); + + // Initiate setup + callback(port, !(c.__initiated ?? false), c.__id); + c.__initiated = true; + }, + ); +} + +/** + * @param {Worker | SharedWorker} worker + */ +export function workerLink(worker) { + if (worker instanceof SharedWorker) { + worker.port.start(); + return worker.port; + } else { + return worker; + } +} + +/** + * @template {Record any>} Actions + * @param {() => MessagePort | Worker} workerLinkCreator + * @returns {ProxiedActions} + */ +export function workerProxy(workerLinkCreator) { + /** @type {RpcChannel<{}, Actions> | undefined} */ + let channel; + + const proxy = new Proxy(/** @type {any} */ ({}), { + get: (_target, /** @type {string} */ prop) => { + /** @param {Parameters} args */ + return (...args) => { + channel ??= new RpcChannel(workerLinkCreator()); + return channel.callMethod(prop, args); + }; + }, + }); + + return /** @type {ProxiedActions} */ (proxy); +} + +/** + * @param {() => MessagePort | Worker | SharedWorker} workerCreator + * @param {{ fromWorker?: (message: any) => Promise<{ data: any, transfer?: Transferable[] }>; toWorker?: (message: any) => Promise<{ data: any, transfer?: Transferable[] }> }} [hooks] + * @returns {Tunnel} + */ +export function workerTunnel(workerCreator, hooks = {}) { + /** @type {MessagePort | Worker | undefined} */ + let link; + + const channel = new MessageChannel(); + + function ensureLink() { + if (link) return link; + + const workerOrLink = workerCreator(); + + link = workerOrLink instanceof SharedWorker + ? workerLink(workerOrLink) + : workerOrLink; + + link.addEventListener("message", workerListener); + + return link; + } + + channel.port1.addEventListener("message", async (event) => { + // Send to worker + const { data, transfer } = await hooks?.toWorker?.(event.data) ?? + { data: event.data }; + ensureLink().postMessage(data, { transfer }); + }); + + /** + * @param {Event} event + */ + const workerListener = async (event) => { + // Receive from worker + const msgEvent = /** @type {MessageEvent} */ (event); + const { data, transfer } = await hooks?.fromWorker?.(msgEvent.data) ?? + { data: msgEvent.data }; + channel.port1.postMessage(data, { transfer }); + }; + + channel.port1.start(); + channel.port2.start(); + + return { + disconnect: () => { + link?.removeEventListener("message", workerListener); + channel.port1.close(); + channel.port2.close(); + }, + port: channel.port2, + }; +} + +//////////////////////////////////////////// +// RAW +//////////////////////////////////////////// + +/** + * @template T + * @param {string} name + * @param {T} args + * @param {MessagePort | Worker | MessengerRealm} [context] Uses `globalThis` by default. + */ +export function announce( + name, + args, + context, +) { + const a = announcement(name, args); + const transferables = getTransferables(a); + (context ?? globalThis).postMessage(a, { transfer: transferables }); +} + +/** + * @template T + * @param {string} name + * @param {(args: T) => void} fn + * @param {MessagePort | Worker | MessengerRealm} [context] + */ +export function listen( + name, + fn, + context = /** @type {MessengerRealm} */ (globalThis), +) { + const c = /** @type {any} */ (context); + + if (!c.__incoming) { + context.addEventListener("message", incomingAnnouncementsHandler(context)); + c.__incoming = {}; + } + + c.__incoming[name] = debounceMicrotask(fn, { updateArguments: true }); +} + +//////////////////////////////////////////// +// RPC +//////////////////////////////////////////// + +/** + * @template {Record any>} LocalAPI + * @template {Record any>} RemoteAPI + * @param {MessagePort | Worker | MessengerRealm} context + * @param {RemoteAPI} actions + * @returns {RpcChannel<{}, RemoteAPI>} + */ +export function rpc(context, actions) { + /** @type {RpcChannel<{}, RemoteAPI>} */ + const channel = new RpcChannel(context, { expose: actions }); + return channel; +} + +//////////////////////////////////////////// +// ⛔️ +//////////////////////////////////////////// + +const ANNOUNCEMENT = "announcement"; + +/** + * @template T + * @param {string} name + * @param {T} args + * @returns {Announcement} + */ +function announcement(name, args) { + return { + ns: ANNOUNCEMENT, + name, + key: xxh32(crypto.randomUUID()), + + type: ANNOUNCEMENT, + args, + }; +} + +/** + * @param {MessagePort | Worker | MessengerRealm} context + */ +function incomingAnnouncementsHandler(context) { + /** @param {any} event */ + return (event) => { + const { ns, type } = event.data; + if (ns !== ANNOUNCEMENT || type !== ANNOUNCEMENT) return; + const announcement = /** @type {Announcement} */ (event.data); + const c = /** @type {any} */ (context); + c.__incoming[announcement.name]?.(announcement.args); + }; +} diff --git a/src/common/worker/rpc-channel.js b/src/common/worker/rpc-channel.js new file mode 100644 index 000000000..4cfad2daf --- /dev/null +++ b/src/common/worker/rpc-channel.js @@ -0,0 +1,172 @@ +/** + * @import {MessengerRealm, ProxiedActions} from "../worker.d.ts" + */ + +/** + * Lightweight RPC channel over postMessage using structured clone. + * + * Protocol: + * - Request: { __rpc: true, id, method, args, type: "request" } + * - Response: { __rpc: true, id, type: "response", result } or { ..., error } + * + * @template {Record any>} LocalAPI + * @template {Record any>} RemoteAPI + */ +export class RpcChannel { + /** @type {LocalAPI | undefined} */ + #actions; + + /** @type {Record void, reject: (e: any) => void }>} */ + #pending = {}; + + #port; + + /** @type {(event: MessageEvent) => void} */ + #listener; + + /** + * @param {MessagePort | Worker | MessengerRealm} port + * @param {{ expose?: LocalAPI }} [options] + */ + constructor(port, options) { + this.#port = port; + this.#actions = options?.expose; + + this.#listener = (/** @type {MessageEvent} */ event) => { + const msg = event.data; + if (!msg || msg.__rpc !== true) return; + + if (msg.type === "request") { + if (this.#actions) this.#handleRequest(msg); + } else if (msg.type === "response") { + this.#handleResponse(msg); + } + }; + + port.addEventListener( + "message", + /** @type {EventListener} */ (this.#listener), + ); + } + + /** + * @param {LocalAPI} actions + */ + expose(actions) { + this.#actions = actions; + } + + /** + * @template {keyof RemoteAPI & string} M + * @param {M} method + * @param {any[]} args + * @returns {Promise} + */ + callMethod(method, args) { + return new Promise((resolve, reject) => { + const id = crypto.randomUUID(); + this.#pending[id] = { resolve, reject }; + this.#port.postMessage({ + __rpc: true, + id, + method, + args, + type: "request", + }); + }); + } + + /** + * @returns {ProxiedActions} + */ + getAPI() { + return /** @type {ProxiedActions} */ ( + new Proxy(/** @type {any} */ ({}), { + get: (_target, /** @type {string} */ prop) => { + return (/** @type {any[]} */ ...args) => this.callMethod(prop, args); + }, + }) + ); + } + + destroy() { + this.#port.removeEventListener( + "message", + /** @type {EventListener} */ (this.#listener), + ); + + if ("close" in this.#port && typeof this.#port.close === "function") { + this.#port.close(); + } + + if ( + "terminate" in this.#port && typeof this.#port.terminate === "function" + ) { + /** @type {any} */ (this.#port).terminate(); + } + + // Reject all pending requests + for (const [id, { reject }] of Object.entries(this.#pending)) { + reject(new Error("RPC channel destroyed")); + delete this.#pending[id]; + } + } + + /** + * @param {{ id: string, method: string, args: any[] }} msg + */ + #handleRequest(msg) { + const { id, method, args } = msg; + const fn = this.#actions?.[/** @type {keyof LocalAPI} */ (method)]; + + if (typeof fn !== "function") { + this.#port.postMessage({ + __rpc: true, + id, + type: "response", + error: `Method "${method}" not found`, + }); + return; + } + + try { + Promise.resolve(fn(...args)).then( + (result) => { + this.#port.postMessage({ __rpc: true, id, type: "response", result }); + }, + (err) => { + console.error(err); + this.#port.postMessage({ + __rpc: true, + id, + type: "response", + error: err?.message ?? String(err), + }); + }, + ); + } catch (err) { + console.error(err); + this.#port.postMessage({ + __rpc: true, + id, + type: "response", + error: /** @type {Error} */ (err)?.message ?? String(err), + }); + } + } + + /** + * @param {{ id: string, result?: any, error?: string }} msg + */ + #handleResponse(msg) { + const pending = this.#pending[msg.id]; + if (!pending) return; + delete this.#pending[msg.id]; + + if (msg.error !== undefined) { + pending.reject(new Error(msg.error)); + } else { + pending.resolve(msg.result); + } + } +} diff --git a/src/components/artwork/audio-metadata/element.js b/src/components/artwork/audio-metadata/element.js new file mode 100644 index 000000000..bdce35452 --- /dev/null +++ b/src/components/artwork/audio-metadata/element.js @@ -0,0 +1,61 @@ +import { defineElement, DiffuseElement, query } from "~/common/element.js"; + +/** + * @import {ProxiedActions} from "~/common/worker.d.ts" + * @import {InputElement} from "@specs/components/input/types.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class AudioMetadataArtwork extends DiffuseElement { + static NAME = "diffuse/artwork/audio-metadata"; + static WORKER_URL = "components/artwork/audio-metadata/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const p = this.workerProxy(); + + this.get = p.get; + } + + // LIFECYCLE + + /** @override */ + async connectedCallback() { + super.connectedCallback(); + + /** @type {InputElement} */ + this.input = query(this, "input-selector"); + + await customElements.whenDefined(this.input.localName); + } + + // WORKERS + + /** + * @override + */ + dependencies() { + if (!this.input) throw new Error("Input element not defined yet"); + return { input: this.input }; + } +} + +export default AudioMetadataArtwork; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = AudioMetadataArtwork; +export const NAME = "da-audio-metadata"; + +defineElement(NAME, AudioMetadataArtwork); diff --git a/src/components/artwork/audio-metadata/worker.js b/src/components/artwork/audio-metadata/worker.js new file mode 100644 index 000000000..f2bc026ff --- /dev/null +++ b/src/components/artwork/audio-metadata/worker.js @@ -0,0 +1,65 @@ +import * as URI from "fast-uri"; +import { musicMetadataTags } from "~/components/metadata/common.js"; +import { ostiary, rpc, workerProxy } from "~/common/worker.js"; + +/** + * @import {Extraction} from "@specs/components/metadata/audio-file/types.d.ts" + * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts" + * @import {InputActions} from "@specs/components/input/types.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** + * @type {ActionsWithTunnel['get']} + */ +export async function get({ data: track, ports }) { + /** @type {ProxiedActions} */ + const input = workerProxy(() => { + ports.input.start(); + return ports.input; + }); + + const resGet = await input.resolve({ method: "GET", uri: track.uri }); + if (!resGet) return null; + + const resHead = "stream" in resGet + ? undefined + : await input.resolve({ method: "HEAD", uri: track.uri }); + + const trackUri = URI.parse(track.uri); + const trackPathParts = trackUri.path?.split("/"); + const filename = trackPathParts?.[trackPathParts.length - 1]; + + const meta = await musicMetadataTags({ + filename, + includeArtwork: true, + stream: "stream" in resGet ? resGet.stream : undefined, + mimeType: "stream" in resGet ? resGet.mimeType : undefined, + urls: "url" in resGet + ? { + get: resGet.url, + head: resHead && "url" in resHead ? resHead.url : resGet.url, + } + : undefined, + }).catch(/** @param {Error} err */ (err) => { + console.error("music-metadata error", err); + return /** @type {Extraction} */ ({}); + }); + + const pictures = meta.artwork ?? []; + if (!pictures.length) return null; + + return pictures[0].data; +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { get }); +}); diff --git a/src/components/artwork/input/element.js b/src/components/artwork/input/element.js new file mode 100644 index 000000000..edf3b5356 --- /dev/null +++ b/src/components/artwork/input/element.js @@ -0,0 +1,61 @@ +import { defineElement, DiffuseElement, query } from "~/common/element.js"; + +/** + * @import {ProxiedActions} from "~/common/worker.d.ts" + * @import {InputElement} from "@specs/components/input/types.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class InputArtwork extends DiffuseElement { + static NAME = "diffuse/artwork/input"; + static WORKER_URL = "components/artwork/input/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const p = this.workerProxy(); + + this.get = p.get; + } + + // LIFECYCLE + + /** @override */ + async connectedCallback() { + super.connectedCallback(); + + /** @type {InputElement} */ + this.input = query(this, "input-selector"); + + await customElements.whenDefined(this.input.localName); + } + + // WORKERS + + /** + * @override + */ + dependencies() { + if (!this.input) throw new Error("Input element not defined yet"); + return { input: this.input }; + } +} + +export default InputArtwork; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = InputArtwork; +export const NAME = "da-input"; + +defineElement(NAME, InputArtwork); diff --git a/src/components/artwork/input/worker.js b/src/components/artwork/input/worker.js new file mode 100644 index 000000000..9839bc235 --- /dev/null +++ b/src/components/artwork/input/worker.js @@ -0,0 +1,32 @@ +import { ostiary, rpc, workerProxy } from "~/common/worker.js"; + +/** + * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts" + * @import {InputActions} from "@specs/components/input/types.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** + * @type {ActionsWithTunnel['get']} + */ +export async function get({ data: track, ports }) { + /** @type {ProxiedActions} */ + const input = workerProxy(() => { + ports.input.start(); + return ports.input; + }); + + return await input.artwork(track.uri); +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { get }); +}); diff --git a/src/components/artwork/last.fm/element.js b/src/components/artwork/last.fm/element.js new file mode 100644 index 000000000..609d64dfa --- /dev/null +++ b/src/components/artwork/last.fm/element.js @@ -0,0 +1,38 @@ +import { defineElement, DiffuseElement } from "~/common/element.js"; + +/** + * @import {ProxiedActions} from "~/common/worker.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class LastFmArtwork extends DiffuseElement { + static NAME = "diffuse/artwork/last.fm"; + static WORKER_URL = "components/artwork/last.fm/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const p = this.workerProxy(); + + this.get = p.get; + } +} + +export default LastFmArtwork; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = LastFmArtwork; +export const NAME = "da-lastfm"; + +defineElement(NAME, LastFmArtwork); diff --git a/src/components/artwork/last.fm/worker.js b/src/components/artwork/last.fm/worker.js new file mode 100644 index 000000000..d2c8c0ed1 --- /dev/null +++ b/src/components/artwork/last.fm/worker.js @@ -0,0 +1,59 @@ +import { ostiary, rpc } from "~/common/worker.js"; + +/** + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** + * @type {Actions['get']} + */ +export async function get(track) { + if (!navigator.onLine) return null; + + const query = track.tags?.artist; + if (!query) return null; + + return await fetch( + `https://ws.audioscrobbler.com/2.0/?method=album.search&album=${query}&api_key=4f0fe85b67baef8bb7d008a8754a95e5&format=json`, + ) + .then((r) => r.json()) + .then((r) => findCover(r.results.albummatches.album)) + .catch((err) => { + console.error(err); + return null; + }); +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { get }); +}); + +//////////////////////////////////////////// +// 🛠️ +//////////////////////////////////////////// + +/** + * @param {any[]} remainingMatches + * @returns {Promise} + */ +async function findCover(remainingMatches) { + const album = remainingMatches[0]; + const url = album ? album.image[album.image.length - 1]["#text"] : null; + + return url && url !== "" + ? await fetch(url) + .then((r) => r.blob()) + .then(async (b) => new Uint8Array(await b.arrayBuffer())) + .catch(() => findCover(remainingMatches.slice(1))) + : album + ? findCover(remainingMatches.slice(1)) + : null; +} diff --git a/src/components/artwork/musicbrainz/element.js b/src/components/artwork/musicbrainz/element.js new file mode 100644 index 000000000..0d01914c1 --- /dev/null +++ b/src/components/artwork/musicbrainz/element.js @@ -0,0 +1,38 @@ +import { defineElement, DiffuseElement } from "~/common/element.js"; + +/** + * @import {ProxiedActions} from "~/common/worker.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class MusicBrainzArtwork extends DiffuseElement { + static NAME = "diffuse/artwork/musicbrainz"; + static WORKER_URL = "components/artwork/musicbrainz/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const p = this.workerProxy(); + + this.get = p.get; + } +} + +export default MusicBrainzArtwork; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = MusicBrainzArtwork; +export const NAME = "da-musicbrainz"; + +defineElement(NAME, MusicBrainzArtwork); diff --git a/src/components/artwork/musicbrainz/worker.js b/src/components/artwork/musicbrainz/worker.js new file mode 100644 index 000000000..7f1d5e975 --- /dev/null +++ b/src/components/artwork/musicbrainz/worker.js @@ -0,0 +1,130 @@ +import { ostiary, rpc } from "~/common/worker.js"; + +/** + * @import {Track} from "~/definitions/types.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** + * @type {Actions['get']} + */ +export async function get(track) { + const artist = track.tags?.artist; + const album = track.tags?.album; + + if (!navigator.onLine) return null; + if (!album && !artist) return null; + + const variousArtists = artist?.toUpperCase() === "VA"; + + return search(track, variousArtists); +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { get }); +}); + +//////////////////////////////////////////// +// 🛠️ +//////////////////////////////////////////// + +/** + * @param {string} str + */ +function escapeLucene(str) { + return [].map + .call(str, (char) => { + if ( + char === "+" || + char === "-" || + char === "&" || + char === "|" || + char === "!" || + char === "(" || + char === ")" || + char === "{" || + char === "}" || + char === "[" || + char === "]" || + char === "^" || + char === '"' || + char === "~" || + char === "*" || + char === "?" || + char === ":" || + char === "\\" || + char === "/" + ) { + return "\\" + char; + } else return char; + }) + .join(""); +} + +/** + * @param {Track} track + * @param {boolean} variousArtists + * @returns {Promise} + */ +async function search(track, variousArtists) { + const artist = track.tags?.artist; + const album = track.tags?.album; + + const query = `release:"${escapeLucene(album || "")}"` + + (variousArtists ? `` : ` AND artistname:"${escapeLucene(artist || "")}"`); + const encodedQuery = encodeURIComponent(query); + + return await fetch( + `https://musicbrainz.org/ws/2/release/?query=${encodedQuery}&fmt=json`, + ) + .then((r) => r.json()) + .then((r) => { + if (r.releases.length === 0 && !variousArtists) { + return search(track, true); + } else { + return findCover(r.releases, track, variousArtists); + } + }) + .catch(() => null); +} + +/** + * @param {any[]} remainingReleases + * @param {Track} track + * @param {boolean} variousArtists + * @returns {Promise} + */ +async function findCover(remainingReleases, track, variousArtists) { + const release = remainingReleases[0]; + if (!release) return null; + + const credit = release?.["artist-credit"]?.[0]?.name; + if ( + variousArtists && credit !== "Various Artists" && + credit !== track.tags?.artist + ) return null; + + return await fetch( + `https://coverartarchive.org/release/${release.id}/front-1200`, + ) + .then((r) => r.blob()) + .then(async (b) => { + if (b.type.startsWith("image/")) { + return new Uint8Array(await b.arrayBuffer()); + } else { + return findCover(remainingReleases.slice(1), track, variousArtists); + } + }) + .catch((err) => { + console.error(err); + return findCover(remainingReleases.slice(1), track, variousArtists); + }); +} diff --git a/src/components/configurator/artwork/element.js b/src/components/configurator/artwork/element.js new file mode 100644 index 000000000..81c795b83 --- /dev/null +++ b/src/components/configurator/artwork/element.js @@ -0,0 +1,53 @@ +import { defineElement, DiffuseElement } from "~/common/element.js"; + +/** + * @import {ProxiedActions} from "~/common/worker.d.ts" + * @import {ArtworkElement} from "@specs/components/artwork/types.d.ts" + * @import {Actions} from "@specs/components/configurator/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class ArtworkConfigurator extends DiffuseElement { + static NAME = "diffuse/configurator/artwork"; + static WORKER_URL = "components/configurator/artwork/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const proxy = this.workerProxy(); + + this.get = proxy.get; + } + + // WORKERS + + /** + * @override + */ + dependencies() { + return Object.fromEntries( + Array.from(this.children).map((element) => { + const artwork = /** @type {ArtworkElement} */ (element); + return [artwork.localName, artwork]; + }), + ); + } +} + +export default ArtworkConfigurator; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = ArtworkConfigurator; +export const NAME = "dc-artwork"; + +defineElement(NAME, ArtworkConfigurator); diff --git a/src/components/configurator/artwork/worker.js b/src/components/configurator/artwork/worker.js new file mode 100644 index 000000000..a5e945637 --- /dev/null +++ b/src/components/configurator/artwork/worker.js @@ -0,0 +1,39 @@ +import { ostiary, rpc, workerProxy } from "~/common/worker.js"; + +/** + * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts" + * @import {Actions} from "@specs/components/artwork/types.d.ts" + * @import {Actions as ConfiguratorActions} from "@specs/components/configurator/artwork/types.d.ts" + */ + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** + * @type {ActionsWithTunnel['get']} + */ +export async function get({ data, ports }) { + const track = data; + + for (const port of Object.values(ports)) { + /** @type {ProxiedActions} */ + const artwork = workerProxy(() => { + port.start(); + return port; + }); + + const bytes = await artwork.get(track); + if (bytes !== null) return bytes; + } + + return null; +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { get }); +}); diff --git a/src/components/configurator/input/element.js b/src/components/configurator/input/element.js new file mode 100644 index 000000000..7f2170ac6 --- /dev/null +++ b/src/components/configurator/input/element.js @@ -0,0 +1,71 @@ +import { defineElement, DiffuseElement } from "~/common/element.js"; + +/** + * @import {ProxiedActions, Tunnel} from "~/common/worker.d.ts" + * @import {InputElement} from "@specs/components/input/types.d.ts" + * @import {Actions} from "@specs/components/configurator/input/types.d.ts" + */ + +/** + * @typedef {{ element: InputElement, tunnel: Tunnel, worker: Worker | SharedWorker }} Input + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class InputConfigurator extends DiffuseElement { + static NAME = "diffuse/configurator/input"; + static WORKER_URL = "components/configurator/input/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const proxy = this.workerProxy(); + + this.artwork = proxy.artwork; + this.consult = proxy.consult; + this.detach = proxy.detach; + this.groupConsult = proxy.groupConsult; + this.list = proxy.list; + this.resolve = proxy.resolve; + + this.cache = proxy.cache; + this.cacheBlob = proxy.cacheBlob; + this.listCached = proxy.listCached; + this.removeFromCache = proxy.removeFromCache; + } + + // WORKERS + + /** + * @override + */ + dependencies() { + return this.inputs(); + } + + inputs() { + return Object.fromEntries( + Array.from(this.children).map((element) => { + const input = /** @type {InputElement} */ (element); + return [input.SCHEME, input]; + }), + ); + } +} + +export default InputConfigurator; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = InputConfigurator; +export const NAME = "dc-input"; + +defineElement(NAME, CLASS); diff --git a/src/components/configurator/input/worker.js b/src/components/configurator/input/worker.js new file mode 100644 index 000000000..7ea9e0dcf --- /dev/null +++ b/src/components/configurator/input/worker.js @@ -0,0 +1,272 @@ +import * as IDB from "idb-keyval"; +import * as URI from "fast-uri"; +import * as Cid from "~/common/cid.js"; + +import { + CACHE_KEY_PREFIX, + SCHEME as CACHE_SCHEME, +} from "~/components/input/ephemeral-cache/constants.js"; + +import { groupTracksPerScheme, groupUrisPerScheme } from "~/common/utils.js"; +import { ostiary, rpc, workerProxy } from "~/common/worker.js"; + +/** + * @import {Track} from "~/definitions/types.d.ts"; + * @import {GroupConsult, InputActions} from "@specs/components/input/types.d.ts" + * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts" + * @import {Actions} from "@specs/components/configurator/input/types.d.ts" + */ + +//////////////////////////////////////////// +// INPUT ACTIONS +//////////////////////////////////////////// + +/** + * @type {ActionsWithTunnel['artwork']} + */ +export async function artwork({ data, ports }) { + const uri = data; + const scheme = uri.split(":", 1)[0]; + const input = grabInput(scheme, ports); + if (!input) return null; + + return await input.artwork(uri); +} + +/** + * @type {ActionsWithTunnel['consult']} + */ +export async function consult({ data, ports }) { + const fileUriOrScheme = data; + const scheme = fileUriOrScheme.includes(":") + ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme + : fileUriOrScheme; + + const input = grabInput(scheme, ports); + + if (!input) { + return { supported: false, reason: "Unsupported scheme" }; + } + + return await input.consult(fileUriOrScheme); +} + +/** + * @type {ActionsWithTunnel['detach']} + */ +export async function detach({ data, ports }) { + const currentTracks = data.tracks; + const groups = groupTracks(currentTracks, ports); + + const promises = Object.entries(groups).map( + async ([scheme, tracksGroup]) => { + const input = grabInput(scheme, ports); + if (!input || tracksGroup.length === 0) return tracksGroup; + if ( + data.fileUriOrScheme.includes("://") + ? data.fileUriOrScheme.startsWith(`${scheme}://`) === false + : data.fileUriOrScheme !== scheme + ) return tracksGroup; + + return await input.detach({ + fileUriOrScheme: data.fileUriOrScheme, + tracks: tracksGroup, + }); + }, + ); + + const nested = await Promise.all(promises); + const tracks = nested.flat(1); + + return tracks; +} + +/** + * @type {ActionsWithTunnel['groupConsult']} + */ +export async function groupConsult({ data, ports }) { + const uris = data; + const groups = groupUrisPerScheme(uris); + + /** @type {GroupConsult[]} */ + const consultations = await Promise.all( + Object.keys(groups).map(async (scheme) => { + const input = grabInput(scheme, ports); + + if (!input) { + return { + [scheme]: { + available: "no", + reason: "Unsupported scheme", + scheme, + uris: groups[scheme] ?? [], + }, + }; + } + + return await input.groupConsult(groups[scheme] ?? []); + }), + ); + + return consultations.reduce((acc, c) => { + return Object.assign(acc, c); + }, {}); +} + +/** + * @type {ActionsWithTunnel['list']} + */ +export async function list({ data, ports }) { + const tracks = data; + const uris = tracks.map((/** @type {Track} */ t) => t.uri); + + /** @type {Map} */ + const tracksByUri = new Map( + tracks.map((/** @type {Track} */ t) => [t.uri, t]), + ); + + const groups = await groupConsult({ data: uris, ports }); + + const promises = Object.values(groups).map( + async ({ available, scheme, uris }) => { + const groupTracks = uris + .map((uri) => tracksByUri.get(uri)) + .filter((/** @type {Track | undefined} */ t) => t !== undefined); + + if (!available) return groupTracks; + + const input = grabInput(scheme, ports); + if (!input) return groupTracks; + return await input.list(groupTracks); + }, + ); + + const nested = await Promise.all(promises); + return nested.flat(1); +} + +/** + * @type {ActionsWithTunnel['resolve']} + */ +export async function resolve({ data, ports }) { + const uri = data.uri; + + const scheme = uri.split(":", 1)[0]; + const input = grabInput(scheme, ports); + if (!input) return undefined; + + return await input.resolve(data); +} + +//////////////////////////////////////////// +// ADDITIONAL ACTIONS +//////////////////////////////////////////// + +/** + * @type {ActionsWithTunnel['cache']} + */ +export async function cache({ data, ports }) { + const uris = data; + + await Promise.all(uris.map(async (uri) => { + if (await IDB.get(CACHE_KEY_PREFIX + uri) !== undefined) return; + + const resolved = await resolve({ data: { uri }, ports }); + if (!resolved || "stream" in resolved) return; + + const response = await fetch(resolved.url); + if (!response.ok) return; + + await IDB.set(CACHE_KEY_PREFIX + uri, await response.blob()); + })); +} + +/** + * @type {ActionsWithTunnel['listCached']} + */ +export async function listCached() { + const keys = /** @type {string[]} */ (await IDB.keys()); + return keys + .filter((k) => k.startsWith(CACHE_KEY_PREFIX)) + .map((k) => k.slice(CACHE_KEY_PREFIX.length)); +} + +/** + * @type {ActionsWithTunnel['removeFromCache']} + */ +export async function removeFromCache({ data }) { + const uris = data; + + await Promise.all(uris.map((uri) => IDB.del(CACHE_KEY_PREFIX + uri))); +} + +/** + * @type {ActionsWithTunnel['cacheBlob']} + */ +export async function cacheBlob({ data }) { + const blob = data; + const buffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(buffer); + const cid = await Cid.create(0x55, bytes); + const uri = `${CACHE_SCHEME}://${cid}`; + if (await IDB.get(CACHE_KEY_PREFIX + uri) === undefined) { + await IDB.set(CACHE_KEY_PREFIX + uri, blob); + } + return uri; +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { + artwork, + consult, + detach, + groupConsult, + list, + resolve, + + cache, + cacheBlob, + listCached, + removeFromCache, + }); +}); + +//////////////////////////////////////////// +// 🛠️ +//////////////////////////////////////////// + +/** + * @param {string} scheme + * @param {Record} ports + * @returns {ProxiedActions | null} + */ +function grabInput(scheme, ports) { + const port = ports[scheme]; + if (!port) return null; + + return workerProxy(() => { + port.start(); + return port; + }); +} + +/** + * @param {Track[]} tracks + * @param {Record} ports + */ +function groupTracks(tracks, ports) { + const grouped = groupTracksPerScheme( + tracks, + Object.fromEntries( + Object.keys(ports).map((k) => { + return [k, []]; + }), + ), + ); + + return grouped; +} diff --git a/src/components/configurator/metadata/element.js b/src/components/configurator/metadata/element.js new file mode 100644 index 000000000..43ff3ee88 --- /dev/null +++ b/src/components/configurator/metadata/element.js @@ -0,0 +1,53 @@ +import { defineElement, DiffuseElement } from "~/common/element.js"; + +/** + * @import {ProxiedActions} from "~/common/worker.d.ts" + * @import {MetadataElement} from "@specs/components/metadata/types.d.ts" + * @import {Actions} from "@specs/components/configurator/metadata/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class MetadataConfigurator extends DiffuseElement { + static NAME = "diffuse/configurator/metadata"; + static WORKER_URL = "components/configurator/metadata/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const proxy = this.workerProxy(); + + this.patch = proxy.patch; + } + + // WORKERS + + /** + * @override + */ + dependencies() { + return Object.fromEntries( + Array.from(this.children).map((element) => { + const metadata = /** @type {MetadataElement} */ (element); + return [metadata.localName, metadata]; + }), + ); + } +} + +export default MetadataConfigurator; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = MetadataConfigurator; +export const NAME = "dc-metadata"; + +defineElement(NAME, MetadataConfigurator); diff --git a/src/components/configurator/metadata/worker.js b/src/components/configurator/metadata/worker.js new file mode 100644 index 000000000..7e6611d3c --- /dev/null +++ b/src/components/configurator/metadata/worker.js @@ -0,0 +1,38 @@ +import { ostiary, rpc, workerProxy } from "~/common/worker.js"; + +/** + * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts" + * @import {Actions} from "@specs/components/metadata/types.d.ts" + * @import {Actions as ConfiguratorActions} from "@specs/components/configurator/metadata/types.d.ts" + */ + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** + * @type {ActionsWithTunnel['patch']} + */ +export async function patch({ data: track, ports }) { + let result = track; + + for (const port of Object.values(ports)) { + /** @type {ProxiedActions} */ + const metadata = workerProxy(() => { + port.start(); + return port; + }); + + result = await metadata.patch(result); + } + + return result; +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { patch }); +}); diff --git a/src/components/configurator/output/element.js b/src/components/configurator/output/element.js new file mode 100644 index 000000000..c18db5a89 --- /dev/null +++ b/src/components/configurator/output/element.js @@ -0,0 +1,480 @@ +import { BroadcastableDiffuseElement, defineElement } from "~/common/element.js"; +import { batch, computed, signal } from "~/common/signal.js"; + +import { PRELUDE_BASE } from "~/common/facets/constants.js"; +import { buildFacets } from "~/common/facets/utils.js"; + +/** + * @import {DiffuseElement} from "~/common/element.js" + * @import {Facet, PlaylistItem, Setting, Track} from "~/definitions/types.d.ts" + * @import {OutputManagerDeputy, OutputElement} from "@specs/components/output/types.d.ts" + * + * @import {OutputConfiguratorElement} from "@specs/components/configurator/output/types.d.ts" + */ + +/** + * @typedef {OutputElement} Output + */ + +const STORAGE_PREFIX = "diffuse/configurator/output"; + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {OutputConfiguratorElement} + */ +class OutputConfigurator extends BroadcastableDiffuseElement { + static NAME = "diffuse/configurator/output"; + + constructor() { + super(); + + /** @type {OutputManagerDeputy} */ + const manager = { + facets: { + collection: computed(() => { + const out = this.#selected.value; + if (out) return out.facets.collection(); + + const def = this.#defaultOutput.value; + if (def) { + const col = def.facets.collection(); + + // Recover from the bootstrap deadlock: a non-default output is + // selected (but its elements aren't created yet, hence `out` is + // null) and the local bucket has no prelude facets to inject the + // output-bundle prelude from. Seed the base infrastructure + // preludes so the page can bootstrap the selected output. + if (col.state === "loaded" && this.#needsPreludeRecovery(col.data)) { + return { + state: "loaded", + data: [...col.data, ...buildFacets(PRELUDE_BASE)], + }; + } + + return col; + } + + if (this.hasDefault()) return { state: "loading" }; + + return this.#setupFinished.value + ? { state: "loaded", data: this.#memory.facets.value } + : { state: "loading" }; + }), + reload: () => { + const def = this.#defaultOutput.value; + if (def) def.facets.reload(); + + const out = this.#selected.value; + if (out) return out.facets.reload(); + + return Promise.resolve(); + }, + save: async (newFacets) => { + const out = this.#selected.value; + if (out) return await out.facets.save(newFacets); + + const def = this.#defaultOutput.value; + if (def) return await def.facets.save(newFacets); + + this.#memory.facets.value = newFacets; + }, + }, + playlistItems: { + collection: computed(() => { + const out = this.#selected.value; + if (out) return out.playlistItems.collection(); + + const def = this.#defaultOutput.value; + if (def) return def.playlistItems.collection(); + if (this.hasDefault()) return { state: "loading" }; + + return this.#setupFinished.value + ? { state: "loaded", data: this.#memory.playlistItems.value } + : { state: "loading" }; + }), + reload: () => { + const def = this.#defaultOutput.value; + if (def) def.playlistItems.reload(); + + const out = this.#selected.value; + if (out) return out.playlistItems.reload(); + + return Promise.resolve(); + }, + save: async (newPlaylistItems) => { + const out = this.#selected.value; + if (out) return await out.playlistItems.save(newPlaylistItems); + + const def = this.#defaultOutput.value; + if (def) return await def.playlistItems.save(newPlaylistItems); + + this.#memory.playlistItems.value = newPlaylistItems; + }, + }, + settings: { + collection: computed(() => { + const out = this.#selected.value; + if (out) return out.settings.collection(); + + const def = this.#defaultOutput.value; + if (def) return def.settings.collection(); + if (this.hasDefault()) return { state: "loading" }; + + return this.#setupFinished.value + ? { state: "loaded", data: this.#memory.settings.value } + : { state: "loading" }; + }), + reload: () => { + const def = this.#defaultOutput.value; + if (def) def.settings.reload(); + + const out = this.#selected.value; + if (out) return out.settings.reload(); + + return Promise.resolve(); + }, + save: async (newSettings) => { + const out = this.#selected.value; + if (out) return await out.settings.save(newSettings); + + const def = this.#defaultOutput.value; + if (def) return await def.settings.save(newSettings); + + this.#memory.settings.value = newSettings; + }, + }, + tracks: { + collection: computed(() => { + const out = this.#selected.value; + if (out) return out.tracks.collection(); + + const def = this.#defaultOutput.value; + if (def) return def.tracks.collection(); + if (this.hasDefault()) return { state: "loading" }; + + return this.#setupFinished.value + ? { state: "loaded", data: this.#memory.tracks.value } + : { state: "loading" }; + }), + reload: () => { + const def = this.#defaultOutput.value; + if (def) def.tracks.reload(); + + const out = this.#selected.value; + if (out) return out.tracks.reload(); + + return Promise.resolve(); + }, + save: async (newTracks) => { + const out = this.#selected.value; + if (out) return await out.tracks.save(newTracks); + + const def = this.#defaultOutput.value; + if (def) return await def.tracks.save(newTracks); + + this.#memory.tracks.value = newTracks; + }, + }, + + // Other + ready: computed(() => { + const out = this.#selected.value; + if (out) return out.ready(); + + const def = this.#defaultOutput.value; + if (def) return def.ready(); + + return this.#setupFinished.value; + }), + }; + + // Assign manager properties to class + this.facets = manager.facets; + this.playlistItems = manager.playlistItems; + this.settings = manager.settings; + this.tracks = manager.tracks; + this.ready = manager.ready; + + // Effects + + /** + * When there's a selected output and its collection changes, + * save it to the default output or memory. + */ + this.effect(() => { + const out = this.#selected.value; + if (!out) return; + + const col = out.facets.collection(); + if (col.state !== "loaded") return; + + const def = this.#defaultOutput.value; + if (def) def.facets.save(col.data); + else this.#memory.facets.set(col.data); + }); + + this.effect(() => { + const out = this.#selected.value; + if (!out) return; + + const col = out.playlistItems.collection(); + if (col.state !== "loaded") return; + + const def = this.#defaultOutput.value; + if (def) def.playlistItems.save(col.data); + else this.#memory.playlistItems.set(col.data); + }); + + this.effect(() => { + const out = this.#selected.value; + if (!out) return; + + const col = out.settings.collection(); + if (col.state !== "loaded") return; + + const def = this.#defaultOutput.value; + if (def) def.settings.save(col.data); + else this.#memory.settings.set(col.data); + }); + + this.effect(() => { + const out = this.#selected.value; + if (!out) return; + + const col = out.tracks.collection(); + if (col.state !== "loaded") return; + + const def = this.#defaultOutput.value; + if (def) def.tracks.save(col.data); + else this.#memory.tracks.set(col.data); + }); + } + + // SIGNALS + + #activated = signal(/** @type {Set} */ (new Set())); + + #defaultOutput = signal( + /** @type {Output | null | undefined} */ (undefined), + ); + + #memory = { + facets: signal(/** @type {Facet[]} */ ([])), + playlistItems: signal(/** @type {PlaylistItem[]} */ ([])), + settings: signal(/** @type {Setting[]} */ ([])), + tracks: signal(/** @type {Track[]} */ ([])), + }; + + #selected = signal( + /** @type {Output | null | undefined} */ (undefined), + ); + + #setupFinished = signal(false); + + // STATE + + activated = this.#activated.get; + selected = computed(() => this.#selected.value ?? null); + + // LIFECYCLE + + /** + * @override + */ + async connectedCallback() { + // Broadcast if needed + if (this.hasAttribute("group")) { + const actions = this.broadcast(this.identifier, { + selectOutput: { + strategy: "replicate", + fn: this.#selectOutput, + }, + }); + + if (actions) { + this.#selectOutput = actions.selectOutput; + } + } + + // Super + super.connectedCallback(); + + // Outputs + const def_ault = this.getAttribute("default"); + const selectedOutputId = this.#selectedOutputId(); + + batch(() => { + /** @type {Set} */ + const activated = new Set(); + + if (def_ault) { + activated.add(def_ault); + } + + if (selectedOutputId) { + activated.add(selectedOutputId); + } + + this.#activated.value = activated; + }); + + /** @type {Output | null} */ + const defaultOutput = def_ault ? await this.#findOutput(def_ault) : null; + const selectedOutput = await this.#findOutput(selectedOutputId); + + batch(() => { + this.#selected.value = selectedOutput; + this.#defaultOutput.value = defaultOutput; + this.#setupFinished.value = true; + }); + } + + // MISC + + /** + * @param {string | null} id + */ + async #findOutput(id) { + const el = id ? this.root().querySelector(`#${id}`) : null; + if (!el) return null; + + await customElements.whenDefined(el.localName); + + if ( + "identifier" in el === false || + "tracks" in el === false + ) { + return null; + } + + return /** @type {Output} */ (/** @type {unknown} */ (el)); + } + + /** + * Whether the local bucket needs the base infrastructure preludes seeded + * into the fallback. + * + * @param {Facet[]} localFacets + * @returns {boolean} + */ + #needsPreludeRecovery(localFacets) { + const selectedId = this.#selectedOutputId(); + const defaultId = this.getAttribute("default"); + + // Only a non-default selection depends on the prelude to be created. + if (!selectedId || selectedId === defaultId) return false; + + // If the local bucket already has preludes, the normal path works. + return !localFacets.some((f) => f.kind === "prelude"); + } + + /** + * @param {string | null} id + */ + #selectOutput = async (id) => { + if (id) { + this.#activated.value = new Set([...this.#activated.value.values(), id]); + } + + this.#selected.value = await this.#findOutput(id); + }; + + #selectedOutputId() { + return localStorage.getItem(`${STORAGE_PREFIX}/selected/id`); + } + + /** + * @override + */ + dependencies = () => { + return Object.fromEntries( + Array.from(this.root().children).flatMap((element) => { + if (element.hasAttribute("id") === false) { + console.warn( + "Missing `id` for output configurator child element with `localName` '" + + element.localName + "'", + ); + return []; + } + + const d = /** @type {DiffuseElement} */ (element); + return [[d.id, d]]; + }), + ); + }; + + // ADDITIONAL ACTIONS + + deselect = async () => { + localStorage.removeItem(`${STORAGE_PREFIX}/selected/id`); + await this.#selectOutput(null); + }; + + hasDefault() { + return this.hasAttribute("default"); + } + + hasSelected() { + return this.#selectedOutputId() !== null; + } + + loadSelected = async () => { + const selectedOutput = await this.#findOutput(this.#selectedOutputId()); + this.#selected.value = selectedOutput; + }; + + options = async () => { + const deps = this.dependencies(); + const entries = Object.entries(deps); + + return entries.map(([k, v]) => { + return { + id: k, + label: v.label ?? v.getAttribute("label"), + element: /** @type {OutputElement} */ (v), + }; + }); + }; + + /** + * @param {string} id + */ + select = async (id) => { + localStorage.setItem(`${STORAGE_PREFIX}/selected/id`, id); + await this.#selectOutput(id); + }; + + /** + * @param {string} label + * @returns {Promise<{ id: string, label: string, element: OutputElement }>} + */ + waitForOption = (label) => { + return new Promise((resolve) => { + const check = async () => { + const opt = (await this.options()).find((o) => o.label === label); + if (opt) { + observer.disconnect(); + resolve(opt); + } + }; + + const observer = new MutationObserver(check); + observer.observe(this, { childList: true }); + check(); + }); + }; +} + +export default OutputConfigurator; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = OutputConfigurator; +export const NAME = "dc-output"; + +defineElement(NAME, CLASS); diff --git a/src/components/configurator/scrobbles/element.js b/src/components/configurator/scrobbles/element.js new file mode 100644 index 000000000..10aad871b --- /dev/null +++ b/src/components/configurator/scrobbles/element.js @@ -0,0 +1,73 @@ +import { defineElement, DiffuseElement } from "~/common/element.js"; + +/** + * @import {Track} from "~/definitions/types.d.ts" + * @import {ScrobbleActions, ScrobbleElement} from "@specs/components/supplement/types.d.ts" + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ScrobbleActions} + */ +class ScrobblesConfigurator extends DiffuseElement { + static NAME = "diffuse/configurator/scrobbles"; + + // SCROBBLE ACTIONS + + /** + * @param {Track} track + */ + async nowPlaying(track) { + return await Promise.all( + this.#activeScrobblers().map((s) => s.nowPlaying(track)), + ); + } + + /** + * @param {Track} track + * @param {number} startedAt Unix timestamp in milliseconds + * @param {{ duration?: number }} [options] duration in milliseconds + */ + async scrobble(track, startedAt, options) { + return await Promise.all( + this.#activeScrobblers().map((s) => s.scrobble(track, startedAt, options)), + ); + } + + // MISC + + /** + * All child scrobble elements, regardless of authentication state. + * + * @returns {ScrobbleElement[]} + */ + scrobblers() { + return Array.from(this.root().children).flatMap((el) => { + if (!("isAuthenticated" in el && "nowPlaying" in el)) return []; + return [/** @type {ScrobbleElement} */ (/** @type {unknown} */ (el))]; + }); + } + + /** + * Child scrobble elements that are currently authenticated. + * + * @returns {ScrobbleElement[]} + */ + #activeScrobblers() { + return this.scrobblers().filter((s) => s.isAuthenticated()); + } +} + +export default ScrobblesConfigurator; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = ScrobblesConfigurator; +export const NAME = "dc-scrobbles"; + +defineElement(NAME, CLASS); diff --git a/src/components/configurator/upload/element.js b/src/components/configurator/upload/element.js new file mode 100644 index 000000000..cfe258250 --- /dev/null +++ b/src/components/configurator/upload/element.js @@ -0,0 +1,64 @@ +import { defineElement, DiffuseElement } from "~/common/element.js"; + +/** + * @import {ProxiedActions, Tunnel} from "~/common/worker.d.ts" + * @import {UploadElement} from "@specs/components/upload/types.d.ts" + * @import {Actions} from "@specs/components/configurator/upload/types.d.ts" + */ + +/** + * @typedef {{ element: UploadElement, tunnel: Tunnel, worker: Worker | SharedWorker }} Upload + */ + +//////////////////////////////////////////// +// ELEMENT +//////////////////////////////////////////// + +/** + * @implements {ProxiedActions} + */ +class UploadConfigurator extends DiffuseElement { + static NAME = "diffuse/configurator/upload"; + static WORKER_URL = "components/configurator/upload/worker.js"; + + constructor() { + super(); + + /** @type {ProxiedActions} */ + const proxy = this.workerProxy(); + + this.consult = proxy.consult; + this.upload = proxy.upload; + this.delete = proxy.delete; + this.createSource = proxy.createSource; + } + + // WORKERS + + /** + * @override + */ + dependencies() { + return this.uploaders(); + } + + uploaders() { + return Object.fromEntries( + Array.from(this.children).map((element) => { + const upload = /** @type {UploadElement} */ (element); + return [upload.SCHEME, upload]; + }), + ); + } +} + +export default UploadConfigurator; + +//////////////////////////////////////////// +// REGISTER +//////////////////////////////////////////// + +export const CLASS = UploadConfigurator; +export const NAME = "dc-upload"; + +defineElement(NAME, CLASS); diff --git a/src/components/configurator/upload/worker.js b/src/components/configurator/upload/worker.js new file mode 100644 index 000000000..1c063b6d2 --- /dev/null +++ b/src/components/configurator/upload/worker.js @@ -0,0 +1,100 @@ +import * as URI from "fast-uri"; +import { ostiary, rpc, workerProxy } from "~/common/worker.js"; + +/** + * @import {UploadActions} from "@specs/components/upload/types.d.ts" + * @import {ActionsWithTunnel, ProxiedActions} from "~/common/worker.d.ts" + * @import {Actions} from "@specs/components/configurator/upload/types.d.ts" + */ + +//////////////////////////////////////////// +// ACTIONS +//////////////////////////////////////////// + +/** + * @type {ActionsWithTunnel['consult']} + */ +export async function consult({ data, ports }) { + const fileUriOrScheme = data; + const scheme = fileUriOrScheme.includes(":") + ? URI.parse(fileUriOrScheme).scheme || fileUriOrScheme + : fileUriOrScheme; + + const upload = grabUploader(scheme, ports); + if (!upload) { + return { supported: false, reason: "Unsupported scheme" }; + } + + return await upload.consult(fileUriOrScheme); +} + +/** + * @type {ActionsWithTunnel['createSource']} + */ +export async function createSource({ data, ports }) { + const upload = grabUploader(data.scheme, ports); + if (!upload) { + throw new Error(`Unsupported scheme: ${data.scheme}`); + } + + return await upload.createSource(data); +} + +/** + * @type {ActionsWithTunnel['delete']} + */ +export async function deleteFn({ data, ports }) { + const uri = data; + const scheme = uri.split(":", 1)[0]; + const upload = grabUploader(scheme, ports); + if (!upload) { + throw new Error(`Unsupported scheme: ${scheme}`); + } + + await upload.delete(uri); +} + +/** + * @type {ActionsWithTunnel['upload']} + */ +export async function upload({ data, ports }) { + const scheme = data.uri.split(":", 1)[0]; + const upload = grabUploader(scheme, ports); + if (!upload) { + throw new Error(`Unsupported scheme: ${scheme}`); + } + + return await upload.upload(data); +} + +//////////////////////////////////////////// +// ⚡️ +//////////////////////////////////////////// + +ostiary((context) => { + rpc(context, { + consult, + upload, + delete: deleteFn, + createSource, + }); +}); + +//////////////////////////////////////////// +// 🛠️ +//////////////////////////////////////////// + +/** + * @param {string} scheme + * @param {Record} ports + * @returns {ProxiedActions | null} + */ +function grabUploader(scheme, ports) { + const port = ports[scheme]; + if (!port) return null; + + return workerProxy(() => { + port.start(); + return port; + }); +} diff --git a/src/components/engine/audio/element.js b/src/components/engine/audio/element.js new file mode 100644 index 000000000..0d37a07af --- /dev/null +++ b/src/components/engine/audio/element.js @@ -0,0 +1,1208 @@ +import { keyed } from "lit-html/directives/keyed.js"; + +import { + BroadcastableDiffuseElement, + defineElement, + nothing, +} from "~/common/element.js"; +import { computed, signal, untracked } from "~/common/signal.js"; + +/** + * @import {Actions, AudioUrl, AudioState, AudioStateReadOnly, LoadingState} from "@specs/components/engine/audio/types.d.ts" + * @import {RenderArg} from "~/common/element.d.ts" + * @import {SignalReader} from "~/common/signal.d.ts" + */ + +//////////////////////////////////////////// +// CONSTANTS +//////////////////////////////////////////// +/** + * Mobile Safari caps the number of live media elements and behaves poorly + * when fresh