diff --git a/.eslintignore b/.eslintignore index 58125c0f6..eb93a38c3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,4 @@ node_modules dist .vscode -build +.eslintrc.cjs diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 94% rename from .eslintrc.js rename to .eslintrc.cjs index 95fe00c90..374b50a9c 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -1,5 +1,8 @@ /* eslint-env node */ module.exports = { + env: { + node: true, + }, root: true, extends: [ 'eslint:recommended', @@ -19,7 +22,7 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { project: true, - project: ['./tsconfig.json', './src/*/tsconfig.json'], + project: ['./tsconfig.json'], tsconfigRootDir: __dirname, }, settings: { react: { version: 'detect' } }, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d779c38e1..818466bde 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,31 +17,33 @@ concurrency: jobs: qa: - # Let's implement different jobs for Windows and Linux at some point, especially for packaging scripts - runs-on: ubuntu-latest + # For QA (lint, test, etc), Linux is enough + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 20 - - name: Display Node.js and npm informations + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: install dependencies (ubuntu only) run: | - echo "node version $(node -v) running" - echo "npm version $(npm -v) running" + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf - name: Install dependencies run: yarn install --frozen-lockfile + - name: Generate types + run: cargo test --manifest-path src-tauri/Cargo.toml + - name: 'Test: formatting' run: 'yarn run test:formatting' - - name: 'Test: typings' - run: 'yarn run test:typings' - - name: 'Test: TS/JS linting' run: 'yarn run test:lint' @@ -51,138 +53,52 @@ jobs: - name: 'Test: unit' run: 'yarn run test:unit' + - name: 'Test: typings' + run: 'yarn run test:typings' + - name: Build application run: yarn run build - - uses: actions/upload-artifact@v3 - with: - name: application-build - path: dist/ + # - uses: actions/upload-artifact@v4 + # with: + # name: application-build + # path: dist/ # Documentation on environments: # https://docs.github.com/en/free-pro-team@latest/actions/reference/specifications-for-github-hosted-runners - binaries-linux: - runs-on: ubuntu-latest - needs: [qa] + binaries: + permissions: + contents: write + strategy: + fail-fast: false + matrix: + platform: [macos-latest, ubuntu-22.04, windows-latest] + runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v3 + - name: setup node + uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 20 - - name: Display Node.js and npm informations - run: | - echo "node version $(node -v) running" - echo "npm version $(npm -v) running" - - - uses: actions/download-artifact@v3 - with: - name: application-build - path: dist/ + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable - - name: Install Linux dependencies + - name: install dependencies (ubuntu only) + if: matrix.platform == 'ubuntu-22.04' run: | - sudo apt update - sudo apt install --no-install-recommends -y libopenjp2-tools rpm gcc-multilib g++-multilib + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - name: Install dependencies + - name: install frontend dependencies run: yarn install --frozen-lockfile - - name: Package Linux binaries - # - name: Package Linux/Windows binaries - run: yarn run package:l - # run: yarn run package:lw + - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/upload-artifact@v3 - with: - name: binaries-linux - path: | - build/museeks-linux-i386.deb - build/museeks-linux-amd64.deb - build/museeks-linux-i386.AppImage - build/museeks-linux-x86_64.AppImage - build/museeks-linux-i686.rpm - build/museeks-linux-x86_64.rpm - build/museeks-linux-x64.tar.gz - - binaries-macos: - runs-on: macos-latest - needs: [qa] - - steps: - - uses: actions/checkout@v3 - - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 20.x - - - name: Display Node.js and npm informations - run: | - echo "node version $(node -v) running" - echo "npm version $(npm -v) running" - - - uses: actions/download-artifact@v3 - with: - name: application-build - path: dist/ - - - name: Install production dependencies - run: yarn install --frozen-lockfile - - - name: Package macOS binaries - run: yarn run package:m - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - - - uses: actions/upload-artifact@v3 - with: - name: binaries-macos - path: | - build/museeks-macos-arm64.dmg - build/museeks-macos-x64.dmg - - binaries-windows: - runs-on: windows-latest - needs: [qa] - - steps: - - uses: actions/checkout@v3 - - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 20.x - - - name: Display Node.js and npm informations - run: | - echo "node version $(node -v) running" - echo "npm version $(npm -v) running" - - - uses: actions/download-artifact@v3 - with: - name: application-build - path: dist/ - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Package Windows binaries - run: yarn run package:w - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/upload-artifact@v3 with: - name: binaries-windows - path: | - build/museeks-win-x64-setup.exe - build/museeks-win-x64-portable.exe + includeRelease: false + includeUpdaterJson: false diff --git a/.gitignore b/.gitignore index e73512da9..b35acca81 100644 --- a/.gitignore +++ b/.gitignore @@ -4,65 +4,26 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -# Runtime data -pids -*.pid -*.seed -*.pid.lock +node_modules +dist +dist-ssr +*.local -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -dist/main/* -dist/renderer/* -dist/preload/* -cache/ -build/ -.vscode +# Editor directories and files +.vscode/* !.vscode/extensions.json - +.idea .DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Generated files +src/generated/typings/* +!src/generated/typings/index.ts +src-tauri/gen diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6167b92e0..53d3031e5 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,10 @@ { "recommendations": [ "dbaeumer.vscode-eslint", - "csstools.postcss", + "vunguyentuan.vscode-postcss", "esbenp.prettier-vscode", "stylelint.vscode-stylelint", - "ZixuanChen.vitest-explorer" + "tauri-apps.tauri-vscode", + "rust-lang.rust-analyzer" ] } diff --git a/electron-builder.yml b/electron-builder.yml deleted file mode 100644 index c397bcc25..000000000 --- a/electron-builder.yml +++ /dev/null @@ -1,68 +0,0 @@ -appId: io.museeks.app -directories: - buildResources: './dist' - output: './build' -files: - - dist/**/* - - src/shared/assets/**/* - - 'node_modules/**/*' - - '!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}' - - '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}' - - '!**/node_modules/*.d.ts' - - '!**/node_modules/.bin' - -# fileAssociations: -# ext: -# - mp3 -# - mp4 -# - aac -# - m4a -# - 3gp -# - wav -# - ogg -# - ogv -# - ogm -# - opus -# - flac -# role: Viewer -mac: - category: public.app-category.music - target: - - target: dmg - arch: - - x64 - - arm64 - icon: src/shared/assets/logos/museeks.icns - artifactName: ${name}-macos-${arch}.${ext} - darkModeSupport: true -linux: - category: AudioVideo - target: - - target: deb - arch: - - x64 - - target: AppImage - arch: - - x64 - - target: rpm - arch: - - x64 - - target: tar.gz - arch: - - x64 - executableName: museeks - artifactName: ${name}-linux-${arch}.${ext} -deb: - depends: ['libdbus-1-dev', 'libglib2.0-dev'] -win: - target: - - target: nsis - arch: - - x64 - - target: portable - arch: - - x64 - icon: src/images/logos/museeks.ico - artifactName: ${name}-win-${arch}-setup.${ext} -portable: - artifactName: ${name}-win-${arch}-portable.${ext} diff --git a/electron.vite.config.ts b/electron.vite.config.ts deleted file mode 100644 index b1c632c9d..000000000 --- a/electron.vite.config.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; -import react from '@vitejs/plugin-react'; - -const externals = ['fs', 'electron', 'globby', 'queue', 'music-metadata']; -const minify = process.env.NODE_ENV === 'production'; - -const commonNodeConfig = { - minify, - target: 'node18', - sourcemap: true, - emptyOutDir: true, -}; - -export default defineConfig({ - main: { - plugins: [externalizeDepsPlugin({ exclude: externals })], - build: { - ...commonNodeConfig, - outDir: 'dist/main', - - lib: { - entry: './src/main/entrypoint.ts', - }, - }, - }, - preload: { - plugins: [externalizeDepsPlugin({ exclude: externals })], - build: { - ...commonNodeConfig, - outDir: 'dist/preload', - lib: { - entry: './src/preload/entrypoint.ts', - }, - }, - }, - renderer: { - plugins: [react()], - appType: 'spa', - build: { - minify, - sourcemap: true, - emptyOutDir: true, - outDir: 'dist/renderer', - }, - }, -}); diff --git a/index.html b/index.html new file mode 100644 index 000000000..4b9f8a550 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + Museeks + + + + +
+ +
+ + diff --git a/package.json b/package.json index 12e416e95..6018bc542 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "museeks", "productName": "Museeks", - "version": "0.14.0", + "version": "0.20.0", "description": "A simple, clean and cross-platform music player.", - "main": "dist/main/entrypoint.js", + "type": "module", "repository": { "type": "git", "url": "https://github.com/martpie/museeks" @@ -14,99 +14,83 @@ "url": "https://github.com/martpie/museeks/issues" }, "scripts": { - "postinstall": "electron-builder install-app-deps", - "build": "electron-vite build", - "dev": "electron-vite dev --watch", - "start": "electron-vite preview", - "museeks": "electron .", - "museeks:debug": "electron . --enable-logging --devtools --trace-warnings", - "test:typings": "yarn run test:typings:root && yarn run test:typings:main && yarn run test:typings:preload && yarn run test:typings:renderer", - "test:typings:root": "tsc --noEmit --project ./tsconfig.json", - "test:typings:main": "tsc --noEmit --project src/main/tsconfig.json", - "test:typings:preload": "tsc --noEmit --project src/preload/tsconfig.json", - "test:typings:renderer": "tsc --noEmit --project src/renderer/tsconfig.json", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "tauri": "tauri", + "test:typings": "tsc --noEmit --project ./tsconfig.json", "test:unit": "vitest run", "test:lint": "eslint src", "test:lint:fix": "eslint src --fix", "test:formatting": "prettier --check \"./**/*.{ts,tsx,js}\"", "test:formatting:fix": "prettier --write \"./**/*.{ts,tsx,js}\"", "test:css": "stylelint \"src/**/*.css\"", - "package:lmw": "electron-builder -lmw && yarn run package:checksums", - "package:lw": "electron-builder -lw", - "package:l": "electron-builder -l", - "package:w": "electron-builder -w", - "package:m": "electron-builder -m", "package:checksums": "bash scripts/checksum.sh" }, + "browserslist": [ + "defaults" + ], "dependencies": { - "@electron/remote": "^2.1.2", "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-slider": "^1.1.2", - "@tanstack/react-virtual": "3.0.4", - "chardet": "^1.6.0", + "@tanstack/react-query": "^5.28.4", + "@tanstack/react-virtual": "3.1.3", + "@tauri-apps/api": "^2.0.0-beta.5", + "@tauri-apps/plugin-dialog": "^2.0.0-beta.2", + "@tauri-apps/plugin-log": "^2.0.0-beta.2", + "@tauri-apps/plugin-notification": "^2.0.0-beta.2", + "@tauri-apps/plugin-os": "^2.0.0-beta.2", + "@tauri-apps/plugin-shell": "^2.0.0-beta.2", "classnames": "^2.5.1", - "electron-store": "^8.1.0", "font-awesome": "^4.7.0", - "globby": "^13.2.2", - "iconv-lite": "^0.6.3", "lodash": "^4.17.21", - "m3ujs": "^0.2.1", - "music-metadata": "^8.2.0", - "nanoid": "^5.0.5", - "pino": "^8.18.0", + "nanoid": "^5.0.6", + "normalize.css": "^8.0.1", + "pino": "^8.19.0", "pino-pretty": "^10.3.1", - "pouchdb": "^8.0.1", - "pouchdb-find": "^8.0.1", - "queue": "^7.0.0", "react": "^18.2.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-fontawesome": "^1.7.1", "react-keybinding-component": "^2.0.2", - "react-router-dom": "6.22.0", + "react-router-dom": "6.22.3", "semver": "^7.6.0", "svg-inline-react": "^3.2.1", - "zustand": "^4.5.0" + "zustand": "^4.5.2" }, "devDependencies": { - "@total-typescript/ts-reset": "^0.5.1", - "@types/lodash": "^4.14.202", - "@types/pouchdb": "^6.4.2", - "@types/react": "^18.2.55", - "@types/react-dom": "^18.2.19", - "@types/react-fontawesome": "^1.6.8", - "@types/react-router-dom": "^5.3.3", - "@types/semver": "^7.5.6", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", + "@tauri-apps/cli": "^2.0.0-beta.9", + "@types/lodash": "^4.17.0", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@types/semver": "^7.5.8", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react": "^4.2.1", - "electron": "28.2.2", - "electron-builder": "^24.12.0", - "electron-devtools-assembler": "^1.2.0", - "electron-vite": "^2.0.0", - "eslint": "^8.56.0", + "autoprefixer": "^10.4.18", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react": "^7.34.0", "eslint-plugin-react-hooks": "^4.6.0", "normalize.css": "^8.0.1", "postcss": "^8.4.35", - "postcss-import": "^15.1.0", + "postcss-import": "^16.0.1", "postcss-nested": "^6.0.1", "postcss-scss": "^4.0.9", "postcss-url": "^10.1.3", "prettier": "^3.2.5", "prettier-eslint": "^16.3.0", - "stylelint": "^15.11.0", - "stylelint-config-css-modules": "^4.3.0", - "stylelint-config-standard": "^34.0.0", - "typescript": "^5.3.3", - "typescript-plugin-css-modules": "^5.0.2", - "vite": "^5.1.0", - "vitest": "^1.2.2" + "stylelint": "^16.2.1", + "stylelint-config-css-modules": "^4.4.0", + "stylelint-config-standard": "^36.0.0", + "typescript": "^5.4.2", + "typescript-plugin-css-modules": "^5.1.0", + "vite": "^5.1.6", + "vite-plugin-react-svg": "^0.2.0", + "vitest": "^1.4.0" } } diff --git a/postcss.config.mjs b/postcss.config.mjs index bd499164c..566405cfb 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -3,5 +3,6 @@ export default { plugins: { 'postcss-import': {}, 'postcss-nested': {}, + 'autoprefixer': {} }, }; diff --git a/public/museeks_logo.png b/public/museeks_logo.png new file mode 100644 index 000000000..337eb7607 Binary files /dev/null and b/public/museeks_logo.png differ diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 000000000..f4dfb82b2 --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 000000000..b1e6470c7 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,6929 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "actionable" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9e6839049e5ad3a410c0fcd32ee25e7eb1f0fb9333310b48314ad6686d09f5" +dependencies = [ + "actionable-macros", + "async-trait", + "serde", + "thiserror", +] + +[[package]] +name = "actionable-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219df0f6a405dcf4f2b0fbb85bc2dafde06d89662d3da7b7f350f3515d65d15d" +dependencies = [ + "darling 0.13.4", + "ident_case", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "thiserror", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.12", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +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 = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_log-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" + +[[package]] +name = "android_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8619b80c242aa7bd638b5c7ddd952addeecb71f69c75e33f1d47b2804f8f883a" +dependencies = [ + "android_log-sys", + "env_logger", + "log", + "once_cell", +] + +[[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.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "arc-bytes" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3de7bfea323262a3d319ed4ed16a960f07395bd903ac650f5e345b32401567d5" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ashpd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b22517ee647547c01a687cf9b76074e1c91334032a4324f7243c6ee0f949390" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.3.0", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451e3cf68011bd56771c79db04a9e333095ab6349f7e47592b788e9b98720cc8" +dependencies = [ + "async-channel", + "async-io", + "async-lock 3.3.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 5.2.0", + "futures-lite", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib 0.18.5", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attribute-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c94f43ede6f25dab1dea046bff84d85dea61bd49aba7a9011ad66c0d449077b" +dependencies = [ + "attribute-derive-macro", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b409e2b2d2dc206d2c0ad3575a93f001ae21a1593e2d0c69b69c308e63f3b422" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.52", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-toml" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2db21524cad41c5591204d22d75e1970a2d1f71060214ca931dc7d5afe2c14e5" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[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 = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel", + "async-lock 3.3.0", + "async-task", + "fastrand", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + +[[package]] +name = "bonsaidb" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e0684e6d0a625039c24880ddd23ff5eec068b4f47a5f7150ec261812bdf7b1" +dependencies = [ + "bonsaidb-core", + "bonsaidb-files", + "bonsaidb-local", + "derive-where", +] + +[[package]] +name = "bonsaidb-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c75825d18ec94b4530afd246b638c318934416808683d6cf6971bf9590f5e48" +dependencies = [ + "actionable", + "arc-bytes", + "async-trait", + "bonsaidb-macros", + "bytecount", + "circulate", + "derive-where", + "futures", + "itertools", + "num-traits", + "ordered-varint", + "pot", + "serde", + "sha2", + "thiserror", + "tinyvec", + "transmog", + "transmog-pot", + "zeroize", +] + +[[package]] +name = "bonsaidb-files" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6cb6f5473367d7f492c9ada0a35fb1317e5e55db8d48737d78e8f49dba82de" +dependencies = [ + "bonsaidb-core", + "bonsaidb-macros", + "bonsaidb-utils", + "derive-where", + "futures", + "lru 0.12.3", + "parking_lot", + "serde", + "thiserror", + "tokio", +] + +[[package]] +name = "bonsaidb-local" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa13e68a85fef7b5c5ebcf90cd3b9d1f821a4ddb6f0e3fbe1150071b5226078a" +dependencies = [ + "async-trait", + "bincode", + "bonsaidb-core", + "bonsaidb-utils", + "byteorder", + "derive-where", + "easy-parallel", + "flume 0.11.0", + "fs2", + "futures", + "itertools", + "log", + "nebari", + "p256", + "parking_lot", + "pot", + "rand 0.8.5", + "serde", + "sysinfo", + "thiserror", + "tokio", + "transmog-versions", + "watchable", +] + +[[package]] +name = "bonsaidb-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c96879fa66046a8d550de9c52f93fa99feea4b24e81917f883c39b5eb11f54" +dependencies = [ + "attribute-derive", + "manyhow", + "proc-macro-crate 2.0.2", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.52", + "trybuild", +] + +[[package]] +name = "bonsaidb-utils" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "184e5ac9b34e6d55fb0a8437bd279650942b25d3ee95b74f3fed2bb5032d5f93" + +[[package]] +name = "borsh" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028" +dependencies = [ + "borsh-derive", + "cfg_aliases 0.1.1", +] + +[[package]] +name = "borsh-derive" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0" +dependencies = [ + "once_cell", + "proc-macro-crate 2.0.2", + "proc-macro2", + "quote", + "syn 2.0.52", + "syn_derive", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + +[[package]] +name = "byte-unit" +version = "5.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ac19bdf0b2665407c39d82dbc937e951e7e2001609f0fb32edd0af45a2d63e" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + +[[package]] +name = "bytemuck" +version = "1.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +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.4.2", + "cairo-sys-rs", + "glib 0.18.5", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cargo_toml" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" +dependencies = [ + "serde", + "toml 0.8.2", +] + +[[package]] +name = "cc" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e53693616d3075149f4ead59bdeecd204ac6b8192d8969757601b74bddf00f" + +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.4", +] + +[[package]] +name = "circulate" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56d670e28743ed41e3cd4388cf7fc928bcf9cedf515b63443a2e13fc4c986df" +dependencies = [ + "arc-bytes", + "flume 0.11.0", + "futures", + "parking_lot", + "pot", + "serde", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.23.1", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colored" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" +dependencies = [ + "is-terminal", + "lazy_static", + "winapi", +] + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[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.52", +] + +[[package]] +name = "ctor" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" +dependencies = [ + "quote", + "syn 2.0.52", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core 0.20.8", + "darling_macro 0.20.8", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.52", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core 0.20.8", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-where" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[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-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[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", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.1", +] + +[[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.52", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "drm" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" +dependencies = [ + "bitflags 2.4.2", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix", +] + +[[package]] +name = "drm-ffi" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" +dependencies = [ + "drm-sys", + "rustix", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" +dependencies = [ + "libc", + "linux-raw-sys 0.6.4", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "easy-parallel" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2afbb9b0aef60e4f0d2b18129b6c0dff035a6f7dbbd17c2f38c1432102ee223c" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embed-resource" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bde55e389bea6a966bd467ad1ad7da0ae14546a5bc794d16d1e55e7fca44881" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.2", + "vswhom", + "winreg 0.51.0", +] + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.2.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "colored", + "log", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[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.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[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 0.3.1", +] + +[[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.52", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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 = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[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" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib 0.18.5", + "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 0.18.5", + "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 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2ea8a4909d530f79921290389cbd7c34cb9d623bfe970eaae65ca5f9cd9cce" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib 0.18.5", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b25e5b3e733153bcab35ee4671b46604b42516163cae442d1601cb716f2ac5" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.53.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[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 0.18.1", + "glib 0.18.5", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +dependencies = [ + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16aa2475c9debed5a32832cb5ff2af5a3f9e1ab9e69df58eaadc1ab2004d6eba" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.16.3", + "glib-macros 0.16.8", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.4.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.18.1", + "glib-macros 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "glib-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +dependencies = [ + "libc", + "system-deps", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gobject-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib 0.18.5", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.4", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.10", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home-config" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27825a5c636b5efd43d1b76065198d4e7927951593202c88764bfd9693cf4cf0" +dependencies = [ + "dirs 4.0.0", + "serde", + "toml 0.5.11", +] + +[[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 = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", +] + +[[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.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "967d6dd42f16dbf0eb8040cb9e477933562684d3918f7d253f2ff9087fb3e7a3" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "infer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199" +dependencies = [ + "cfb", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[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-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[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 = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[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 0.18.5", + "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 0.18.1", + "gobject-sys 0.18.0", + "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", + "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.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.4.2", + "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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib 0.18.5", + "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.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "linux-raw-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lofty" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75066eb1d25a7047fb2667edb410ae2592439ed81546f95c28b0a1c7d7d3818" +dependencies = [ + "byteorder", + "data-encoding", + "flate2", + "lofty_attr", + "log", + "ogg_pager", + "paste", +] + +[[package]] +name = "lofty_attr" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764b60e1ddd07e5665a6a17636a95cd7d8f3b86c73503a69c32979d05f72f3cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +dependencies = [ + "value-bag", +] + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown 0.14.3", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "manyhow" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b76546495d933baa165075b95c0a15e8f7ef75e53f56b19b7144d80fd52bd" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "manyhow-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba072c0eadade3160232e70893311f1f8903974488096e2eb8e48caba2f0cf1" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[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 = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df4051db13d0816cf23196d3baa216385ae099339f5d0645a8d9ff2305e82b8" +dependencies = [ + "lazy_static", + "lru 0.7.8", + "memoize-inner", +] + +[[package]] +name = "memoize-inner" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bdece7e91f0d1e33df7b46ec187a93ea0d4e642113a1039ac8bfdd4a3273ac" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "muda" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c47e7625990fc1af2226ea4f34fb2412b03c12639fcb91868581eb3a6893453" +dependencies = [ + "cocoa 0.25.0", + "crossbeam-channel", + "gtk", + "keyboard-types", + "objc", + "once_cell", + "png", + "serde", + "thiserror", + "windows-sys 0.52.0", +] + +[[package]] +name = "museeks" +version = "0.20.0" +dependencies = [ + "anyhow", + "base64 0.22.0", + "bonsaidb", + "dbus", + "dirs 5.0.1", + "futures", + "home-config", + "lofty", + "log", + "memoize", + "nosleep", + "rayon", + "serde", + "serde_json", + "strum", + "strum_macros", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-log", + "tauri-plugin-notification", + "tauri-plugin-os", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-window-state", + "thiserror", + "tokio", + "ts-rs", + "uuid", + "walkdir", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.12", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "raw-window-handle 0.5.2", + "thiserror", +] + +[[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.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nebari" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d803ae55feaf2b1e177b2d4c3315468f4ccc215c5f9a83d31edfb862803f5b5e" +dependencies = [ + "arc-bytes", + "backtrace", + "byteorder", + "crc", + "flume 0.10.14", + "lru 0.7.8", + "num_cpus", + "once_cell", + "parking_lot", + "thiserror", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nosleep" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cedad558d018cdc0abbca10a799e91ed63fa442d4a567f91dc9dce775a3ebba" +dependencies = [ + "nosleep-mac-sys", + "nosleep-nix", + "nosleep-types", + "nosleep-windows", +] + +[[package]] +name = "nosleep-mac-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b2df065619dbb4fc6034967a7511b0bc4d2caa7688cd6ea0f5601edd7f3901" +dependencies = [ + "cc", + "nosleep-types", + "objc-foundation", + "objc_id", + "snafu", +] + +[[package]] +name = "nosleep-nix" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb4d94c4c2a46b6756dd6f2401c143cd2cb6b8324b0b151cff5de9dad3e59bd" +dependencies = [ + "dbus", + "nosleep-types", + "snafu", +] + +[[package]] +name = "nosleep-types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2d4d904e07b1c33a67ca29e5ac82902e7bb867130d993e938f8c4402986449" +dependencies = [ + "snafu", +] + +[[package]] +name = "nosleep-windows" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2570bd99af538bf408bfaec6f14a63d7d89ad8fe82a17c0c43a31395da4c8d1e" +dependencies = [ + "nosleep-types", + "snafu", + "windows 0.36.1", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "ogg_pager" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c949d63b387b25c332f6e39d1762dd4b405008289dd7681f02c258b1294653ca" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "open" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b3fbb0d52bf0cbb5225ba3d2c303aa136031d43abff98284332a9981ecddec" +dependencies = [ + "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 = "ordered-varint" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9cc9f18ab4bad1e01726bda1259feb8f11e5e76308708a966b4c0136e9db34c" + +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib 0.18.5", + "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 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plist" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +dependencies = [ + "base64 0.21.7", + "indexmap 2.2.4", + "line-wrap", + "quick-xml 0.31.0", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "pot" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df842bdb3b0553a411589e64aaa1a7d0c0259f72fabcedfaa841683ae3019d80" +dependencies = [ + "byteorder", + "half", + "serde", +] + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[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-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" +dependencies = [ + "quote", + "quote-use-macros", + "syn 2.0.52", +] + +[[package]] +name = "quote-use-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ea44c7e20f16017a76a245bb42188517e13d16dcb1aa18044bc406cdc3f4af" +dependencies = [ + "derive-where", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[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.12", +] + +[[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.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" + +[[package]] +name = "rayon" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom 0.2.12", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rfd" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373d2fc6310e2d14943d4e66ebed5b774a2b6b3b1610e7377edf124fb2760d6b" +dependencies = [ + "ashpd", + "block", + "dispatch", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk-sys", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle 0.6.0", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rkyv" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "schemars_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[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 = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +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.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling 0.20.8", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_child" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "softbuffer" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071916a85d1db274b4ed57af3a14afb66bd836ae7f82ebb6f1fd3455107830d9" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases 0.2.0", + "cocoa 0.25.0", + "core-graphics 0.23.1", + "drm", + "fastrand", + "foreign-types 0.5.0", + "js-sys", + "log", + "memmap2", + "objc", + "raw-window-handle 0.6.0", + "redox_syscall", + "rustix", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.52.0", + "x11rb", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib 0.18.5", + "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 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.52", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "swift-rs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bbdb58577b6301f8d17ae2561f32002a5bae056d444e0f69e611e504a276204" +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.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.29.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "winapi", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccba570365293ca309d60f30fdac2c5271b732dc762e6154e59c85d2c762a0a1" +dependencies = [ + "bitflags 1.3.2", + "cocoa 0.25.0", + "core-foundation", + "core-graphics 0.23.1", + "crossbeam-channel", + "dispatch", + "dlopen2", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "png", + "raw-window-handle 0.6.0", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.52.0", + "windows-implement", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "tauri" +version = "2.0.0-beta.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4c18889ceb808488d2a3b2707efaaf8b4d0ecb04a4014b2eaad97030d51b4e8" +dependencies = [ + "anyhow", + "bytes", + "cocoa 0.25.0", + "dirs-next", + "embed_plist", + "futures-util", + "getrandom 0.2.12", + "glob", + "gtk", + "heck", + "http", + "http-range", + "image", + "jni", + "libc", + "log", + "mime", + "muda", + "objc", + "percent-encoding", + "raw-window-handle 0.6.0", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror", + "tokio", + "tray-icon", + "url", + "urlpattern", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.52.0", +] + +[[package]] +name = "tauri-build" +version = "2.0.0-beta.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4671dc132fefbcd7d17c4019679abc190717216818dee608a4990418ad66f335" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "glob", + "heck", + "json-patch", + "quote", + "schemars", + "semver", + "serde", + "serde_json", + "tauri-codegen", + "tauri-utils", + "tauri-winres", + "toml 0.8.2", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.0.0-beta.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027be730d88694e46ccc4e3a17e17d53e697c1ef27b609d6f0df400b54e6766f" +dependencies = [ + "base64 0.22.0", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.52", + "tauri-utils", + "thiserror", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.0.0-beta.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7b6bbc2a8a739dc3fd351d864e9f42d83833afbd938f3a2c2d84f6d945dfcc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.52", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.0.0-beta.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84ccc2181a81c66f2b203775b6b67590cc357bc477c57f0fac844c31dd5c474" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars", + "serde", + "serde_json", + "tauri-utils", + "toml 0.8.2", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c7894fb904ed003fd15915b263655672be4e4581298f7fa8916016e50010ed" +dependencies = [ + "glib 0.16.9", + "log", + "raw-window-handle 0.6.0", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d71f69535111078131380bcf2a4c2f190ef4d045a33d787a606e7d4fc6a786" +dependencies = [ + "anyhow", + "glob", + "schemars", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror", + "url", + "uuid", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0a35ecaa923a40a2c86c758e6927d40cb8bc74d975eabae14ccacaa3294fa8" +dependencies = [ + "android_logger", + "byte-unit", + "cocoa 0.24.1", + "fern", + "log", + "objc", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "time", +] + +[[package]] +name = "tauri-plugin-notification" +version = "2.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c69417d3e60449a6b88fd8af94854a39cab6b5096e50bdbc433d5271d9b465" +dependencies = [ + "log", + "mac-notification-sys", + "rand 0.8.5", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-winrt-notification", + "thiserror", + "time", + "url", + "zbus", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d3b15199f234b9f9c9df69e75aaf0d3e001ffa71c53d7c91c0aaca02964503" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e5c2d4187bc552d1be72081588c34187eb29e4c375cdfe99872f8d57b6aead" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.0.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71935cac98dffdaf038126e1198c40a2a3433466c6866f662c92ec6602f31b23" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "thiserror", + "windows-sys 0.52.0", + "zbus", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0a47b47cc725c1aa53b590c0d14b562bc0b46cf238bff61fb757662d087f75" +dependencies = [ + "bincode", + "bitflags 2.4.2", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror", +] + +[[package]] +name = "tauri-runtime" +version = "2.0.0-beta.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dd438ad0324372d2260e2b440f1df95a77c396dce0c0086383e7ab24d7cc6e" +dependencies = [ + "gtk", + "http", + "jni", + "raw-window-handle 0.6.0", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "windows 0.52.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.0.0-beta.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b38a6017dd8d457270785e740756280fa0139607b428e92e8d7f1f0799d1ca" +dependencies = [ + "cocoa 0.25.0", + "gtk", + "http", + "jni", + "log", + "percent-encoding", + "raw-window-handle 0.6.0", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.52.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.0.0-beta.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec939fd261ec3848b30027a236c0c5a9039d48fa729b4873bdc353a1f4afff1" +dependencies = [ + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "heck", + "html5ever", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "regex", + "schemars", + "semver", + "serde", + "serde_json", + "serde_with", + "swift-rs", + "thiserror", + "toml 0.8.2", + "url", + "urlpattern", + "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 = "tauri-winrt-notification" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2" +dependencies = [ + "quick-xml 0.30.0", + "windows 0.51.1", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.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 = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[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.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa 1.0.10", + "libc", + "num-conv", + "num_threads", + "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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4098d49269baa034a8d1eae9bd63e9fa532148d772121dace3bcd6a6c98eb6d" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading 0.8.1", + "tracing", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +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.2.4", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.2.4", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "transmog" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a7f05cba0120a41e81c7309f084e8b1014118ed19857d6e878c79f0fc4efac" + +[[package]] +name = "transmog-pot" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f777f5fd9b33fa0fd78c5b5ce0e23273098a4f3ff37e3a9b22733b43e71f914e" +dependencies = [ + "pot", + "serde", + "transmog", +] + +[[package]] +name = "transmog-versions" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8950fe6741bdec0c5efb79db30d0f976951653996ba7bcf81763d04ea014135c" +dependencies = [ + "ordered-varint", + "thiserror", + "transmog", +] + +[[package]] +name = "tray-icon" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4d9ddd4a7c0f3b6862af1c4911b529a49db4ee89310d3a258859c2f5053fdd" +dependencies = [ + "cocoa 0.25.0", + "core-graphics 0.23.1", + "crossbeam-channel", + "dirs-next", + "libappindicator", + "muda", + "objc", + "once_cell", + "png", + "serde", + "thiserror", + "windows-sys 0.52.0", +] + +[[package]] +name = "treediff" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d127780145176e2b5d16611cc25a900150e86e9fd79d3bde6ff3a37359c9cb5" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "trybuild" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9d3ba662913483d6722303f619e75ea10b7855b0f8e0d72799cf8621bb488f" +dependencies = [ + "basic-toml", + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", +] + +[[package]] +name = "ts-rs" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6" +dependencies = [ + "thiserror", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f7f9b821696963053a89a7bd8b292dc34420aea8294d7b225274d488f3ec92" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.52", + "termcolor", +] + +[[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-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609" +dependencies = [ + "derive_more", + "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 = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom 0.2.12", + "md-5", + "rand 0.8.5", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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 = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.52", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "watchable" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b42a2f611916b5965120a9cde2b60f2db4454826dd9ad5e6f47c24a5b3b259" +dependencies = [ + "event-listener 4.0.3", + "futures-util", + "parking_lot", + "thiserror", +] + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +dependencies = [ + "bitflags 2.4.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml 0.31.0", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +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 0.18.1", + "glib 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "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 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ae9c7e420783826cf769d2c06ac9ba462f450eca5893bb8c6c6529a4e5dd33" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.52.0", + "windows-core 0.52.0", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "webview2-com-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ad85fceee6c42fa3d61239eba5a11401bf38407a849ed5ea1b407df08cca72" +dependencies = [ + "thiserror", + "windows 0.52.0", + "windows-core 0.52.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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33082acd404763b315866e14a0d5193f3422c81086657583937a750cdd3ec340" +dependencies = [ + "cocoa 0.25.0", + "objc", + "raw-window-handle 0.6.0", + "windows-sys 0.52.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53b97a83176b369b0eb2fd8158d4ae215357d02df9d40c1e1bf1879c5482c80" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-implement", + "windows-interface", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" +dependencies = [ + "windows-core 0.53.0", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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.4", +] + +[[package]] +name = "windows-core" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +dependencies = [ + "windows-result", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-implement" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "windows-interface" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "windows-result" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +dependencies = [ + "windows-targets 0.52.4", +] + +[[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.4", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.4", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wry" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b717040ba9771fd88eb428c6ea6b555f8e734ff8534f02c13e8f10d97f5935e" +dependencies = [ + "base64 0.21.7", + "block", + "cfg_aliases 0.1.1", + "cocoa 0.25.0", + "core-graphics 0.23.1", + "crossbeam-channel", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "objc_id", + "once_cell", + "percent-encoding", + "raw-window-handle 0.6.0", + "serde", + "serde_json", + "sha2", + "soup3", + "tao-macros", + "thiserror", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.52.0", + "windows-implement", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[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 = "x11rb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.1", + "once_cell", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + +[[package]] +name = "xdg-home" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "zbus" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock 3.3.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "derivative", + "enumflags2", + "event-listener 5.2.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "zvariant" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 000000000..dbbfaf353 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,47 @@ +# https://doc.rust-lang.org/cargo/reference/manifest.html + +[package] +name = "museeks" +version = "0.20.0" +description = "A simple, clean and cross-platform music player" +authors = ["Pierre de la Martinière "] +license = "MIT" +repository = "https://github.com/martpie/museeks" +edition = "2021" + +[build-dependencies] +tauri-build = { version = "2.0.0-beta.9", features = ["codegen"] } +[dependencies] +tauri = { version = "2.0.0-beta.11", features = ["protocol-asset", "image-png"] } +tauri-plugin-dialog = "2.0.0-beta.2" +tauri-plugin-notification = "2.0.0-beta.2" +tauri-plugin-os = "2.0.0-beta.2" +tauri-plugin-log = {version = "2.0.0-beta.2", features = ["colored"] } +tauri-plugin-shell = "2.0.0-beta.2" +tauri-plugin-single-instance = "2.0.0-beta.2" +tauri-plugin-window-state = "2.0.0-beta.2" + +# non-Tauri dependencies +anyhow = "1.0.81" +base64 = "0.22.0" +bonsaidb = { version = "0.5.0", features = ["local", "async"] } +dirs = "5.0.1" +futures = "0.3.30" +home-config = { version = "0.6.0", features = ["toml"] } +log = "0.4.21" +lofty = "0.18.2" +memoize = "0.4.2" +nosleep = "0.2.1" +rayon = "1.9.0" +serde_json = "1.0.114" +serde = { version = "1.0.197", features = ["derive"] } +strum = "0.26.2" +strum_macros = "0.26.2" +thiserror = "1.0.58" +tokio = { version = "1.36.0", features = ["full"] } +ts-rs = "7.1.1" +uuid = { version = "1.7.0", features = ["v3", "v4", "fast-rng"] } +walkdir = "2.5.0" + +[target.'cfg(target_os = "linux")'.dependencies] +dbus = "0.9" diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist new file mode 100644 index 000000000..fe253ec7b --- /dev/null +++ b/src-tauri/Info.plist @@ -0,0 +1,10 @@ + + + + + NSCameraUsageDescription + Request camera access for WebRTC + NSMicrophoneUsageDescription + Request microphone access for WebRTC + + diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 000000000..f56d31b50 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,43 @@ +fn main() { + tauri_build::try_build( + tauri_build::Attributes::new() + .codegen(tauri_build::CodegenContext::new()) + .plugin( + "config", + tauri_build::InlinedPlugin::new().commands(&["get_config", "set_config"]), + ) + .plugin( + "cover", + tauri_build::InlinedPlugin::new().commands(&["get_cover"]), + ) + .plugin( + "database", + tauri_build::InlinedPlugin::new().commands(&[ + "import_tracks_to_library", + "get_all_tracks", + "remove_tracks", + "get_tracks", + "get_all_playlists", + "get_playlist", + "create_playlist", + "rename_playlist", + "set_playlist_tracks", + "delete_playlist", + "reset", + ]), + ) + .plugin( + "default-view", + tauri_build::InlinedPlugin::new().commands(&["set"]), + ) + .plugin( + "shell-extension", + tauri_build::InlinedPlugin::new().commands(&["show_item_in_folder"]), + ) + .plugin( + "sleepblocker", + tauri_build::InlinedPlugin::new().commands(&["enable", "disable"]), + ), + ) + .expect("Failed to run tauri-build"); +} diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json new file mode 100644 index 000000000..42cd0db2b --- /dev/null +++ b/src-tauri/capabilities/main.json @@ -0,0 +1,45 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main-capability", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "path:default", + "event:default", + "window:default", + "app:default", + "resources:default", + "menu:default", + "tray:default", + "shell:allow-open", + "dialog:allow-open", + "dialog:allow-ask", + "window:allow-start-dragging", + "os:allow-os-type", + "app:allow-version", + "app:allow-tauri-version", + "log:allow-log", + "menu:allow-new", + "menu:allow-popup", + "notification:default", + "window:allow-show", + "config:allow-set-config", + "config:allow-get-config", + "cover:allow-get-cover", + "database:allow-import-tracks-to-library", + "database:allow-get-all-tracks", + "database:allow-get-tracks", + "database:allow-remove-tracks", + "database:allow-get-all-playlists", + "database:allow-get-playlist", + "database:allow-create-playlist", + "database:allow-rename-playlist", + "database:allow-set-playlist-tracks", + "database:allow-delete-playlist", + "database:allow-reset", + "default-view:allow-set", + "shell-extension:allow-show-item-in-folder", + "sleepblocker:allow-disable" + ], + "platforms": ["linux", "macOS", "windows"] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 000000000..f5b259b08 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 000000000..ace5d6284 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 000000000..0271dc331 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 000000000..1207cae2d Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 000000000..bf6a33647 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 000000000..b0d136bf4 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 000000000..6e9b0e073 Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 000000000..5e96f04f6 Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 000000000..6cb0a5a9f Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 000000000..ee795844a Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 000000000..a18f49170 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 000000000..73a81d5c8 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 000000000..d3b10dff9 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/app-icon-no-margin.png b/src-tauri/icons/app-icon-no-margin.png new file mode 100644 index 000000000..337eb7607 Binary files /dev/null and b/src-tauri/icons/app-icon-no-margin.png differ diff --git a/src-tauri/icons/app-icon.png b/src-tauri/icons/app-icon.png new file mode 100644 index 000000000..69d52210d Binary files /dev/null and b/src-tauri/icons/app-icon.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 000000000..24fdd88fc Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 000000000..3cad002b9 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 000000000..a68f1d50e Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/libs/error.rs b/src-tauri/src/libs/error.rs new file mode 100644 index 000000000..fa89788ac --- /dev/null +++ b/src-tauri/src/libs/error.rs @@ -0,0 +1,50 @@ +use anyhow::Result; +use serde::{ser::Serializer, Serialize}; +use thiserror::Error; + +/** + * Create the error type that represents all errors possible in our program + * Stolen from https://github.com/tauri-apps/tauri/discussions/3913 + */ +#[derive(Debug, Error)] +pub enum MuseeksError { + #[error(transparent)] + Lofty(#[from] lofty::LoftyError), + + #[error(transparent)] + Database(#[from] bonsaidb::core::Error), + + #[error(transparent)] + LocalDatabase(#[from] bonsaidb::local::Error), + + #[error(transparent)] + NoSleep(#[from] nosleep::Error), + + // #[error(transparent)] + // NoSleepPoison(#[from] PoisonError), + #[error(transparent)] + Unknown(#[from] anyhow::Error), + + /** + * Custom errors + */ + #[error("{message}")] + Library { message: String }, + + #[error("Playlist not found")] + PlaylistNotFound, +} + +/** + * Let's make anyhow's errors Tauri friendly, so they can be used for commands + */ +impl Serialize for MuseeksError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +pub type AnyResult = Result; diff --git a/src-tauri/src/libs/events.rs b/src-tauri/src/libs/events.rs new file mode 100644 index 000000000..c0b45a92e --- /dev/null +++ b/src-tauri/src/libs/events.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::{AsRefStr, Display}; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, TS, Display, AsRefStr)] +#[ts(export, export_to = "../src/generated/typings/IPCEvent.ts")] +pub enum IPCEvent<'a> { + // Playback-related events + PlaybackPlay(&'a str), + PlaybackPause, + PlaybackStop, + PlaybackPlayPause, + PlaybackPrevious, + PlaybackNext, + PlaybackTrackChange, + // Scan-related events + LibraryScanProgress, + // Menu-related events + GoToLibrary, + GoToPlaylists, + GoToSettings, + JumpToPlayingTrack, +} diff --git a/src-tauri/src/libs/mod.rs b/src-tauri/src/libs/mod.rs new file mode 100644 index 000000000..7947cb112 --- /dev/null +++ b/src-tauri/src/libs/mod.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod events; +pub mod utils; diff --git a/src-tauri/src/libs/utils.rs b/src-tauri/src/libs/utils.rs new file mode 100644 index 000000000..80c5fb71f --- /dev/null +++ b/src-tauri/src/libs/utils.rs @@ -0,0 +1,114 @@ +/** + * Small utility to display time metrics with a log message + */ +use log::info; +use std::{ffi::OsStr, path::PathBuf, time::Instant}; +use tauri::{Runtime, WebviewWindow}; +use walkdir::WalkDir; + +/** + * Small helper to compute the execution time of some code + */ +pub struct TimeLogger { + start_time: Instant, + message: String, +} + +impl TimeLogger { + pub fn new(message: String) -> Self { + TimeLogger { + start_time: Instant::now(), + message, + } + } + + pub fn complete(&self) { + let duration = self.start_time.elapsed(); + info!("{} in {:.2?}", self.message, duration); + } +} + +/** + * Get the app configuration/storage directory + */ +pub fn get_app_storage_dir() -> PathBuf { + let path = dirs::home_dir().expect("Get home dir"); + path.join(".museeks") +} + +/** + * Check if a directory or a file is visible or not + */ +fn is_dir_visible(entry: &walkdir::DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| !s.starts_with(".")) + .unwrap_or(false) +} + +/** + * Take an entry and filter out: + * - directories + * - non-allowed extensions + */ +fn is_entry_valid( + result: std::result::Result, + allowed_extensions: &[&str], +) -> Option { + // If the user does not have access to the file + if result.is_err() { + return None; + } + + let entry = result.unwrap(); + let file_type = entry.file_type(); + + let extension = entry + .path() + .extension() + .and_then(OsStr::to_str) + .unwrap_or(""); + + let is_file = file_type.is_file(); + let has_valid_extension = allowed_extensions.contains(&extension); + + if is_file && has_valid_extension { + // Only return the file path, that's what we're interested in + return Some(entry.into_path()); + } + + return None; +} + +/** + * Scan multiple directories and filter files by extension + */ +pub fn scan_dirs(paths: &Vec, allowed_extensions: &[&str]) -> Vec { + paths + .iter() + .map(|path| scan_dir(path, allowed_extensions)) + .flatten() + .collect() +} + +/** + * Scan directory and filter files by extension + */ +pub fn scan_dir(path: &PathBuf, allowed_extensions: &[&str]) -> Vec { + WalkDir::new(path) + .follow_links(true) + .into_iter() + .filter_entry(|entry| is_dir_visible(entry)) + .filter_map(|entry| is_entry_valid(entry, allowed_extensions)) + .collect() +} + +/** + * Ensure a window is shown and visible + */ +pub fn show_window(window: &WebviewWindow) { + window.maximize().unwrap(); + window.show().unwrap(); + window.set_focus().unwrap(); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 000000000..c4f15bbe6 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,59 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod libs; +mod plugins; + +use libs::utils::show_window; +use log::LevelFilter; +use tauri::Manager; +use tauri_plugin_log::fern::colors::ColoredLevelConfig; +use tauri_plugin_log::{Target, TargetKind}; + +/** + * The beast + */ +#[tokio::main] +async fn main() { + // Is there any way to instantiate that in the plugin directly? + let db = plugins::database::setup().await.ok().unwrap(); + + tauri::Builder::default() + // Custom integrations + .plugin(plugins::app_menu::init()) + .plugin(plugins::config::init()) + .plugin(plugins::cover::init()) + .plugin(plugins::database::init(db)) + .plugin(plugins::debug::init()) + .plugin(plugins::default_view::init()) + .plugin(plugins::shell_extension::init()) + .plugin(plugins::sleepblocker::init()) + .plugin(plugins::macos::init()) + // Tauri integrations with the Operating System + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_single_instance::init(|app_handle, _, _| { + let window = app_handle.get_webview_window("main").unwrap(); + show_window(&window); + })) + .plugin(tauri_plugin_window_state::Builder::default().build()) + .plugin( + tauri_plugin_log::Builder::default() + .targets([ + Target::new(TargetKind::Stdout), + Target::new(TargetKind::Webview), + ]) + .level(LevelFilter::Info) + .with_colors(ColoredLevelConfig::default()) + .build(), + ) + // TODO: tauri-plugin-theme to update the native theme at runtime + .setup(|_app| { + // :] + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/plugins/app_menu.rs b/src-tauri/src/plugins/app_menu.rs new file mode 100644 index 000000000..3f1b09fe8 --- /dev/null +++ b/src-tauri/src/plugins/app_menu.rs @@ -0,0 +1,173 @@ +use std::path::PathBuf; +use tauri::image::Image; +use tauri::{ + menu::{AboutMetadataBuilder, MenuBuilder, MenuId, MenuItemBuilder, SubmenuBuilder}, + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +use crate::libs::events::IPCEvent; + +pub fn init() -> TauriPlugin { + Builder::new("app-menu") + .invoke_handler(tauri::generate_handler![/*popup, toggle*/]) + .on_window_ready(|window| { + let app_handle = window.app_handle(); + + // ----------------------------------------------------------------- + // Museeks sub-menu + // ----------------------------------------------------------------- + let package_info = app_handle.package_info(); + let version = package_info.version.to_string().into(); + let resource_path: PathBuf = app_handle.path().resource_dir().unwrap(); + let icon_path = resource_path.join("icons").join("icon.png"); + let icon = Image::from_path(icon_path).unwrap(); + + let about_metadata = AboutMetadataBuilder::new() + .version(version) // TODO: Automate all that? + .authors(Some(vec![package_info.authors.to_string()])) + .license("MIT".into()) + .website("https://museeks.io".into()) + .website_label("museeks.io".into()) + .icon(Some(icon)) + .build(); + + let museeks_menu = SubmenuBuilder::new(app_handle, "Museeks") + .about(Some(about_metadata)) + .separator() + .services() + .hide() + .hide_others() + .show_all() + .separator() + .quit() + .build() + .unwrap(); + + // ----------------------------------------------------------------- + // File sub-menu + // ----------------------------------------------------------------- + let file_menu = SubmenuBuilder::new(app_handle, "File") + .close_window() + .build() + .unwrap(); + + // ----------------------------------------------------------------- + // Edit sub-menu + // ----------------------------------------------------------------- + let edit_menu = SubmenuBuilder::new(app_handle, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .select_all() + .build() + .unwrap(); + + // ----------------------------------------------------------------- + // View sub-menu + // ----------------------------------------------------------------- + // TODO: create events listeners and shortcuts + let view_menu = SubmenuBuilder::new(app_handle, "View") + .item( + &MenuItemBuilder::new("Jump to playing track") + .id(MenuId::new("jump_to_playing_track")) + .accelerator("CmdOrCtrl+T") + .build(app_handle) + .unwrap(), + ) + .separator() + .item( + &MenuItemBuilder::new("Go to library") + .id(MenuId::new(IPCEvent::GoToLibrary.as_ref())) + .accelerator("CmdOrCtrl+L") + .build(app_handle) + .unwrap(), + ) + .item( + &MenuItemBuilder::new("Go to playlists") + .id(MenuId::new(IPCEvent::GoToPlaylists.as_ref())) + .accelerator("CmdOrCtrl+P") + .build(app_handle) + .unwrap(), + ) + .item( + &MenuItemBuilder::new("Go to settings") + .id(MenuId::new(IPCEvent::GoToSettings.as_ref())) + .accelerator("CmdOrCtrl+P") + .build(app_handle) + .unwrap(), + ) + .build() + .unwrap(); + + // ----------------------------------------------------------------- + // Window sub-menu + // ----------------------------------------------------------------- + let window_menu: tauri::menu::Submenu = SubmenuBuilder::new(app_handle, "Window") + .item( + &MenuItemBuilder::new("-") + .enabled(false) + .build(app_handle) + .unwrap(), + ) + .build() + .unwrap(); + + // ----------------------------------------------------------------- + // Help sub-menu + // ----------------------------------------------------------------- + let help_menu = SubmenuBuilder::new(app_handle, "Help") + .item( + &MenuItemBuilder::new("-") + .enabled(false) + .build(app_handle) + .unwrap(), + ) + .build() + .unwrap(); + + // ----------------------------------------------------------------- + // Assembled menu + listeners + // ----------------------------------------------------------------- + let menu = MenuBuilder::new(app_handle) + .items(&[ + &museeks_menu, + &file_menu, + &edit_menu, + &view_menu, + &window_menu, + &help_menu, + ]) + .build() + .unwrap(); + + // The menu on macOS is app-wide and not specific to one window + #[cfg(target_os = "macos")] + { + app_handle.set_menu(menu).unwrap(); + } + + // On Windows / Linux, app menus are wasting vertical space, so we + // hide it by default. The menu get toggle-able by pressing Alt. + #[cfg(not(target_os = "macos"))] + { + window.set_menu(menu).unwrap(); + window.hide_menu().unwrap(); + } + + // TODO: hide/show menu with Alt on Linux + Windows + // TODO: support menu events + // https://github.com/tauri-apps/tauri/issues/9060 + // window.on_menu_event(|_app_handle, event| { + // // let main_webview = app_handle.get_webview_window("main").unwrap(); + + // info!("event {:?}", event.id()); + // // TODO: + // // main_webview.emit(); + // }); + }) + .build() +} diff --git a/src-tauri/src/plugins/config.rs b/src-tauri/src/plugins/config.rs new file mode 100644 index 000000000..d7a8cd440 --- /dev/null +++ b/src-tauri/src/plugins/config.rs @@ -0,0 +1,183 @@ +/** + * Module in charge of persisting and returning the config to/from the filesystem + */ +use home_config::HomeConfig; +use log::info; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, sync::RwLock}; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{Manager, Runtime, State}; +use ts_rs::TS; + +use crate::libs::utils::get_app_storage_dir; + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export, export_to = "../src/generated/typings/Repeat.ts")] +pub enum Repeat { + All, + One, + None, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export, export_to = "../src/generated/typings/SortBy.ts")] +pub enum SortBy { + Artist, + Album, + Title, + Duration, + Genre, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export, export_to = "../src/generated/typings/SortOrder.ts")] +pub enum SortOrder { + Asc, + Dsc, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export, export_to = "../src/generated/typings/DefaultView.ts")] +pub enum DefaultView { + Library, + Playlists, +} + +#[derive(Serialize, Deserialize, Debug, Clone, TS)] +#[ts(export, export_to = "../src/generated/typings/Config.ts")] +pub struct Config { + pub theme: String, + pub audio_volume: f32, + pub audio_playback_rate: f32, + pub audio_output_device: String, + pub audio_muted: bool, + pub audio_shuffle: bool, + pub audio_repeat: Repeat, + pub default_view: DefaultView, + pub library_sort_by: SortBy, + pub library_sort_order: SortOrder, + pub library_folders: Vec, // Not used yet + pub sleepblocker: bool, + pub auto_update_checker: bool, + pub minimize_to_tray: bool, + pub notifications: bool, + pub track_view_density: String, +} + +impl Config { + pub fn default() -> Self { + Config { + theme: "__system".to_owned(), + audio_volume: 1.0, + audio_playback_rate: 1.0, + audio_output_device: "default".to_owned(), + audio_muted: false, + audio_shuffle: false, + audio_repeat: Repeat::None, + default_view: DefaultView::Library, + library_sort_by: SortBy::Artist, + library_sort_order: SortOrder::Asc, + library_folders: vec![], + sleepblocker: false, + auto_update_checker: true, + minimize_to_tray: false, + notifications: false, + track_view_density: "normal".to_owned(), + } + } +} + +#[derive(Debug)] +pub struct ConfigManager { + manager: HomeConfig, + pub data: RwLock, +} + +impl ConfigManager { + pub fn get(&self) -> Config { + self.data.read().unwrap().clone() + } + + pub fn update(&self, config: Config) { + let mut writer = self.data.write().unwrap(); + *writer = config; + std::mem::drop(writer); + self.save(); + } + + pub fn set_sleepblocker(&self, sleepblocker: bool) { + let mut writer = self.data.write().unwrap(); + writer.sleepblocker = sleepblocker; + std::mem::drop(writer); + self.save(); + } + + pub fn set_default_view(&self, default_view: DefaultView) { + let mut writer = self.data.write().unwrap(); + writer.default_view = default_view; + std::mem::drop(writer); + self.save(); + } + + fn save(&self) { + let config = self.data.read().unwrap(); + self.manager.save_toml(config.clone()).unwrap(); + info!("Config updated"); + } +} + +#[tauri::command] +pub fn get_config(config_manager: State) -> Config { + config_manager.get() +} + +#[tauri::command] +pub fn set_config(config_manager: State, config: Config) { + config_manager.update(config); +} + +pub fn init() -> TauriPlugin { + let conf_path = get_app_storage_dir(); + let manager = HomeConfig::with_file(conf_path.join("config.toml")); + let existing_config = manager.toml::(); + + let config = match existing_config { + Ok(config) => ConfigManager { + manager, + data: RwLock::new(config), + }, + Err(_) => { + // The config does not exist, so let's instantiate it with defaults + // Potential issue: if the config is extended, the defaults will be + // reloaded + let default_config = Config::default(); + manager.save_toml(&default_config).unwrap(); + + ConfigManager { + manager, + data: RwLock::new(default_config), + } + } + }; + + // We need to inject the initial state of the config to the window object of + // our webview, because some of our front-end modules are instantiated at + // parsing time and require data that would otherwise only load-able asynchronously + let initial_config_script = format!( + r#" + window.__MUSEEKS_INITIAL_CONFIG = {}; + window.__MUSEEKS_PLATFORM = {:?}; + "#, + serde_json::to_string(&config.get()).unwrap(), + tauri_plugin_os::type_().to_string() + ); + + Builder::::new("config") + .invoke_handler(tauri::generate_handler![get_config, set_config,]) + .js_init_script(initial_config_script) + .setup(|app_handle, _api| { + app_handle.manage(config); + Ok(()) + }) + .build() +} diff --git a/src-tauri/src/plugins/cover.rs b/src-tauri/src/plugins/cover.rs new file mode 100644 index 000000000..232624541 --- /dev/null +++ b/src-tauri/src/plugins/cover.rs @@ -0,0 +1,91 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use base64::prelude::*; +use lofty::{MimeType, PictureType, TaggedFileExt}; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::Runtime; + +use crate::libs::error::AnyResult; +use crate::libs::utils::scan_dir; + +const SUPPORTED_COVER_EXTENSIONS: [&str; 5] = ["png", "jpg", "jpeg", "bmp", "gif"]; +const SUPPORTED_COVER_NAMES: [&str; 5] = ["album", "albumart", "folder", "cover", "front"]; + +#[memoize::memoize] +fn get_cover_from_id3(path: String) -> Option { + let tagged_file = match lofty::read_from_path(path) { + Ok(tagged_file) => tagged_file, + Err(_) => return None, + }; + + let primary_tag = match tagged_file.primary_tag() { + Some(tag) => tag, + None => return None, + }; + + let cover = match primary_tag.get_picture_type(PictureType::CoverFront) { + Some(cover) => cover, + None => return None, + }; + + let format = match cover.mime_type() { + Some(MimeType::Png) => "png".to_string(), + Some(MimeType::Jpeg) => "jpg".to_string(), + Some(MimeType::Tiff) => "tiff".to_string(), + Some(MimeType::Bmp) => "bmp".to_string(), + Some(MimeType::Gif) => "gif".to_string(), + _ => return None, + }; + + let cover_base64 = BASE64_STANDARD.encode(&cover.data()); + Some(format!("data:{};base64,{}", format, cover_base64)) +} + +#[memoize::memoize] +fn get_cover_from_filesystem<'a>(path: String) -> Option { + let dir_path = PathBuf::from_str(&path) + .unwrap() + .parent() + .unwrap() + .to_path_buf(); + + match scan_dir(&dir_path, &SUPPORTED_COVER_EXTENSIONS) + .iter() + .find(|file| { + let file_stem = file + .file_stem() + .unwrap() + .to_str() + .unwrap_or("???") + .to_lowercase(); + + SUPPORTED_COVER_NAMES.contains(&file_stem.as_str()) + }) { + // Ideally, the file URL would be converted to asset.localhost + // here, but I could not find the equivalent on convertFileSrc + // for the back-end; + Some(path) => Some(path.to_str().unwrap().into()), + None => None, + } +} + +#[tauri::command] +pub async fn get_cover(path: String) -> AnyResult> { + // 1. Try to get the Cover from the ID3 tag + match get_cover_from_id3(path.clone()) { + Some(path) => Ok(Some(path)), + // 2. Cover was not found, so let's fallback to scanning the directory + // for a valid cover file + None => Ok(get_cover_from_filesystem(path)), + } +} + +/** + * Module in charge of assisting the UI with cover retrieval + */ +pub fn init() -> TauriPlugin { + Builder::::new("cover") + .invoke_handler(tauri::generate_handler![get_cover]) + .build() +} diff --git a/src-tauri/src/plugins/database.rs b/src-tauri/src/plugins/database.rs new file mode 100644 index 000000000..fb309ba32 --- /dev/null +++ b/src-tauri/src/plugins/database.rs @@ -0,0 +1,581 @@ +use bonsaidb::core::connection::{AsyncCollection, AsyncConnection, AsyncStorageConnection}; +use bonsaidb::core::document::OwnedDocument; +use bonsaidb::core::schema::{Collection, SerializedCollection}; +use bonsaidb::core::transaction::{Operation, Transaction}; +use bonsaidb::local::config::{Builder as BonsaiBuilder, StorageConfiguration}; +use bonsaidb::local::AsyncDatabase; +use bonsaidb::local::AsyncStorage; +use lofty::{Accessor, AudioFile, ItemKey, TaggedFileExt}; +use log::{error, info, warn}; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{Manager, Runtime, State}; +use ts_rs::TS; +use uuid::Uuid; + +use crate::libs::error::{AnyResult, MuseeksError}; +use crate::libs::events::IPCEvent; +use crate::libs::utils::{get_app_storage_dir, scan_dirs, TimeLogger}; + +const INSERTION_BATCH: usize = 200; + +pub const SUPPORTED_TRACKS_EXTENSIONS: [&str; 12] = [ + "mp3", "mp4", "aac", "m4a", "3gp", "wav", /* mp3 / mp4 */ + "ogg", "ogv", "ogm", "opus", /* Opus */ + "flac", /* Flac */ + "webm", /* Web media */ +]; + +// pub const SUPPORTED_PLAYLISTS_EXTENSIONS: [&str; 1] = [".m3u"]; + +/** ---------------------------------------------------------------------------- + * Databases + * exposes databases for tracks and playlists + * TODO: + * - Export all needed structs to a single file: ts-rs#59 + * -------------------------------------------------------------------------- */ +pub struct DB { + pub tracks: AsyncDatabase, + pub playlists: AsyncDatabase, +} + +impl DB { + fn tracks_collection(&self) -> AsyncCollection<'_, AsyncDatabase, Track> { + self.tracks.collection::() + } + + fn playlists_collection(&self) -> AsyncCollection<'_, AsyncDatabase, Playlist> { + self.playlists.collection::() + } + + /** + * Get all the tracks (and their content) from the database + */ + pub async fn get_all_tracks(&self) -> AnyResult> { + let timer = TimeLogger::new("Retrieved and decoded tracks".into()); + let docs = self.tracks_collection().all().await?; + let tracks = self.decode_docs::(docs); + timer.complete(); + tracks + } + + /** + * Get tracks (and their content) given a set of document IDs + */ + pub async fn get_tracks(&self, track_ids: Vec) -> AnyResult> { + let docs = self.tracks_collection().get_multiple(&track_ids).await?; + + match self.decode_docs::(docs) { + Ok(mut tracks) => { + // document may not ordered the way we want, so let's ensure they map to track_ids + tracks.sort_by_key(|track| track_ids.iter().position(|id| id == &track._id)); + Ok(tracks) + } + Err(err) => Err(err), + } + } + + /** Delete multiple tracks by ID */ + pub async fn remove_tracks(&self, ids: Vec) -> AnyResult<()> { + let tracks = self.tracks_collection().get_multiple(&ids).await?; + + let mut tx = Transaction::new(); + for track in tracks { + tx.push(Operation::delete(Track::collection_name(), track.header)); + } + tx.apply_async(&self.tracks).await?; + + Ok(()) + } + + /** + * Insert a new track in the DB, will fail in case there is a duplicate unique + * key (like track.path) + * + * Doc: https://github.com/khonsulabs/bonsaidb/blob/main/examples/basic-local/examples/basic-local-multidb.rs + */ + pub async fn insert_track(&self, tracks: &Vec) -> AnyResult<()> { + // BonsaiDB does not work well (as of today) with a lot of very small + // insertions, so let's insert tracks by batch instead then + // TODO: if a batch fails (because for example a duplicate path), the whole + // transaction should not + let batches: Vec> = tracks.chunks(INSERTION_BATCH).map(|x| x.to_vec()).collect(); + + info!("Splitting tracks in {} batche(s)", batches.len()); + + for batch in batches { + let mut tx = Transaction::new(); + + for track in batch { + tx.push(Operation::push_serialized::(&track)?); + } + + // Let's goooo + let result = tx.apply_async(&self.tracks).await; + + match result { + Ok(_) => (), + // TODO: + // Err(bonsaidb::core::Error::DocumentConflict(_err, _)) => { + // info!("Track already in library: '{:?}'", &track.path); + // } + Err(err) => { + error!("Failed to insert tracks: {:?}", err); + } + } + } + + Ok(()) + } + + /** Get all the playlists (and their content) from the database */ + pub async fn get_all_playlists(&self) -> AnyResult> { + let timer = TimeLogger::new("Retrieved and decoded playlists".into()); + let docs = self.playlists_collection().all().await?; + let playlists = self.decode_docs::(docs); + timer.complete(); + playlists + } + + /** Get a single playlist by ID */ + pub async fn get_playlist(&self, playlist_id: String) -> AnyResult> { + let maybe_doc = self.playlists_collection().get(&playlist_id).await?; + + match maybe_doc { + Some(doc) => Ok(Some(self.decode_doc::(doc)?)), + None => Ok(None), + } + } + + /** Create a playlist given a name and a set of track IDs */ + pub async fn create_playlist(&self, name: String, tracks: Vec) -> AnyResult { + let playlist = Playlist { + _id: uuid::Uuid::new_v4().to_string(), + name, + tracks, + import_path: None, + }; + + self.playlists_collection() + .insert(&playlist._id, &playlist) + .await?; + + Ok(playlist) + } + + /** Set the tracks of a playlist given its ID and tracks IDs */ + pub async fn set_playlist_tracks( + &self, + id: String, + tracks: Vec, + ) -> AnyResult { + if let Some(document) = self.playlists_collection().get(&id).await? { + let mut playlist = self.decode_doc::(document)?; + + // Insert new tracks + make sure we remove duplicates (the UI does + // not play well with those). + playlist.tracks = tracks + .into_iter() + .collect::>() + .into_iter() + .collect::>(); + + match playlist.overwrite_into_async(&id, &self.playlists).await { + Ok(doc) => Ok(doc.contents), + Err(_) => Err(MuseeksError::Library { + message: "Failed to set playlist tracks".into(), + }), + } + } else { + Err(MuseeksError::PlaylistNotFound) + } + } + + /** Update a playlist name by ID */ + pub async fn rename_playlist(&self, id: String, name: String) -> AnyResult { + if let Some(document) = self.playlists_collection().get(&id).await? { + let mut playlist = self.decode_doc::(document)?; + playlist.name = name; + + match playlist.overwrite_into_async(&id, &self.playlists).await { + Ok(doc) => Ok(doc.contents), + Err(_) => Err(MuseeksError::Library { + message: "Failed to rename playlist".into(), + }), + } + } else { + Err(MuseeksError::PlaylistNotFound) + } + } + + /** Delete a playlist by ID */ + pub async fn delete_playlist(&self, id: String) -> AnyResult<()> { + if let Some(document) = self.playlists_collection().get(&id).await? { + Ok(self.playlists_collection().delete(&document).await?) + } else { + Err(MuseeksError::PlaylistNotFound) + } + } + + /** + * Decode the content for a given set of document (track, playlist, etc) + */ + fn decode_docs( + &self, + docs: Vec, + ) -> AnyResult::Contents>> { + let mut entries = vec![]; + + for doc in docs { + let deserialized = T::document_contents(&doc)?; + entries.push(deserialized); + } + + Ok(entries) + } + + fn decode_doc( + &self, + doc: OwnedDocument, + ) -> AnyResult<::Contents> { + Ok(T::document_contents(&doc)?) + } +} + +/** ---------------------------------------------------------------------------- + * Track + * represent a single track, id and path should be unique + * -------------------------------------------------------------------------- */ +#[derive(Debug, Clone, Serialize, Deserialize, Collection, TS)] +#[collection(name="tracks", primary_key = String)] +#[ts(export, export_to = "../src/generated/typings/Track.ts")] +pub struct Track { + #[natural_id] + pub _id: String, + pub title: String, + pub album: String, + pub artists: Vec, + pub genres: Vec, + pub year: Option, + pub duration: u32, + pub track: NumberOf, + pub disk: NumberOf, + pub path: PathBuf, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../src/generated/typings/NumberOf.ts")] +pub struct NumberOf { + pub no: Option, + pub of: Option, +} + +/** ---------------------------------------------------------------------------- + * Playlist + * represent a playlist, that has a name and a list of tracks + * -------------------------------------------------------------------------- */ + +#[derive(Debug, Clone, Serialize, Deserialize, Collection, TS)] +#[collection(name = "playlists", primary_key = String)] +#[ts(export, export_to = "../src/generated/typings/Playlist.ts")] +pub struct Playlist { + #[natural_id] + pub _id: String, + pub name: String, + pub tracks: Vec, // vector of IDs + pub import_path: Option, +} + +/** + * Scan progress + */ +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../src/generated/typings/Progress.ts")] +pub struct Progress { + current: usize, + total: usize, +} + +/** ---------------------------------------------------------------------------- + * Commands + * -------------------------------------------------------------------------- */ + +/** + * Popup a directory picker dialog, scan the selected folders, extract all + * ID3 tags from it, and update the DB accordingly. + */ +#[tauri::command] +async fn import_tracks_to_library( + window: tauri::Window, + db: State<'_, DB>, + import_paths: Vec, +) -> AnyResult> { + let webview_window = window.get_webview_window("main").unwrap(); + + info!("Importing paths to library:"); + for path in &import_paths { + info!(" - {:?}", path) + } + + let paths = scan_dirs(&import_paths, &SUPPORTED_TRACKS_EXTENSIONS); + let task_count = paths.len(); + + let progress = Arc::new(AtomicUsize::new(1)); + let total = Arc::new(AtomicUsize::new(paths.len())); + + webview_window + .emit( + IPCEvent::LibraryScanProgress.as_ref(), + Progress { + current: 0, + total: paths.len(), + }, + ) + .unwrap(); + + // Let's get all tracks ID3 + info!("Importing ID3 tags from {} files", task_count); + let scan_logger = TimeLogger::new("Scanned all id3 tags".into()); + + let tracks = &paths + .par_iter() + .map(|path| -> Option { + // let counter = processed.clone(); + let p_current = progress.clone().fetch_add(1, Ordering::SeqCst); + let p_total = total.clone().load(Ordering::SeqCst); + + if p_current % 200 == 0 || p_current == p_total { + info!("Processing tracks {:?}/{:?}", p_current, total); + webview_window + .emit( + IPCEvent::LibraryScanProgress.as_ref(), + Progress { + current: p_current, + total: p_total, + }, + ) + .unwrap(); + } + + match lofty::read_from_path(&path) { + Ok(tagged_file) => { + let tag = tagged_file.primary_tag()?; + + // TODO: make sure we don't save tracks that are already in DB + // IMPROVE ME: Is there a more idiomatic way of doing the following? + let mut artists: Vec = tag + .get_strings(&ItemKey::TrackArtist) + .map(ToString::to_string) + .collect(); + + if artists.is_empty() { + artists = tag + .get_strings(&ItemKey::AlbumArtist) + .map(ToString::to_string) + .collect(); + } + + if artists.is_empty() { + artists = vec!["Unknown Artist".into()]; + } + + Some(Track { + _id: Uuid::new_v3(&Uuid::NAMESPACE_OID, path.to_string_lossy().as_bytes()) + .to_string(), + title: tag + .get_string(&ItemKey::TrackTitle) + .unwrap_or("Unknown") + .to_string(), + album: tag + .get_string(&ItemKey::AlbumTitle) + .unwrap_or("Unknown") + .to_string(), + artists, + genres: tag + .get_strings(&ItemKey::Genre) + .map(ToString::to_string) + .collect(), + year: tag.year(), + duration: u32::try_from(tagged_file.properties().duration().as_secs()) + .unwrap_or(0), + track: NumberOf { + no: tag.track(), + of: tag.track_total(), + }, + disk: NumberOf { + no: tag.disk(), + of: tag.disk_total(), + }, + path: path.to_owned(), + }) + } + Err(err) => { + warn!("Failed to get ID3 tags: \"{}\". File {:?}", err, path); + None + } + } + }) + .flatten() + .collect::>(); + + info!("{} tracks successfully scanned", tracks.len()); + info!("{} tracks failed to be scanned", paths.len() - tracks.len()); + scan_logger.complete(); + + let db_insert_logger: TimeLogger = TimeLogger::new("Inserted tracks".into()); + + // Insert all tracks in the DB + let result = db.insert_track(tracks).await; + + if result.is_err() { + warn!("Something went wrong when inserting tracks"); + } + + db_insert_logger.complete(); + + let tracks = db.get_all_tracks().await.unwrap(); + + Ok(tracks) +} + +#[tauri::command] +async fn get_all_tracks(db: State<'_, DB>) -> AnyResult> { + db.get_all_tracks().await +} + +#[tauri::command] +async fn get_tracks(db: State<'_, DB>, ids: Vec) -> AnyResult> { + db.get_tracks(ids).await +} +#[tauri::command] +async fn remove_tracks(db: State<'_, DB>, ids: Vec) -> AnyResult<()> { + db.remove_tracks(ids).await +} + +#[tauri::command] +async fn get_all_playlists(db: State<'_, DB>) -> AnyResult> { + db.get_all_playlists().await +} + +#[tauri::command] +async fn get_playlist(db: State<'_, DB>, id: String) -> AnyResult { + match db.get_playlist(id).await { + Ok(Some(playlist)) => Ok(playlist), + Ok(None) => Err(MuseeksError::PlaylistNotFound), + Err(err) => Err(err), + } +} + +#[tauri::command] +async fn create_playlist( + db: State<'_, DB>, + name: String, + tracks: Vec, +) -> AnyResult { + db.create_playlist(name, tracks).await +} + +#[tauri::command] +async fn rename_playlist(db: State<'_, DB>, id: String, name: String) -> AnyResult { + db.rename_playlist(id, name).await +} + +#[tauri::command] +async fn set_playlist_tracks( + db: State<'_, DB>, + id: String, + tracks: Vec, +) -> AnyResult { + db.set_playlist_tracks(id, tracks).await +} + +#[tauri::command] +async fn delete_playlist(db: State<'_, DB>, id: String) -> AnyResult<()> { + db.delete_playlist(id).await +} + +#[tauri::command] +async fn reset(db: State<'_, DB>) -> AnyResult<()> { + info!("Resetting DB..."); + let timer = TimeLogger::new("Reset DB".into()); + + let tracks = db.tracks_collection().all().await?; + let playlists = db.playlists_collection().all().await?; + + // We create a transaction to delete tracks much faster + let mut tx = Transaction::new(); + + for track in tracks { + tx.push(Operation::delete(Track::collection_name(), track.header)); + } + + tx.apply_async(&db.tracks).await?; + + // Now let's delete playlists + tx = Transaction::new(); + + for playlist in playlists { + tx.push(Operation::delete( + Playlist::collection_name(), + playlist.header, + )); + } + + tx.apply_async(&db.playlists).await?; + + timer.complete(); + + Ok(()) +} + +/** + * Database setup + * Doc: https://github.com/khonsulabs/bonsaidb/blob/main/examples/basic-local/examples/basic-local-multidb.rs + */ +pub async fn setup() -> AnyResult { + let conf_path = get_app_storage_dir(); + let storage_configuration = StorageConfiguration::new(conf_path.join("main.bonsaidb")) + .with_schema::()? + .with_schema::()?; + + let storage = AsyncStorage::open(storage_configuration).await?; + + let tracks = storage.create_database::("tracks", true).await?; + let playlists = storage + .create_database::("playlists", true) + .await?; + + Ok(DB { tracks, playlists }) +} + +/** + * Database plugin, exposing commands and state + */ +pub fn init(db: DB) -> TauriPlugin { + Builder::::new("database") + .invoke_handler(tauri::generate_handler![ + get_all_tracks, + get_tracks, + remove_tracks, + get_tracks, + get_all_playlists, + get_playlist, + get_playlist, + create_playlist, + rename_playlist, + set_playlist_tracks, + delete_playlist, + reset, + import_tracks_to_library, + ]) + .setup(|app_handle, _api| { + app_handle.manage(db); + Ok(()) + }) + .build() +} diff --git a/src-tauri/src/plugins/debug.rs b/src-tauri/src/plugins/debug.rs new file mode 100644 index 000000000..71ceb4674 --- /dev/null +++ b/src-tauri/src/plugins/debug.rs @@ -0,0 +1,13 @@ +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::Runtime; + +pub fn init() -> TauriPlugin { + Builder::::new("debug") + .on_webview_ready(|window| { + #[cfg(dev)] + { + window.open_devtools(); + } + }) + .build() +} diff --git a/src-tauri/src/plugins/default_view.rs b/src-tauri/src/plugins/default_view.rs new file mode 100644 index 000000000..a58f973dd --- /dev/null +++ b/src-tauri/src/plugins/default_view.rs @@ -0,0 +1,38 @@ +use log::info; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{Manager, Runtime, State}; + +use crate::libs::error::AnyResult; +use crate::plugins::config::{ConfigManager, DefaultView}; + +#[tauri::command] +pub fn set(config_manager: State, default_view: DefaultView) -> AnyResult<()> { + info!("Default view set to '{:?}'", default_view); + config_manager.set_default_view(default_view); + Ok(()) +} + +/** + * Set the default view on application load based on user preference + */ +pub fn init() -> TauriPlugin { + Builder::::new("default-view") + .invoke_handler(tauri::generate_handler![set]) + .on_webview_ready(|mut window| { + if window.label().eq("main") { + let config_manager = window.state::(); + let mut url = window.url(); + let default_view = config_manager.get().default_view; + + let fragment = match default_view { + DefaultView::Library => "/library", + DefaultView::Playlists => "/playlists", + }; + + info!("Navigating to '{}'", fragment); + url.set_fragment(Some(fragment)); + window.navigate(url); + } + }) + .build() +} diff --git a/src-tauri/src/plugins/macos.rs b/src-tauri/src/plugins/macos.rs new file mode 100644 index 000000000..d006420ae --- /dev/null +++ b/src-tauri/src/plugins/macos.rs @@ -0,0 +1,27 @@ +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{Manager, Runtime}; + +/** + * Plugin in charge on making sure closing the app does not stop the audio + */ +pub fn init() -> TauriPlugin { + Builder::::new("sleepblocker") + .on_window_ready(|win| { + // Prevent macOS to kill the player when closing the main window. Instead, + // the window should be hidden and re-shown when invoking it again. + #[cfg(target_os = "macos")] + { + // Weird, should "win" be a reference instead maybe? + let window = win.clone(); + + win.on_window_event(move |event| match event { + tauri::WindowEvent::CloseRequested { api, .. } => { + window.app_handle().hide().unwrap(); + api.prevent_close(); + } + _ => {} + }); + } + }) + .build() +} diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs new file mode 100644 index 000000000..b9e41c81d --- /dev/null +++ b/src-tauri/src/plugins/mod.rs @@ -0,0 +1,31 @@ +/** + * Those are a bunch of Tauri plugins used to interact with the Operating Systems + * features, like global menu, sleep-blocker, dock, thumbar, etc. + * + * It also holds the different DB creations and various helpers. + */ +pub mod debug; + +/** + * Core features + */ +pub mod app_menu; +pub mod cover; +pub mod shell_extension; + +/** + * Stores + */ +pub mod config; +pub mod database; + +/** + * Settings-related plugins + */ +pub mod default_view; +pub mod sleepblocker; + +/** + * OS-specific plugins + */ +pub mod macos; diff --git a/src-tauri/src/plugins/shell_extension.rs b/src-tauri/src/plugins/shell_extension.rs new file mode 100644 index 000000000..96c2263d2 --- /dev/null +++ b/src-tauri/src/plugins/shell_extension.rs @@ -0,0 +1,112 @@ +// Stolen and adapted from https://github.com/tauri-apps/plugins-workspace/issues/999 +// TODO: make sure it works on Windows and Linux + +use std::process::Command; + +#[cfg(not(target_os = "windows"))] +use std::path::PathBuf; + +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{generate_handler, Runtime}; + +#[cfg(target_os = "linux")] +use { + std::sync::Mutex, + std::time::Duration, + tauri::{Manager, State}, +}; + +/** + * Show item in folder for Linux, using dbus + */ +#[cfg(target_os = "linux")] +#[tauri::command] +pub fn show_item_in_folder(path: String, dbus_state: State) -> Result<(), String> { + let dbus_guard = dbus_state.0.lock().map_err(|e| e.to_string())?; + + // see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76 + if dbus_guard.is_none() || path.contains(",") { + let mut path_buf = PathBuf::from(&path); + let new_path = match path_buf.is_dir() { + true => path, + false => { + path_buf.pop(); + path_buf.into_os_string().into_string().unwrap() + } + }; + Command::new("xdg-open") + .arg(&new_path) + .spawn() + .map_err(|e| format!("{e:?}"))?; + } else { + // https://docs.rs/dbus/latest/dbus/ + let dbus = dbus_guard.as_ref().unwrap(); + let proxy = dbus.with_proxy( + "org.freedesktop.FileManager1", + "/org/freedesktop/FileManager1", + Duration::from_secs(5), + ); + let (_,): (bool,) = proxy + .method_call( + "org.freedesktop.FileManager1", + "ShowItems", + (vec![format!("file://{path}")], ""), + ) + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +/** + * Show item in folder + * - for macOS, using Finder + * - for Windows, using Explorer + */ +#[cfg(not(target_os = "linux"))] +#[tauri::command] +pub fn show_item_in_folder(path: String) -> Result<(), String> { + #[cfg(target_os = "windows")] + { + Command::new("explorer") + .args(["/select,", &path]) // The comma after select is not a typo + .spawn() + .map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "macos")] + { + let path_buf = PathBuf::from(&path); + if path_buf.is_dir() { + Command::new("open") + .args([&path]) + .spawn() + .map_err(|e| e.to_string())?; + } else { + Command::new("open") + .args(["-R", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +#[cfg(target_os = "linux")] +pub struct DbusState(Mutex>); + +/** + * Plugin in charge of adding shell-related extended features + */ +pub fn init() -> TauriPlugin { + Builder::::new("shell-extension") + .invoke_handler(generate_handler![show_item_in_folder]) + .setup(|_app_handle, _| { + #[cfg(target_os = "linux")] + _app_handle.manage(DbusState(Mutex::new( + dbus::blocking::SyncConnection::new_session().ok(), + ))); + + Ok(()) + }) + .build() +} diff --git a/src-tauri/src/plugins/sleepblocker.rs b/src-tauri/src/plugins/sleepblocker.rs new file mode 100644 index 000000000..ffe2c7967 --- /dev/null +++ b/src-tauri/src/plugins/sleepblocker.rs @@ -0,0 +1,58 @@ +use std::sync::Mutex; + +use log::info; +use nosleep::{NoSleep, NoSleepType}; +use tauri::plugin::{Builder, TauriPlugin}; +use tauri::{Manager, Runtime, State}; + +use crate::libs::error::AnyResult; +use crate::plugins::config::ConfigManager; + +pub struct NoSleepInstance(Mutex); + +#[tauri::command] +pub fn enable( + config_manager: State, + nosleep: State, +) -> AnyResult<()> { + config_manager.set_sleepblocker(true); + + nosleep + .0 + .lock() + .unwrap() // TODO, use ? + .start(NoSleepType::PreventUserIdleSystemSleep)?; + + info!("Enabled sleepblocker"); + Ok(()) +} + +#[tauri::command] +pub fn disable( + config_manager: State, + nosleep: State, +) -> AnyResult<()> { + config_manager.set_sleepblocker(false); + + nosleep + .0 + .lock() + .unwrap() // TODO, use ? + .stop()?; + + info!("Disabled sleepblocker"); + Ok(()) +} + +/** + * Plugin in charge of preventing the app from going to sleep + */ +pub fn init() -> TauriPlugin { + Builder::::new("sleepblocker") + .invoke_handler(tauri::generate_handler![enable, disable]) + .setup(|app_handle, _api| { + app_handle.manage(NoSleepInstance(Mutex::new(NoSleep::new().unwrap()))); + Ok(()) + }) + .build() +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 000000000..f55d575e2 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,48 @@ +{ + "productName": "Museeks", + "version": "0.20.0", + "identifier": "io.museeks.app", + "build": { + "beforeDevCommand": "yarn dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "yarn build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Museeks", + "visible": false, + "hiddenTitle": true, + "titleBarStyle": "Overlay", + "height": 550, + "minHeight": 550, + "width": 900, + "minWidth": 900, + "fullscreen": false, + "resizable": true, + "fileDropEnabled": false + } + ], + "security": { + "assetProtocol": { + "enable": true, + "scope": ["**/*"] + }, + "csp": "default-src 'none'; img-src 'self' data:; media-src 'self' asset: https://asset.localhost http://asset.localhost; child-src 'self'; object-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src ipc: http://ipc.localhost 'self' https://api.github.com; font-src 'self' data:" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "resources": ["icons/icon.png"] + } +} diff --git a/src/shared/assets/icons/player-next.svg b/src/assets/icons/player-next.svg similarity index 100% rename from src/shared/assets/icons/player-next.svg rename to src/assets/icons/player-next.svg diff --git a/src/shared/assets/icons/player-pause.svg b/src/assets/icons/player-pause.svg similarity index 100% rename from src/shared/assets/icons/player-pause.svg rename to src/assets/icons/player-pause.svg diff --git a/src/shared/assets/icons/player-play.svg b/src/assets/icons/player-play.svg similarity index 100% rename from src/shared/assets/icons/player-play.svg rename to src/assets/icons/player-play.svg diff --git a/src/shared/assets/icons/player-previous.svg b/src/assets/icons/player-previous.svg similarity index 100% rename from src/shared/assets/icons/player-previous.svg rename to src/assets/icons/player-previous.svg diff --git a/src/shared/assets/icons/player-queue.svg b/src/assets/icons/player-queue.svg similarity index 100% rename from src/shared/assets/icons/player-queue.svg rename to src/assets/icons/player-queue.svg diff --git a/src/shared/assets/icons/player-repeat-one.svg b/src/assets/icons/player-repeat-one.svg similarity index 100% rename from src/shared/assets/icons/player-repeat-one.svg rename to src/assets/icons/player-repeat-one.svg diff --git a/src/shared/assets/icons/player-repeat.svg b/src/assets/icons/player-repeat.svg similarity index 100% rename from src/shared/assets/icons/player-repeat.svg rename to src/assets/icons/player-repeat.svg diff --git a/src/shared/assets/icons/player-shuffle.svg b/src/assets/icons/player-shuffle.svg similarity index 100% rename from src/shared/assets/icons/player-shuffle.svg rename to src/assets/icons/player-shuffle.svg diff --git a/src/shared/assets/icons/windows/backward-disabled.ico b/src/assets/icons/windows/backward-disabled.ico similarity index 100% rename from src/shared/assets/icons/windows/backward-disabled.ico rename to src/assets/icons/windows/backward-disabled.ico diff --git a/src/shared/assets/icons/windows/backward.ico b/src/assets/icons/windows/backward.ico similarity index 100% rename from src/shared/assets/icons/windows/backward.ico rename to src/assets/icons/windows/backward.ico diff --git a/src/shared/assets/icons/windows/forward-disabled.ico b/src/assets/icons/windows/forward-disabled.ico similarity index 100% rename from src/shared/assets/icons/windows/forward-disabled.ico rename to src/assets/icons/windows/forward-disabled.ico diff --git a/src/shared/assets/icons/windows/forward.ico b/src/assets/icons/windows/forward.ico similarity index 100% rename from src/shared/assets/icons/windows/forward.ico rename to src/assets/icons/windows/forward.ico diff --git a/src/shared/assets/icons/windows/pause-disabled.ico b/src/assets/icons/windows/pause-disabled.ico similarity index 100% rename from src/shared/assets/icons/windows/pause-disabled.ico rename to src/assets/icons/windows/pause-disabled.ico diff --git a/src/shared/assets/icons/windows/pause.ico b/src/assets/icons/windows/pause.ico similarity index 100% rename from src/shared/assets/icons/windows/pause.ico rename to src/assets/icons/windows/pause.ico diff --git a/src/shared/assets/icons/windows/play-disabled.ico b/src/assets/icons/windows/play-disabled.ico similarity index 100% rename from src/shared/assets/icons/windows/play-disabled.ico rename to src/assets/icons/windows/play-disabled.ico diff --git a/src/shared/assets/icons/windows/play.ico b/src/assets/icons/windows/play.ico similarity index 100% rename from src/shared/assets/icons/windows/play.ico rename to src/assets/icons/windows/play.ico diff --git a/src/shared/assets/logos/museeks-128.png b/src/assets/logos/museeks-128.png similarity index 100% rename from src/shared/assets/logos/museeks-128.png rename to src/assets/logos/museeks-128.png diff --git a/src/shared/assets/logos/museeks-128@2x.png b/src/assets/logos/museeks-128@2x.png similarity index 100% rename from src/shared/assets/logos/museeks-128@2x.png rename to src/assets/logos/museeks-128@2x.png diff --git a/src/shared/assets/logos/museeks-32.png b/src/assets/logos/museeks-32.png similarity index 100% rename from src/shared/assets/logos/museeks-32.png rename to src/assets/logos/museeks-32.png diff --git a/src/shared/assets/logos/museeks-32@2x.png b/src/assets/logos/museeks-32@2x.png similarity index 100% rename from src/shared/assets/logos/museeks-32@2x.png rename to src/assets/logos/museeks-32@2x.png diff --git a/src/shared/assets/logos/museeks-48.png b/src/assets/logos/museeks-48.png similarity index 100% rename from src/shared/assets/logos/museeks-48.png rename to src/assets/logos/museeks-48.png diff --git a/src/shared/assets/logos/museeks-48@2x.png b/src/assets/logos/museeks-48@2x.png similarity index 100% rename from src/shared/assets/logos/museeks-48@2x.png rename to src/assets/logos/museeks-48@2x.png diff --git a/src/shared/assets/logos/museeks-64.png b/src/assets/logos/museeks-64.png similarity index 100% rename from src/shared/assets/logos/museeks-64.png rename to src/assets/logos/museeks-64.png diff --git a/src/shared/assets/logos/museeks-64@2x.png b/src/assets/logos/museeks-64@2x.png similarity index 100% rename from src/shared/assets/logos/museeks-64@2x.png rename to src/assets/logos/museeks-64@2x.png diff --git a/src/shared/assets/logos/museeks-tray-dark.png b/src/assets/logos/museeks-tray-dark.png similarity index 100% rename from src/shared/assets/logos/museeks-tray-dark.png rename to src/assets/logos/museeks-tray-dark.png diff --git a/src/shared/assets/logos/museeks-tray-dark@2x.png b/src/assets/logos/museeks-tray-dark@2x.png similarity index 100% rename from src/shared/assets/logos/museeks-tray-dark@2x.png rename to src/assets/logos/museeks-tray-dark@2x.png diff --git a/src/shared/assets/logos/museeks-tray.ico b/src/assets/logos/museeks-tray.ico similarity index 100% rename from src/shared/assets/logos/museeks-tray.ico rename to src/assets/logos/museeks-tray.ico diff --git a/src/shared/assets/logos/museeks-tray.png b/src/assets/logos/museeks-tray.png similarity index 100% rename from src/shared/assets/logos/museeks-tray.png rename to src/assets/logos/museeks-tray.png diff --git a/src/shared/assets/logos/museeks.icns b/src/assets/logos/museeks.icns similarity index 100% rename from src/shared/assets/logos/museeks.icns rename to src/assets/logos/museeks.icns diff --git a/src/shared/assets/logos/museeks.ico b/src/assets/logos/museeks.ico similarity index 100% rename from src/shared/assets/logos/museeks.ico rename to src/assets/logos/museeks.ico diff --git a/src/shared/assets/logos/museeks.png b/src/assets/logos/museeks.png similarity index 100% rename from src/shared/assets/logos/museeks.png rename to src/assets/logos/museeks.png diff --git a/src/shared/assets/placeholder.png b/src/assets/placeholder.png similarity index 100% rename from src/shared/assets/placeholder.png rename to src/assets/placeholder.png diff --git a/src/renderer/components/AudioOutputSelect/AudioOutputSelect.tsx b/src/components/AudioOutputSelect/AudioOutputSelect.tsx similarity index 63% rename from src/renderer/components/AudioOutputSelect/AudioOutputSelect.tsx rename to src/components/AudioOutputSelect/AudioOutputSelect.tsx index cfe2dac2e..d44c2ea36 100644 --- a/src/renderer/components/AudioOutputSelect/AudioOutputSelect.tsx +++ b/src/components/AudioOutputSelect/AudioOutputSelect.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; -import logger from '../../../shared/lib/logger'; import * as Setting from '../Setting/Setting'; +import { logAndNotifyError } from '../../lib/utils'; type Props = { defaultValue: string; @@ -15,13 +15,24 @@ export default function AudioOutputSelect(props: Props) { useEffect(() => { const refreshDevices = async () => { try { + // Webkit sucks, we need to request permissions for inputs, when we only + // need outputs + // const test = await navigator.mediaDevices.getUserMedia({ + // audio: true, + // video: false, + // }); + // console.log('media', test); const devices = await navigator.mediaDevices.enumerateDevices(); - - setDevices(devices.filter((device) => device.kind === 'audiooutput')); + // console.log('all devices', devices); + const audioDevices = devices.filter( + (device) => device.kind === 'audiooutput', + ); + // console.log('audioDevices', audioDevices); + setDevices(audioDevices); } catch (err) { setDevices([]); setHasError(true); - logger.warn(err); + logAndNotifyError(err); } }; @@ -34,17 +45,17 @@ export default function AudioOutputSelect(props: Props) { if (!devices) { return ( - + ); } if (hasError) { return ( - + ); } diff --git a/src/renderer/components/Cover/Cover.module.css b/src/components/Cover/Cover.module.css similarity index 73% rename from src/renderer/components/Cover/Cover.module.css rename to src/components/Cover/Cover.module.css index c9bdbd3ee..8354305aa 100644 --- a/src/renderer/components/Cover/Cover.module.css +++ b/src/components/Cover/Cover.module.css @@ -7,16 +7,20 @@ align-items: center; justify-content: center; box-sizing: content-box; + background-color: var(--header-bg-softer); } .empty { border: solid 1px var(--border-color); - border-width: 0 1px; box-sizing: border-box; } +.no_border { + border-width: 0; +} + .cover__note { font-family: monospace; /* the sexy one */ - font-size: 28px; - line-height: 28px; + font-size: 1em; + line-height: 1em; } diff --git a/src/components/Cover/Cover.tsx b/src/components/Cover/Cover.tsx new file mode 100644 index 000000000..136812675 --- /dev/null +++ b/src/components/Cover/Cover.tsx @@ -0,0 +1,37 @@ +import * as AspectRatio from '@radix-ui/react-aspect-ratio'; +import cx from 'classnames'; + +import type { Track } from '../../generated/typings'; +import useCover from '../../hooks/useCover'; + +import styles from './Cover.module.css'; + +type Props = { + track: Track; + noBorder?: boolean; +}; + +export default function Cover(props: Props) { + const coverPath = useCover(props.track); + + if (coverPath) { + return ( + + Album cover + + ); + } + + const classes = cx(styles.cover, styles.empty, { + [styles.no_border]: props.noBorder, + }); + + return ( + +
+ {/** billion dollar problem: convert emoji to text, good luck 🎵 */} +
+
+
+ ); +} diff --git a/src/renderer/components/DropzoneImport/DropzoneImport.module.css b/src/components/DropzoneImport/DropzoneImport.module.css similarity index 100% rename from src/renderer/components/DropzoneImport/DropzoneImport.module.css rename to src/components/DropzoneImport/DropzoneImport.module.css diff --git a/src/components/DropzoneImport/DropzoneImport.tsx b/src/components/DropzoneImport/DropzoneImport.tsx new file mode 100644 index 000000000..fdf5caa49 --- /dev/null +++ b/src/components/DropzoneImport/DropzoneImport.tsx @@ -0,0 +1,66 @@ +import cx from 'classnames'; +// import { getCurrent } from '@tauri-apps/api/window'; +import { /** useEffect,*/ useState } from 'react'; +// import { getCurrent } from '@tauri-apps/api/window'; + +// import { logAndNotifyError } from '../../lib/utils'; + +import styles from './DropzoneImport.module.css'; + +export default function DropzoneImport() { + const [isShown, setIsShown] = useState(false); + + // const unlisten = await getCurrent().onFileDropEvent((event) => { + // if (event.payload.type === 'hover') { + // console.log('User hovering', event.payload.paths); + // } else if (event.payload.type === 'drop') { + // console.log('User dropped', event.payload.paths); + // } else { + // console.log('File drop cancelled'); + // } + // }); + + // useEffect(() => { + // async function attachFileDropEvent() { + // await getCurrent() + // .onFileDropEvent((event) => { + // if (event.payload.type === 'hover') { + // setIsShown(true); + // } else if (event.payload.type === 'drop') { + // console.log(event.payload.paths); + // setIsShown(false); + // } else { + // setIsShown(false); + // } + // }) + // .catch(logAndNotifyError); + // } + + // attachFileDropEvent().catch(logAndNotifyError); + + // return getCurrent().clearEffects; + // }, []); + + const classes = cx(styles.dropzone, { + [styles.shown]: isShown, + }); + + // TODO: Fix this, drop files from TAURI instead + // const files = item.files.map((file) => file.path); + // libraryAPI + // .add(files) + // .then((/* _importedTracks */) => { + // // TODO: Import to playlist here + // }) + // .catch((err) => { + // logger.warn(err); + // }); + return ( +
+
Add music to the library
+
+ Drop files or folders anywhere +
+
+ ); +} diff --git a/src/renderer/components/Events/AppEvents.tsx b/src/components/Events/AppEvents.tsx similarity index 55% rename from src/renderer/components/Events/AppEvents.tsx rename to src/components/Events/AppEvents.tsx index 721d87b09..fe79473cf 100644 --- a/src/renderer/components/Events/AppEvents.tsx +++ b/src/components/Events/AppEvents.tsx @@ -1,14 +1,10 @@ import { useEffect } from 'react'; -import type { IpcRendererEvent } from 'electron'; import player from '../../lib/player'; -import { preventNativeDefault } from '../../lib/utils-events'; -import SettingsAPI from '../../stores/SettingsAPI'; -import channels from '../../../shared/lib/ipc-channels'; -import type { Theme } from '../../../shared/types/museeks'; -import logger from '../../../shared/lib/logger'; - -const { ipcRenderer } = window.ElectronAPI; +import { isDev, preventNativeDefault } from '../../lib/utils-events'; +// import SettingsAPI from '../../stores/SettingsAPI'; +// import type { Theme } from '../../types/museeks'; +import { logAndNotifyError } from '../../lib/utils'; /** * Handle app-level IPC Events init and cleanup @@ -19,19 +15,25 @@ function AppEvents() { window.addEventListener('dragover', preventNativeDefault, false); window.addEventListener('drop', preventNativeDefault, false); - // Auto-update theme if set to system and the native theme changes - function updateTheme(_event: IpcRendererEvent, theme: unknown) { - SettingsAPI.applyThemeToUI(theme as Theme); + // Disable the default context menu on production builds + if (!isDev()) { + window.addEventListener('contextmenu', preventNativeDefault); } - ipcRenderer.on(channels.THEME_APPLY, updateTheme); + // TODO: fix that https://github.com/tauri-apps/tauri/issues/5279 + // Auto-update theme if set to system and the native theme changes + // function updateTheme(_event: IpcRendererEvent, theme: unknown) { + // SettingsAPI.applyThemeToUI(theme as Theme); + // } + + // ipcRenderer.on(channels.THEME_APPLY, updateTheme); // Support for multiple audio output async function updateOutputDevice() { try { await player.setOutputDevice('default'); } catch (err) { - logger.warn(err); + logAndNotifyError(err); } } @@ -41,7 +43,11 @@ function AppEvents() { window.removeEventListener('dragover', preventNativeDefault, false); window.removeEventListener('drop', preventNativeDefault, false); - ipcRenderer.off(channels.THEME_APPLY, updateTheme); + if (!isDev()) { + window.removeEventListener('contextmenu', preventNativeDefault); + } + + // ipcRenderer.off(channels.THEME_APPLY, updateTheme); navigator.mediaDevices.removeEventListener( 'devicechange', diff --git a/src/renderer/components/Events/GlobalKeyBindings.tsx b/src/components/Events/GlobalKeyBindings.tsx similarity index 94% rename from src/renderer/components/Events/GlobalKeyBindings.tsx rename to src/components/Events/GlobalKeyBindings.tsx index bec92c54a..59cab3f84 100644 --- a/src/renderer/components/Events/GlobalKeyBindings.tsx +++ b/src/components/Events/GlobalKeyBindings.tsx @@ -13,7 +13,7 @@ function GlobalKeyBindings() { const navigate = useNavigate(); const playerAPI = usePlayerAPI(); - // App shortcuts (not using Electron's global shortcuts API to avoid conflicts + // App shortcuts (not using global shortcuts API to avoid conflicts // with other applications) const onKey = useCallback( async (e: KeyboardEvent) => { diff --git a/src/renderer/components/Events/IPCNavigationEvents.tsx b/src/components/Events/IPCNavigationEvents.tsx similarity index 56% rename from src/renderer/components/Events/IPCNavigationEvents.tsx rename to src/components/Events/IPCNavigationEvents.tsx index 93381a64f..be4efadb3 100644 --- a/src/renderer/components/Events/IPCNavigationEvents.tsx +++ b/src/components/Events/IPCNavigationEvents.tsx @@ -1,10 +1,8 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { getCurrent } from '@tauri-apps/api/window'; import { usePlayerAPI } from '../../stores/usePlayerStore'; -import channels from '../../../shared/lib/ipc-channels'; - -const { ipcRenderer } = window.ElectronAPI; /** * Handle app-level IPC Navigation events @@ -27,14 +25,23 @@ function IPCNavigationEvents() { } // Shortcuts from the application menu - ipcRenderer.on(channels.MENU_GO_TO_LIBRARY, goToLibrary); - ipcRenderer.on(channels.MENU_GO_TO_PLAYLISTS, goToPlaylists); - ipcRenderer.on(channels.MENU_JUMP_TO_PLAYING_TRACK, goToPlayingTrack); + const unlistenGoToLibrary = getCurrent().listen( + 'MENU_GO_TO_LIBRARY', + goToLibrary, + ); + const unlistenGoToPlaylists = getCurrent().listen( + 'MENU_GO_TO_PLAYLISTS', + goToPlaylists, + ); + const unlistenGoToPlayingTrack = getCurrent().listen( + 'MENU_JUMP_TO_PLAYING_TRACK', + goToPlayingTrack, + ); return function cleanup() { - ipcRenderer.off(channels.MENU_GO_TO_LIBRARY, goToLibrary); - ipcRenderer.off(channels.MENU_GO_TO_PLAYLISTS, goToPlaylists); - ipcRenderer.off(channels.MENU_JUMP_TO_PLAYING_TRACK, goToPlayingTrack); + unlistenGoToLibrary.then((u) => u()); + unlistenGoToPlaylists.then((u) => u()); + unlistenGoToPlayingTrack.then((u) => u()); }; }, [navigate, playerAPI]); diff --git a/src/renderer/components/Events/IPCPlayerEvents.tsx b/src/components/Events/IPCPlayerEvents.tsx similarity index 83% rename from src/renderer/components/Events/IPCPlayerEvents.tsx rename to src/components/Events/IPCPlayerEvents.tsx index 7755af7f3..eccc58598 100644 --- a/src/renderer/components/Events/IPCPlayerEvents.tsx +++ b/src/components/Events/IPCPlayerEvents.tsx @@ -1,26 +1,18 @@ import { useEffect } from 'react'; -import channels from '../../../shared/lib/ipc-channels'; +import channels from '../../lib/ipc-channels'; import { usePlayerAPI } from '../../stores/usePlayerStore'; -import useCurrentViewTracks from '../../hooks/useCurrentViewTracks'; import player from '../../lib/player'; -const { ipcRenderer } = window.ElectronAPI; - /** * Handle app-level IPC Events init and cleanup */ function IPCPlayerEvents() { const playerAPI = usePlayerAPI(); - const tracks = useCurrentViewTracks(); useEffect(() => { function play() { - if (player.getTrack()) { - playerAPI.play(); - } else { - playerAPI.start(tracks); - } + playerAPI.play(); } function onPlay() { @@ -57,7 +49,7 @@ function IPCPlayerEvents() { player.getAudio().removeEventListener('play', onPlay); player.getAudio().removeEventListener('pause', onPause); }; - }, [playerAPI, tracks]); + }, [playerAPI]); return null; } diff --git a/src/components/Events/LibraryEvents.tsx b/src/components/Events/LibraryEvents.tsx new file mode 100644 index 000000000..52d0252df --- /dev/null +++ b/src/components/Events/LibraryEvents.tsx @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; +import { listen } from '@tauri-apps/api/event'; + +import { useLibraryAPI } from '../../stores/useLibraryStore'; +import { IPCEvent, Progress } from '../../generated/typings'; + +/** + * Handle Library-related app events, like refreshing and progress status + */ +function LibraryEvents() { + const { setRefresh } = useLibraryAPI(); + + useEffect(() => { + const promise = listen( + 'LibraryScanProgress' satisfies IPCEvent, + ({ payload }) => { + setRefresh(payload.current, payload.total); + }, + ); + + return () => { + promise.then((unlisten) => unlisten()); + }; + }, [setRefresh]); + + return null; +} + +export default LibraryEvents; diff --git a/src/renderer/components/Events/MediaSessionEvents.tsx b/src/components/Events/MediaSessionEvents.tsx similarity index 59% rename from src/renderer/components/Events/MediaSessionEvents.tsx rename to src/components/Events/MediaSessionEvents.tsx index da3006977..b550ca5f4 100644 --- a/src/renderer/components/Events/MediaSessionEvents.tsx +++ b/src/components/Events/MediaSessionEvents.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { usePlayerAPI } from '../../stores/usePlayerStore'; import player from '../../lib/player'; +import library from '../../lib/library'; /** * Integration for MediaSession (mpris, macOS player controls etc)... @@ -14,24 +15,28 @@ function MediaSessionEvents() { player.getAudio().addEventListener('play', onAudioPlay); player.getAudio().addEventListener('pause', onAudioPause); - navigator.mediaSession.setActionHandler('play', () => playerAPI.play()); - navigator.mediaSession.setActionHandler('pause', () => playerAPI.pause()); - navigator.mediaSession.setActionHandler('previoustrack', () => - playerAPI.previous(), - ); - navigator.mediaSession.setActionHandler('nexttrack', () => - playerAPI.next(), - ); + if ('mediaSession' in navigator) { + navigator.mediaSession.setActionHandler('play', () => playerAPI.play()); + navigator.mediaSession.setActionHandler('pause', () => playerAPI.pause()); + navigator.mediaSession.setActionHandler('previoustrack', () => + playerAPI.previous(), + ); + navigator.mediaSession.setActionHandler('nexttrack', () => + playerAPI.next(), + ); + } return function cleanup() { player.getAudio().removeEventListener('loadstart', syncArtwork); player.getAudio().removeEventListener('play', onAudioPlay); player.getAudio().removeEventListener('pause', onAudioPause); - navigator.mediaSession.setActionHandler('play', null); - navigator.mediaSession.setActionHandler('pause', null); - navigator.mediaSession.setActionHandler('previoustrack', null); - navigator.mediaSession.setActionHandler('nexttrack', null); + if ('mediaSession' in navigator) { + navigator.mediaSession.setActionHandler('play', null); + navigator.mediaSession.setActionHandler('pause', null); + navigator.mediaSession.setActionHandler('previoustrack', null); + navigator.mediaSession.setActionHandler('nexttrack', null); + } }; }, [playerAPI]); @@ -48,11 +53,11 @@ async function syncArtwork() { const track = player.getTrack(); if (track) { - const cover = await window.MuseeksAPI.covers.getCoverAsBase64(track); + const cover = await library.getCover(track.path); navigator.mediaSession.metadata = new MediaMetadata({ title: track.title, - artist: track.artist.join(', '), + artist: track.artists.join(', '), album: track.album, artwork: cover ? [{ src: cover }] : undefined, }); diff --git a/src/renderer/components/Events/PlayerEvents.tsx b/src/components/Events/PlayerEvents.tsx similarity index 61% rename from src/renderer/components/Events/PlayerEvents.tsx rename to src/components/Events/PlayerEvents.tsx index 69ae424cc..22eae5e18 100644 --- a/src/renderer/components/Events/PlayerEvents.tsx +++ b/src/components/Events/PlayerEvents.tsx @@ -1,9 +1,14 @@ import { useEffect } from 'react'; +import { sendNotification } from '@tauri-apps/plugin-notification'; +import { getCurrent } from '@tauri-apps/api/window'; import { usePlayerAPI } from '../../stores/usePlayerStore'; import { useToastsAPI } from '../../stores/useToastsStore'; import { useLibraryAPI } from '../../stores/useLibraryStore'; import player from '../../lib/player'; +import config from '../../lib/config'; +import { logAndNotifyError } from '../../lib/utils'; +import library from '../../lib/library'; const AUDIO_ERRORS = { aborted: 'The video playback was aborted.', @@ -21,23 +26,6 @@ function PlayerEvents() { const libraryAPI = useLibraryAPI(); const toastsAPI = useToastsAPI(); - // // If no queue is provided, we create it based on the screen the user is on - // if (!newQueue) { - // if (hash.startsWith('#/playlists')) { - // newQueue = library.tracks.playlist; - // newQueue = []; - // } else { - // // we are either on the library or the settings view - // // so let's play the whole library - // // Because the tracks in the store are not ordered, let's filter - // // and sort everything - // const { sort, search } = library; - // newQueue = library.tracks; - - // newQueue = sortTracks(filterTracks(newQueue, search), SORT_ORDERS[sort.by][sort.order]); - // } - // } - useEffect(() => { function handleAudioError(e: ErrorEvent) { playerAPI.stop(); @@ -66,22 +54,41 @@ function PlayerEvents() { } } - function incrementPlayCount() { - if (player.isThresholdReached()) { - const track = player.getTrack(); - if (track) libraryAPI.incrementPlayCount(track); + async function notifyTrackChange() { + const track = player.getTrack(); + const isEnabled = await config.get('notifications'); + const isMinimized = await getCurrent() + .isMinimized() + .catch(logAndNotifyError); + const isFocused = await getCurrent().isFocused().catch(logAndNotifyError); + + if (track == null || !isEnabled || isFocused || !isMinimized) { + return; } + + // FIXME: cover is not working as intended + const cover = await library.getCover(track.path); + + sendNotification({ + title: track.title, + body: `${track.artists.join(', ')}\n${track.album}`, + silent: true, + icon: cover ?? undefined, + // TODO: onClick event https://github.com/tauri-apps/tauri/issues/3698 + // show + focus + unminimize + }); } + // Bind player events // Audio Events - player.getAudio().addEventListener('ended', playerAPI.next); + player.getAudio().addEventListener('play', notifyTrackChange); player.getAudio().addEventListener('error', handleAudioError); - player.getAudio().addEventListener('timeupdate', incrementPlayCount); + player.getAudio().addEventListener('ended', playerAPI.next); return function cleanup() { + player.getAudio().removeEventListener('play', notifyTrackChange); player.getAudio().removeEventListener('ended', playerAPI.next); player.getAudio().removeEventListener('error', handleAudioError); - player.getAudio().removeEventListener('timeupdate', incrementPlayCount); }; }, [libraryAPI, toastsAPI, playerAPI]); diff --git a/src/renderer/components/Footer/Footer.module.css b/src/components/Footer/Footer.module.css similarity index 100% rename from src/renderer/components/Footer/Footer.module.css rename to src/components/Footer/Footer.module.css diff --git a/src/renderer/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx similarity index 94% rename from src/renderer/components/Footer/Footer.tsx rename to src/components/Footer/Footer.tsx index 664346fd3..29a609fa5 100644 --- a/src/renderer/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -12,12 +12,12 @@ export default function Footer() { const refreshing = useLibraryStore((state) => state.refreshing); const getStatusContent = useCallback(() => { - const { processed, total } = refresh; + const { current, total } = refresh; if (refreshing) { // Sketchy const isScanning = total === 0; - const progress = total > 0 ? Math.round((processed / total) * 100) : 100; + const progress = total > 0 ? Math.round((current / total) * 100) : 100; return (
@@ -30,7 +30,7 @@ export default function Footer() {
{total > 0 && (
- {processed} / {total} + {current} / {total}
)} diff --git a/src/renderer/components/Header/Header.module.css b/src/components/Header/Header.module.css similarity index 75% rename from src/renderer/components/Header/Header.module.css rename to src/components/Header/Header.module.css index bf0e89536..3a2cb8f1c 100644 --- a/src/renderer/components/Header/Header.module.css +++ b/src/components/Header/Header.module.css @@ -1,10 +1,10 @@ -:global(.os__darwin) .header__mainControls { - padding-left: 65px; /* let some space for titleBarStyle */ +:global(.os__macos) .header__mainControls { + padding-left: 60px; /* let some space for titleBarStyle */ } /* The native frame may be light, so we need to increase the contrast between the frame and the header */ -:global(.os__win32), +:global(.os__windows), :global(.os__linux) { .header { border-top: 1px solid var(--border-color); @@ -14,6 +14,7 @@ .header { border-bottom: solid 1px var(--border-color); background-color: var(--header-bg); + box-sizing: content-box; color: var(--header-color); padding: 0 10px; display: flex; @@ -21,9 +22,6 @@ justify-content: space-between; height: 50px; flex: 0 0 auto; - - /* Draggable region (zone able to move the window) */ - -webkit-app-region: drag; } .header__mainControls { @@ -31,10 +29,10 @@ display: flex; align-items: center; padding-right: 10px; + min-width: 200px; } .header__search { - -webkit-app-region: no-drag; margin-left: 12px; flex: 0 0 auto; display: flex; @@ -45,11 +43,7 @@ flex: 1 1 auto; min-width: 0; max-width: 600px; -} - -.header__queue { display: flex; - align-items: center; } .queueToggle { @@ -60,11 +54,10 @@ background: transparent; font-size: 14px; box-shadow: none; - -webkit-app-region: no-drag; + flex-shrink: 0; } .queueContainer { - -webkit-app-region: no-drag; display: none; position: absolute; z-index: 1000; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 000000000..45c3f801c --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,54 @@ +import Icon from 'react-fontawesome'; +import * as Popover from '@radix-ui/react-popover'; + +import Queue from '../Queue/Queue'; +import PlayingBar from '../PlayingBar/PlayingBar'; +import PlayerControls from '../PlayerControls/PlayerControls'; +import Search from '../Search/Search'; +import usePlayerStore from '../../stores/usePlayerStore'; +import usePlayingTrack from '../../hooks/usePlayingTrack'; + +import styles from './Header.module.css'; + +export default function Header() { + const queue = usePlayerStore((state) => state.queue); + const queueCursor = usePlayerStore((state) => state.queueCursor); + const trackPlaying = usePlayingTrack(); + + return ( +
+
+ +
+
+ {trackPlaying != null && ( + <> + + + + + + + + + + + + + )} +
+
+ +
+
+ ); +} diff --git a/src/renderer/components/PlayerControls/PlayerControls.module.css b/src/components/PlayerControls/PlayerControls.module.css similarity index 94% rename from src/renderer/components/PlayerControls/PlayerControls.module.css rename to src/components/PlayerControls/PlayerControls.module.css index f7c76039a..93c0b8144 100644 --- a/src/renderer/components/PlayerControls/PlayerControls.module.css +++ b/src/components/PlayerControls/PlayerControls.module.css @@ -1,5 +1,4 @@ .playerControls { - -webkit-app-region: no-drag; display: flex; align-items: center; position: relative; diff --git a/src/renderer/components/PlayerControls/PlayerControls.tsx b/src/components/PlayerControls/PlayerControls.tsx similarity index 89% rename from src/renderer/components/PlayerControls/PlayerControls.tsx rename to src/components/PlayerControls/PlayerControls.tsx index 8b8c0abb0..12e65b2f1 100644 --- a/src/renderer/components/PlayerControls/PlayerControls.tsx +++ b/src/components/PlayerControls/PlayerControls.tsx @@ -1,7 +1,7 @@ import Icon from 'react-fontawesome'; import VolumeControl from '../VolumeControl/VolumeControl'; -import { PlayerStatus } from '../../../shared/types/museeks'; +import { PlayerStatus } from '../../types/museeks'; import usePlayerStore, { usePlayerAPI } from '../../stores/usePlayerStore'; import styles from './PlayerControls.module.css'; @@ -17,6 +17,7 @@ export default function PlayerControls() { className={styles.control} title="Previous" onClick={playerAPI.previous} + data-museeks-action > @@ -24,6 +25,7 @@ export default function PlayerControls() { className={`${styles.control} ${styles.play}`} title={playerStatus === PlayerStatus.PLAY ? 'Pause' : 'Play'} onClick={playerAPI.playPause} + data-museeks-action > diff --git a/src/renderer/components/PlayerOptionsButtons/ButtonRepeat.tsx b/src/components/PlayerOptionsButtons/ButtonRepeat.tsx similarity index 53% rename from src/renderer/components/PlayerOptionsButtons/ButtonRepeat.tsx rename to src/components/PlayerOptionsButtons/ButtonRepeat.tsx index 2ac2fb4c8..d49e63533 100644 --- a/src/renderer/components/PlayerOptionsButtons/ButtonRepeat.tsx +++ b/src/components/PlayerOptionsButtons/ButtonRepeat.tsx @@ -1,30 +1,38 @@ import InlineSVG from 'svg-inline-react'; import cx from 'classnames'; -import { Repeat } from '../../../shared/types/museeks'; import icons from '../../lib/icons'; import usePlayerStore, { usePlayerAPI } from '../../stores/usePlayerStore'; +import { Repeat } from '../../generated/typings'; import styles from './common.module.css'; -const svgMap = { - [Repeat.ONE]: icons.REPEAT_ONE, - [Repeat.ALL]: icons.REPEAT, - [Repeat.NONE]: icons.REPEAT, - default: icons.REPEAT, -}; +function getIcon(repeat: Repeat) { + switch (repeat) { + case 'One': + return icons.REPEAT_ONE; + case 'None': + case 'All': + default: + return icons.REPEAT; + } +} export default function ButtonRepeat() { const repeat = usePlayerStore((state) => state.repeat); const playerAPI = usePlayerAPI(); - const svg = svgMap[repeat] || svgMap.default; + const svg = getIcon(repeat); const buttonClasses = cx(styles.button, { - [styles.active]: repeat === Repeat.ONE || repeat === Repeat.ALL, + [styles.active]: repeat === 'One' || repeat === 'All', }); return ( - ); diff --git a/src/renderer/components/PlayerOptionsButtons/ButtonShuffle.tsx b/src/components/PlayerOptionsButtons/ButtonShuffle.tsx similarity index 96% rename from src/renderer/components/PlayerOptionsButtons/ButtonShuffle.tsx rename to src/components/PlayerOptionsButtons/ButtonShuffle.tsx index 7af930c98..e27101c80 100644 --- a/src/renderer/components/PlayerOptionsButtons/ButtonShuffle.tsx +++ b/src/components/PlayerOptionsButtons/ButtonShuffle.tsx @@ -21,6 +21,7 @@ export default function ButtonShuffle() { onClick={() => { playerAPI.toggleShuffle(); }} + data-museeks-action > diff --git a/src/renderer/components/PlayerOptionsButtons/common.module.css b/src/components/PlayerOptionsButtons/common.module.css similarity index 91% rename from src/renderer/components/PlayerOptionsButtons/common.module.css rename to src/components/PlayerOptionsButtons/common.module.css index 95a3a5949..a2e3257d7 100644 --- a/src/renderer/components/PlayerOptionsButtons/common.module.css +++ b/src/components/PlayerOptionsButtons/common.module.css @@ -7,11 +7,11 @@ } .button { - -webkit-app-region: no-drag; border: 0; color: inherit; background: transparent; font-size: 20px; + padding: 0 8px; &.active { .icon > svg { diff --git a/src/renderer/components/PlayingBar/PlayingBar.module.css b/src/components/PlayingBar/PlayingBar.module.css similarity index 89% rename from src/renderer/components/PlayingBar/PlayingBar.module.css rename to src/components/PlayingBar/PlayingBar.module.css index 84a8c9920..52737eec8 100644 --- a/src/renderer/components/PlayingBar/PlayingBar.module.css +++ b/src/components/PlayingBar/PlayingBar.module.css @@ -6,6 +6,8 @@ background: var(--header-bg-softer); border: solid 1px var(--border-color-softer); border-width: 0 1px; + flex: 1 1 auto; + min-width: 0; } .playingBar__cover { @@ -14,10 +16,10 @@ overflow: hidden; box-sizing: content-box; border-right: solid 1px var(--border-color-softer); + font-size: 28px; } .playerOptions { - -webkit-app-region: no-drag; flex-shrink: 0; display: flex; align-items: center; diff --git a/src/renderer/components/PlayingBar/PlayingBar.tsx b/src/components/PlayingBar/PlayingBar.tsx similarity index 77% rename from src/renderer/components/PlayingBar/PlayingBar.tsx rename to src/components/PlayingBar/PlayingBar.tsx index 7ba769983..0799fd7fd 100644 --- a/src/renderer/components/PlayingBar/PlayingBar.tsx +++ b/src/components/PlayingBar/PlayingBar.tsx @@ -3,24 +3,23 @@ import Cover from '../Cover/Cover'; import usePlayerStore from '../../stores/usePlayerStore'; import ButtonRepeat from '../PlayerOptionsButtons/ButtonRepeat'; import ButtonShuffle from '../PlayerOptionsButtons/ButtonShuffle'; -import usePlayingTrack from '../../hooks/usePlayingTrack'; +import { Track } from '../../generated/typings'; import styles from './PlayingBar.module.css'; -export default function PlayingBar() { +type Props = { + trackPlaying: Track; +}; + +export default function PlayingBar(props: Props) { const repeat = usePlayerStore((state) => state.repeat); const shuffle = usePlayerStore((state) => state.shuffle); - - const trackPlaying = usePlayingTrack(); - - if (trackPlaying === null) { - return null; - } + const trackPlaying = props.trackPlaying; return (
- +
+
{utils.parseDuration(elapsed)}
+
{trackPlaying.title}
- {trackPlaying.artist.join(', ')} + {trackPlaying.artists.join(', ')}  —  {trackPlaying.album}
diff --git a/src/renderer/components/PlayingIndicator/PlayingIndicator.module.css b/src/components/PlayingIndicator/PlayingIndicator.module.css similarity index 79% rename from src/renderer/components/PlayingIndicator/PlayingIndicator.module.css rename to src/components/PlayingIndicator/PlayingIndicator.module.css index d1f70c422..bef0a5d61 100644 --- a/src/renderer/components/PlayingIndicator/PlayingIndicator.module.css +++ b/src/components/PlayingIndicator/PlayingIndicator.module.css @@ -6,15 +6,6 @@ align-items: center; justify-content: center; cursor: pointer; - transform-origin: center; -} - -.playingIndicator :global(.fa) { - transition: transform ease-in-out 0.2s; -} - -.playingIndicator:hover :global(.fa) { - transform: scale(1.3); } .animation { @@ -35,8 +26,6 @@ animation-timing-function: ease-in-out; animation-iteration-count: infinite; transform-origin: bottom; - transform: scale3d(1, 0, 1); - will-change: transform; } .animation .bar.barSecond { diff --git a/src/renderer/components/PlayingIndicator/PlayingIndicator.tsx b/src/components/PlayingIndicator/PlayingIndicator.tsx similarity index 93% rename from src/renderer/components/PlayingIndicator/PlayingIndicator.tsx rename to src/components/PlayingIndicator/PlayingIndicator.tsx index b0d594d9c..dfbfb5355 100644 --- a/src/renderer/components/PlayingIndicator/PlayingIndicator.tsx +++ b/src/components/PlayingIndicator/PlayingIndicator.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import Icon from 'react-fontawesome'; -import { PlayerStatus } from '../../../shared/types/museeks'; +import { PlayerStatus } from '../../types/museeks'; import usePlayerStore, { usePlayerAPI } from '../../stores/usePlayerStore'; import styles from './PlayingIndicator.module.css'; @@ -38,6 +38,7 @@ export default function TrackPlayingIndicator() { onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} tabIndex={0} + data-museeks-action > {icon} diff --git a/src/renderer/components/PlaylistsNav/PlaylistsNav.module.css b/src/components/PlaylistsNav/PlaylistsNav.module.css similarity index 100% rename from src/renderer/components/PlaylistsNav/PlaylistsNav.module.css rename to src/components/PlaylistsNav/PlaylistsNav.module.css diff --git a/src/renderer/components/PlaylistsNav/PlaylistsNav.tsx b/src/components/PlaylistsNav/PlaylistsNav.tsx similarity index 66% rename from src/renderer/components/PlaylistsNav/PlaylistsNav.tsx rename to src/components/PlaylistsNav/PlaylistsNav.tsx index 79f2d3363..ace709e6c 100644 --- a/src/renderer/components/PlaylistsNav/PlaylistsNav.tsx +++ b/src/components/PlaylistsNav/PlaylistsNav.tsx @@ -1,64 +1,68 @@ /* eslint-disable jsx-a11y/no-autofocus */ -import type { MenuItemConstructorOptions } from 'electron'; import React, { useCallback, useState } from 'react'; import Icon from 'react-fontawesome'; +import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'; import PlaylistsAPI from '../../stores/PlaylistsAPI'; import PlaylistsNavLink from '../PlaylistsNavLink/PlaylistsNavLink'; -import { PlaylistModel } from '../../../shared/types/museeks'; +import { Playlist } from '../../generated/typings'; +import { logAndNotifyError } from '../../lib/utils'; import styles from './PlaylistsNav.module.css'; -const { menu } = window.ElectronAPI; - type Props = { - playlists: PlaylistModel[]; + playlists: Playlist[]; }; export default function PlaylistsNav(props: Props) { const [renamed, setRenamed] = useState(null); - const showContextMenu = useCallback((playlistID: string) => { - const template: MenuItemConstructorOptions[] = [ - { - label: 'Rename', - click: () => { - setRenamed(playlistID); - }, - }, - { - label: 'Delete', - click: async () => { - await PlaylistsAPI.remove(playlistID); - }, - }, - { - type: 'separator', - }, - { - label: 'Duplicate', - click: async () => { - await PlaylistsAPI.duplicate(playlistID); - }, - }, - { - type: 'separator', - }, - { - label: 'Export', - click: async () => { - await PlaylistsAPI.exportToM3u(playlistID); - }, - }, - ]; - - menu.showContextMenu(template); - }, []); + const showContextMenu = useCallback( + async (e: React.MouseEvent, playlistID: string) => { + e.preventDefault(); + + const menuItems = await Promise.all([ + MenuItem.new({ + text: 'Rename', + action: () => { + setRenamed(playlistID); + }, + }), + MenuItem.new({ + text: 'Delete', + action: async () => { + await PlaylistsAPI.remove(playlistID); + }, + }), + PredefinedMenuItem.new({ item: 'Separator' }), + MenuItem.new({ + text: 'Duplicate', + action: async () => { + await PlaylistsAPI.duplicate(playlistID); + }, + }), + PredefinedMenuItem.new({ item: 'Separator' }), + MenuItem.new({ + text: 'Export', + action: async () => { + await PlaylistsAPI.exportToM3u(playlistID); + }, + }), + ]); + + const menu = await Menu.new({ + items: menuItems, + }); + + await menu.popup().catch(logAndNotifyError); + }, + [], + ); const createPlaylist = useCallback(async () => { - // Todo 'new playlist 1', 'new playlist 2' ... - await PlaylistsAPI.create('New playlist', [], false, true); + // TODO: 'new playlist 1', 'new playlist 2' ... + await PlaylistsAPI.create('New playlist', [], false); }, []); const rename = useCallback(async (playlistID: string, name: string) => { @@ -108,7 +112,6 @@ export default function PlaylistsNav(props: Props) { const { playlists } = props; - // TODO (y.solovyov): extract into separate method that returns items const nav = playlists.map((elem) => { let navItemContent; @@ -148,6 +151,7 @@ export default function PlaylistsNav(props: Props) { className={styles.action} onClick={createPlaylist} title="New playlist" + data-museeks-action > diff --git a/src/renderer/components/PlaylistsNavLink/PlaylistsNavLink.module.css b/src/components/PlaylistsNavLink/PlaylistsNavLink.module.css similarity index 100% rename from src/renderer/components/PlaylistsNavLink/PlaylistsNavLink.module.css rename to src/components/PlaylistsNavLink/PlaylistsNavLink.module.css diff --git a/src/renderer/components/PlaylistsNavLink/PlaylistsNavLink.tsx b/src/components/PlaylistsNavLink/PlaylistsNavLink.tsx similarity index 82% rename from src/renderer/components/PlaylistsNavLink/PlaylistsNavLink.tsx rename to src/components/PlaylistsNavLink/PlaylistsNavLink.tsx index 7749b759c..56e5abf29 100644 --- a/src/renderer/components/PlaylistsNavLink/PlaylistsNavLink.tsx +++ b/src/components/PlaylistsNavLink/PlaylistsNavLink.tsx @@ -9,7 +9,7 @@ type Props = { children: React.ReactNode; className?: string; playlistID: string; - onContextMenu: (playlistID: string) => void; + onContextMenu: (e: React.MouseEvent, playlistID: string) => void; }; export default function PlaylistsNavLink(props: Props) { @@ -19,7 +19,7 @@ export default function PlaylistsNavLink(props: Props) { `${props.className} ${styles.playlistLink} ${isActive && 'isActive'}` } to={`/playlists/${props.playlistID}`} - onContextMenu={() => props.onContextMenu(props.playlistID)} + onContextMenu={(e) => props.onContextMenu(e, props.playlistID)} draggable={false} onDoubleClick={() => PlaylistsAPI.play(props.playlistID)} > diff --git a/src/components/ProgressBar/ProgressBar.module.css b/src/components/ProgressBar/ProgressBar.module.css new file mode 100644 index 000000000..93d9e85e6 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.module.css @@ -0,0 +1,13 @@ +.progress { + --progress-height: 6px; + + flex: 1; + display: block; + height: var(--progress-height); + background-color: var(--progress-bg); +} + +.progressBar { + height: var(--progress-height); + background-color: var(--main-color); +} diff --git a/src/renderer/components/ProgressBar/ProgressBar.tsx b/src/components/ProgressBar/ProgressBar.tsx similarity index 55% rename from src/renderer/components/ProgressBar/ProgressBar.tsx rename to src/components/ProgressBar/ProgressBar.tsx index 6f81dc924..3299f16d6 100644 --- a/src/renderer/components/ProgressBar/ProgressBar.tsx +++ b/src/components/ProgressBar/ProgressBar.tsx @@ -1,3 +1,4 @@ +import * as Progress from '@radix-ui/react-progress'; import cx from 'classnames'; import styles from './ProgressBar.module.css'; @@ -9,11 +10,14 @@ type Props = { export default function ProgressBar(props: Props) { return ( -
-
+
-
+ /> + ); } diff --git a/src/renderer/components/Queue/Queue.module.css b/src/components/Queue/Queue.module.css similarity index 84% rename from src/renderer/components/Queue/Queue.module.css rename to src/components/Queue/Queue.module.css index 6b31f98d8..f7431944b 100644 --- a/src/renderer/components/Queue/Queue.module.css +++ b/src/components/Queue/Queue.module.css @@ -2,6 +2,7 @@ width: 300px; background: var(--queue-bg); border: solid 1px var(--border-color); + border-radius: var(--border-radius); text-overflow: ellipsis; overflow-x: hidden; font-size: 12px; diff --git a/src/renderer/components/Queue/Queue.tsx b/src/components/Queue/Queue.tsx similarity index 90% rename from src/renderer/components/Queue/Queue.tsx rename to src/components/Queue/Queue.tsx index 380cf9a6f..263478048 100644 --- a/src/renderer/components/Queue/Queue.tsx +++ b/src/components/Queue/Queue.tsx @@ -2,12 +2,12 @@ import React, { useMemo } from 'react'; import QueueEmpty from '../QueueEmpty/QueueEmpty'; import QueueList from '../QueueList/QueueList'; -import { TrackModel } from '../../../shared/types/museeks'; +import { Track } from '../../generated/typings'; import styles from './Queue.module.css'; type Props = { - queue: TrackModel[]; + queue: Track[]; queueCursor: number | null; }; diff --git a/src/renderer/components/QueueEmpty/QueueEmpty.module.css b/src/components/QueueEmpty/QueueEmpty.module.css similarity index 100% rename from src/renderer/components/QueueEmpty/QueueEmpty.module.css rename to src/components/QueueEmpty/QueueEmpty.module.css diff --git a/src/renderer/components/QueueEmpty/QueueEmpty.tsx b/src/components/QueueEmpty/QueueEmpty.tsx similarity index 100% rename from src/renderer/components/QueueEmpty/QueueEmpty.tsx rename to src/components/QueueEmpty/QueueEmpty.tsx diff --git a/src/renderer/components/QueueList/QueueList.module.css b/src/components/QueueList/QueueList.module.css similarity index 83% rename from src/renderer/components/QueueList/QueueList.module.css rename to src/components/QueueList/QueueList.module.css index 965d417c0..85dac43f0 100644 --- a/src/renderer/components/QueueList/QueueList.module.css +++ b/src/components/QueueList/QueueList.module.css @@ -1,6 +1,7 @@ .queue__header { padding: 5px 10px; background-color: var(--queue-header-bg); + border-bottom: solid 1px var(--border-color); } .queue__header__infos { diff --git a/src/renderer/components/QueueList/QueueList.tsx b/src/components/QueueList/QueueList.tsx similarity index 98% rename from src/renderer/components/QueueList/QueueList.tsx rename to src/components/QueueList/QueueList.tsx index eb5af15e8..b6cb0cef9 100644 --- a/src/renderer/components/QueueList/QueueList.tsx +++ b/src/components/QueueList/QueueList.tsx @@ -2,16 +2,16 @@ import React, { useCallback, useState } from 'react'; import QueueListItem from '../QueueListItem/QueueListItem'; import { getStatus } from '../../lib/utils-library'; -import { TrackModel } from '../../../shared/types/museeks'; import Button from '../../elements/Button/Button'; import { usePlayerAPI } from '../../stores/usePlayerStore'; +import { Track } from '../../generated/typings'; import styles from './QueueList.module.css'; const INITIAL_QUEUE_SIZE = 20; type Props = { - queue: TrackModel[]; + queue: Track[]; queueCursor: number; }; diff --git a/src/renderer/components/QueueListItem/QueueListItem.module.css b/src/components/QueueListItem/QueueListItem.module.css similarity index 80% rename from src/renderer/components/QueueListItem/QueueListItem.module.css rename to src/components/QueueListItem/QueueListItem.module.css index 5747201e8..46ac41565 100644 --- a/src/renderer/components/QueueListItem/QueueListItem.module.css +++ b/src/components/QueueListItem/QueueListItem.module.css @@ -3,9 +3,8 @@ flex-wrap: nowrap; width: 100%; position: relative; - padding-bottom: 5px; - padding-top: 5px; cursor: pointer; + align-items: center; &:not(:first-child) { border-top: dashed 1px var(--border-color); @@ -41,6 +40,15 @@ } } +.queue__item__cover { + margin: 8px; + width: 32px; + aspect-ratio: 1; + border-radius: 3px; + overflow: hidden; + font-size: 16px; +} + .queue__item__remove { color: var(--text); border: none; @@ -68,12 +76,18 @@ .queue__item__info__title { font-weight: bold; + margin-bottom: 4px; } .queue__item__info__title, .queue__item__info__otherInfos { - padding: 0 10px; + padding-right: 10px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } + +.queue__item__info__otherInfos { + opacity: 0.7; + font-size: 0.875rem; +} diff --git a/src/renderer/components/QueueListItem/QueueListItem.tsx b/src/components/QueueListItem/QueueListItem.tsx similarity index 88% rename from src/renderer/components/QueueListItem/QueueListItem.tsx rename to src/components/QueueListItem/QueueListItem.tsx index 265837638..170ddb88e 100644 --- a/src/renderer/components/QueueListItem/QueueListItem.tsx +++ b/src/components/QueueListItem/QueueListItem.tsx @@ -1,8 +1,9 @@ import React, { useCallback } from 'react'; import cx from 'classnames'; -import { TrackModel } from '../../../shared/types/museeks'; +import { Track } from '../../generated/typings'; import { usePlayerAPI } from '../../stores/usePlayerStore'; +import Cover from '../Cover/Cover'; import styles from './QueueListItem.module.css'; @@ -11,7 +12,7 @@ type Props = { draggedOver: boolean; dragPosition?: null | 'above' | 'below'; index: number; - track: TrackModel; + track: Track; onDragStart: (e: React.DragEvent, index: number) => void; onDragOver: (e: React.DragEvent, index: number) => void; onDragEnd: React.DragEventHandler; @@ -60,10 +61,13 @@ export default function QueueListItem(props: Props) { onDragOver={onDragOver} onDragEnd={props.onDragEnd} > +
+ +
{track.title}
- {track.artist} - {track.album} + {track.artists[0]} - {track.album}
)} diff --git a/src/renderer/components/Setting/Setting.module.css b/src/components/Setting/Setting.module.css similarity index 89% rename from src/renderer/components/Setting/Setting.module.css rename to src/components/Setting/Setting.module.css index d1ae07bdc..7136662a7 100644 --- a/src/renderer/components/Setting/Setting.module.css +++ b/src/components/Setting/Setting.module.css @@ -23,6 +23,7 @@ .settingSelect, .settingInput { + appearance: none; display: block; background: var(--input-bg); color: var(--input-color); @@ -37,6 +38,11 @@ outline: none; border-color: var(--main-color); } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } } .settingError { diff --git a/src/renderer/components/Setting/Setting.tsx b/src/components/Setting/Setting.tsx similarity index 100% rename from src/renderer/components/Setting/Setting.tsx rename to src/components/Setting/Setting.tsx diff --git a/src/renderer/components/SettingCheckbox/SettingCheckbox.module.css b/src/components/SettingCheckbox/SettingCheckbox.module.css similarity index 76% rename from src/renderer/components/SettingCheckbox/SettingCheckbox.module.css rename to src/components/SettingCheckbox/SettingCheckbox.module.css index 79f50cbc2..f3716b90a 100644 --- a/src/renderer/components/SettingCheckbox/SettingCheckbox.module.css +++ b/src/components/SettingCheckbox/SettingCheckbox.module.css @@ -1,8 +1,9 @@ .checkbox { - input[type='checkbox'] { + input[type="checkbox"] { appearance: none; width: 16px; height: 16px; + border: solid 1px var(--border-color); border-radius: var(--border-radius); display: inline-block; background-color: var(--checkbox-bg); @@ -17,16 +18,17 @@ &:checked { background-color: var(--main-color); + border-color: var(--main-color-darker); &::after { - content: '\f00c'; + content: "\f00c"; /* stylelint-disable-next-line */ font-family: FontAwesome; font-size: 10px; color: white; position: absolute; - top: 3px; - left: 3px; + top: 2px; + left: 2px; } } } diff --git a/src/renderer/components/SettingCheckbox/SettingCheckbox.tsx b/src/components/SettingCheckbox/SettingCheckbox.tsx similarity index 100% rename from src/renderer/components/SettingCheckbox/SettingCheckbox.tsx rename to src/components/SettingCheckbox/SettingCheckbox.tsx diff --git a/src/renderer/components/Toasts/Toasts.module.css b/src/components/Toasts/Toasts.module.css similarity index 84% rename from src/renderer/components/Toasts/Toasts.module.css rename to src/components/Toasts/Toasts.module.css index ed0ceeb5b..f963c1c3a 100644 --- a/src/renderer/components/Toasts/Toasts.module.css +++ b/src/components/Toasts/Toasts.module.css @@ -1,7 +1,7 @@ .toasts { position: fixed; bottom: 40px; - right: 12px; + right: 10px; width: 350px; z-index: 1000; } diff --git a/src/renderer/components/Toasts/Toasts.tsx b/src/components/Toasts/Toasts.tsx similarity index 96% rename from src/renderer/components/Toasts/Toasts.tsx rename to src/components/Toasts/Toasts.tsx index 11a5ad22d..01c5779dd 100644 --- a/src/renderer/components/Toasts/Toasts.tsx +++ b/src/components/Toasts/Toasts.tsx @@ -8,7 +8,7 @@ export default function Toasts() { return (
{toasts.map((toast) => ( - + ))}
); diff --git a/src/renderer/components/TrackProgress/TrackProgress.module.css b/src/components/TrackProgress/TrackProgress.module.css similarity index 87% rename from src/renderer/components/TrackProgress/TrackProgress.module.css rename to src/components/TrackProgress/TrackProgress.module.css index 9e2ee3d90..a89a56140 100644 --- a/src/renderer/components/TrackProgress/TrackProgress.module.css +++ b/src/components/TrackProgress/TrackProgress.module.css @@ -1,7 +1,6 @@ .trackRoot { - --progress-height: 6px; + --progress-height: 7px; - -webkit-app-region: no-drag; position: relative; display: flex; align-items: center; @@ -11,7 +10,7 @@ /* the track progress is too close to the metadata, but using margin would * push the whole section up */ - transform: translateY(3px); + transform: translateY(4px); } .trackProgress { @@ -19,7 +18,8 @@ position: relative; width: 100%; height: 100%; - background-color: var(--progress-bg); + background-color: var(--header-bg); + border: solid 1px var(--border-color); } .trackRange { @@ -27,7 +27,6 @@ height: 100%; background-color: var(--main-color); box-shadow: inset 0 0 0 1px rgba(0 0 0 / 0.2); - min-width: 1px; } .progressTooltip { @@ -39,6 +38,7 @@ bottom: 10px; z-index: 1; transform: translateX(-11px); + pointer-events: none; &::before, &::after { diff --git a/src/renderer/components/TrackProgress/TrackProgress.tsx b/src/components/TrackProgress/TrackProgress.tsx similarity index 82% rename from src/renderer/components/TrackProgress/TrackProgress.tsx rename to src/components/TrackProgress/TrackProgress.tsx index 72f6354fa..49a4a29c3 100644 --- a/src/renderer/components/TrackProgress/TrackProgress.tsx +++ b/src/components/TrackProgress/TrackProgress.tsx @@ -2,14 +2,14 @@ import { useCallback, useState } from 'react'; import * as Slider from '@radix-ui/react-slider'; import { usePlayerAPI } from '../../stores/usePlayerStore'; -import { TrackModel } from '../../../shared/types/museeks'; +import { Track } from '../../generated/typings'; import usePlayingTrackCurrentTime from '../../hooks/usePlayingTrackCurrentTime'; import { parseDuration } from '../../lib/utils'; import styles from './TrackProgress.module.css'; type Props = { - trackPlaying: TrackModel; + trackPlaying: Track; }; export default function TrackProgress(props: Props) { @@ -60,19 +60,20 @@ export default function TrackProgress(props: Props) { value={[elapsed]} onValueChange={jumpAudioTo} className={styles.trackRoot} - onMouseMove={showTooltip} + onMouseMoveCapture={showTooltip} onMouseLeave={hideTooltip} > - {tooltipX !== null && ( -
- {parseDuration(tooltipTargetTime)} -
- )} +
+ {parseDuration(tooltipTargetTime)} +
diff --git a/src/renderer/components/TrackRow/TrackRow.module.css b/src/components/TrackRow/TrackRow.module.css similarity index 98% rename from src/renderer/components/TrackRow/TrackRow.module.css rename to src/components/TrackRow/TrackRow.module.css index e9ad3211d..ab889a76b 100644 --- a/src/renderer/components/TrackRow/TrackRow.module.css +++ b/src/components/TrackRow/TrackRow.module.css @@ -20,7 +20,7 @@ background-color: var(--tracks-bg-even); align-items: center; - &:nth-child(even) { + &.even { background-color: var(--tracks-bg-odd); } diff --git a/src/renderer/components/TrackRow/TrackRow.tsx b/src/components/TrackRow/TrackRow.tsx similarity index 96% rename from src/renderer/components/TrackRow/TrackRow.tsx rename to src/components/TrackRow/TrackRow.tsx index 9f459007b..cc5a01384 100644 --- a/src/renderer/components/TrackRow/TrackRow.tsx +++ b/src/components/TrackRow/TrackRow.tsx @@ -3,14 +3,14 @@ import cx from 'classnames'; import PlayingIndicator from '../PlayingIndicator/PlayingIndicator'; import { parseDuration } from '../../lib/utils'; -import { TrackModel } from '../../../shared/types/museeks'; import cellStyles from '../TracksListHeader/TracksListHeader.module.css'; +import { Track } from '../../generated/typings'; import styles from './TrackRow.module.css'; type Props = { selected: boolean; - track: TrackModel; + track: Track; index: number; isPlaying: boolean; onDoubleClick: (trackID: string) => void; @@ -99,6 +99,7 @@ export default function TrackRow(props: Props) { [styles.isReorderedOver]: reorderOver, [styles.isAbove]: reorderPosition === 'above', [styles.isBelow]: reorderPosition === 'below', + [styles.even]: index % 2 === 0, }); return ( @@ -135,13 +136,13 @@ export default function TrackRow(props: Props) { {parseDuration(track.duration)}
- {track.artist.sort().join(', ')} + {track.artists.join(', ')}
{track.album}
- {track.genre.join(', ')} + {track.genres.join(', ')}
); diff --git a/src/renderer/components/TracksList/TracksList.module.css b/src/components/TracksList/TracksList.module.css similarity index 73% rename from src/renderer/components/TracksList/TracksList.module.css rename to src/components/TracksList/TracksList.module.css index 9dc6a1a0c..a935903f1 100644 --- a/src/renderer/components/TracksList/TracksList.module.css +++ b/src/components/TracksList/TracksList.module.css @@ -8,21 +8,12 @@ } .tracksListScroller { - overflow: auto; + overflow-y: scroll; flex: 1 1 auto; + position: relative; } .tracksListRows { width: 100%; position: relative; } - -.tiles { - position: relative; -} - -.tile { - position: absolute; - width: 100%; - z-index: 10; -} diff --git a/src/renderer/components/TracksList/TracksList.tsx b/src/components/TracksList/TracksList.tsx similarity index 73% rename from src/renderer/components/TracksList/TracksList.tsx rename to src/components/TracksList/TracksList.tsx index 977ff9b66..5fae2890d 100644 --- a/src/renderer/components/TracksList/TracksList.tsx +++ b/src/components/TracksList/TracksList.tsx @@ -1,30 +1,31 @@ -import type { MenuItemConstructorOptions } from 'electron'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import Keybinding from 'react-keybinding-component'; -import { useNavigate } from 'react-router-dom'; import { useVirtualizer } from '@tanstack/react-virtual'; +import { + Menu, + MenuItem, + PredefinedMenuItem, + Submenu, +} from '@tauri-apps/api/menu'; +import { invoke } from '@tauri-apps/api/core'; +import { useNavigate } from 'react-router-dom'; import TrackRow from '../TrackRow/TrackRow'; import TracksListHeader from '../TracksListHeader/TracksListHeader'; -import PlaylistsAPI from '../../stores/PlaylistsAPI'; import { isLeftClick, isRightClick, isCtrlKey, isAltKey, } from '../../lib/utils-events'; -import { - Config, - PlaylistModel, - TrackModel, -} from '../../../shared/types/museeks'; import { usePlayerAPI } from '../../stores/usePlayerStore'; import useLibraryStore, { useLibraryAPI } from '../../stores/useLibraryStore'; +import { Config, Playlist, Track } from '../../generated/typings'; +import { logAndNotifyError } from '../../lib/utils'; +import PlaylistsAPI from '../../stores/PlaylistsAPI'; import styles from './TracksList.module.css'; -const { menu } = window.ElectronAPI; - const ROW_HEIGHT = 30; const ROW_HEIGHT_COMPACT = 24; @@ -34,10 +35,10 @@ const ROW_HEIGHT_COMPACT = 24; type Props = { type: string; - tracks: TrackModel[]; - tracksDensity: Config['tracksDensity']; + tracks: Track[]; + tracksDensity: Config['track_view_density']; trackPlayingID: string | null; - playlists: PlaylistModel[]; + playlists: Playlist[]; currentPlaylist?: string; reorderable?: boolean; onReorder?: ( @@ -68,7 +69,8 @@ export default function TracksList(props: Props) { const scrollableRef = useRef(null); const virtualizer = useVirtualizer({ count: tracks.length, - overscan: 10, + overscan: 20, + scrollPaddingEnd: 22, // Height of the track list header getScrollElement: () => scrollableRef.current, estimateSize: () => { switch (tracksDensity) { @@ -118,18 +120,18 @@ export default function TracksList(props: Props) { * Keyboard navigations events/helpers */ const onEnter = useCallback( - async (index: number, tracks: TrackModel[]) => { + async (index: number, tracks: Track[]) => { if (index !== -1) playerAPI.start(tracks, tracks[index]._id); }, [playerAPI], ); - const onControlAll = useCallback((tracks: TrackModel[]) => { + const onControlAll = useCallback((tracks: Track[]) => { setSelected(tracks.map((track) => track._id)); }, []); const onUp = useCallback( - (index: number, tracks: TrackModel[], shiftKeyPressed: boolean) => { + (index: number, tracks: Track[], shiftKeyPressed: boolean) => { const addedIndex = Math.max(0, index - 1); // Add to the selection if shift key is pressed @@ -145,7 +147,7 @@ export default function TracksList(props: Props) { ); const onDown = useCallback( - (index: number, tracks: TrackModel[], shiftKeyPressed: boolean) => { + (index: number, tracks: Track[], shiftKeyPressed: boolean) => { const addedIndex = Math.min(tracks.length - 1, index + 1); // Add to the selection if shift key is pressed @@ -342,148 +344,157 @@ export default function TracksList(props: Props) { * Context menus */ const showContextMenu = useCallback( - (_e: React.MouseEvent, index: number) => { + async (e: React.MouseEvent, index: number) => { + e.preventDefault(); + const selectedCount = selected.length; const track = tracks[index]; let shownPlaylists = playlists; - // Hide current playlist if needed + // Hide current playlist if one the given playlist view if (type === 'playlist') { shownPlaylists = playlists.filter( (elem) => elem._id !== currentPlaylist, ); } - const playlistTemplate: MenuItemConstructorOptions[] = []; - let addToQueueTemplate: MenuItemConstructorOptions[] = []; - - if (shownPlaylists) { - playlistTemplate.push( - { - label: 'Create new playlist...', - click: async () => { - await PlaylistsAPI.create('New playlist', selected); - }, - }, - { - type: 'separator', + // Playlist sub-menu + const playlistSubMenu = await Promise.all([ + MenuItem.new({ + text: 'Create new playlist...', + async action() { + await PlaylistsAPI.create('New playlist', selected); }, + }), + PredefinedMenuItem.new({ + item: 'Separator', + }), + ]); + + if (shownPlaylists.length === 0) { + playlistSubMenu.push( + await MenuItem.new({ text: 'No playlists', enabled: false }), + ); + } else { + playlistSubMenu.push( + ...(await Promise.all( + shownPlaylists.map((playlist) => + MenuItem.new({ + text: playlist.name, + async action() { + await PlaylistsAPI.addTracks(playlist._id, selected); + }, + }), + ), + )), ); - - if (shownPlaylists.length === 0) { - playlistTemplate.push({ - label: 'No playlists', - enabled: false, - }); - } else { - shownPlaylists.forEach((playlist) => { - playlistTemplate.push({ - label: playlist.name, - click: async () => { - await PlaylistsAPI.addTracks(playlist._id, selected); - }, - }); - }); - } } - addToQueueTemplate = [ - { - label: 'Add to queue', - click: async () => { - playerAPI.addInQueue(selected); - }, - }, - { - label: 'Play next', - click: async () => { - playerAPI.addNextInQueue(selected); - }, - }, - { - type: 'separator', - }, - ]; - - const template: MenuItemConstructorOptions[] = [ - { - label: + const menuItems = await Promise.all([ + MenuItem.new({ + text: selectedCount > 1 ? `${selectedCount} tracks selected` : `${selectedCount} track selected`, enabled: false, - }, - { - type: 'separator', - }, - ...addToQueueTemplate, - { - label: 'Add to playlist', - submenu: playlistTemplate, - }, - { - type: 'separator', - }, - ]; - - for (const artist of track.artist) { - template.push({ - label: `Search for "${artist}" `, - click: () => { - libraryAPI.search(track.artist[0]); + }), + PredefinedMenuItem.new({ + text: '?', + item: 'Separator', + }), + MenuItem.new({ + text: 'Add to queue', + action() { + playerAPI.addInQueue(selected); }, - }); - } + }), + MenuItem.new({ + text: 'Play next', + action() { + playerAPI.addNextInQueue(selected); + }, + }), + PredefinedMenuItem.new({ + item: 'Separator', + }), + Submenu.new({ + text: 'Add to playlist', + items: playlistSubMenu, + }), + PredefinedMenuItem.new({ + text: '?', + item: 'Separator', + }), + ]); + + menuItems.push( + ...(await Promise.all( + track.artists.map((artist) => + MenuItem.new({ + text: `Search for "${artist}" `, + action: () => { + libraryAPI.search(artist); + }, + }), + ), + )), + ); - template.push({ - label: `Search for "${track.album}"`, - click: () => { - libraryAPI.search(track.album); - }, - }); + menuItems.push( + await MenuItem.new({ + text: `Search for "${track.album}"`, + action() { + libraryAPI.search(track.album); + }, + }), + ); if (type === 'playlist' && currentPlaylist) { - template.push( - { - type: 'separator', - }, - { - label: 'Remove from playlist', - click: async () => { - await PlaylistsAPI.removeTracks(currentPlaylist, selected); - }, - }, + menuItems.push( + ...(await Promise.all([ + PredefinedMenuItem.new({ item: 'Separator' }), + MenuItem.new({ + text: 'Remove from playlist', + async action() { + await PlaylistsAPI.removeTracks(currentPlaylist, selected); + }, + }), + ])), ); } - template.push( - { - type: 'separator', - }, - { - label: 'Edit track', - click: () => { - navigate(`/details/${track._id}`); - }, - }, - { - type: 'separator', - }, - { - label: 'Show in file manager', - click: () => { - window.MuseeksAPI.library.showTrackInFolder(track); - }, - }, - { - label: 'Remove from library', - click: () => { - libraryAPI.remove(selected); - }, - }, + menuItems.push( + ...(await Promise.all([ + PredefinedMenuItem.new({ item: 'Separator' }), + MenuItem.new({ + text: 'Edit track', + action: () => { + navigate(`/details/${track._id}`); + }, + }), + PredefinedMenuItem.new({ item: 'Separator' }), + MenuItem.new({ + text: 'Show in file manager', + action: async () => { + await invoke('plugin:shell-extension|show_item_in_folder', { + path: track.path, + }); + }, + }), + MenuItem.new({ + text: 'Remove from library', + action: () => { + libraryAPI.remove(selected); + }, + }), + ])), ); - menu.showContextMenu(template); + const menu = await Menu.new({ + items: menuItems, + }); + + await menu.popup().catch(logAndNotifyError); }, [ currentPlaylist, @@ -500,9 +511,10 @@ export default function TracksList(props: Props) { return (
- {/* Scrollable element */}
+ + {/* The large inner element to hold all of the items */}
{ - if (sort && sort.by === sortType) { - if (sort.order === SortOrder.ASC) { +const getIcon = ( + sortBy: SortBy, + sortOrder: SortOrder, + sortByTarget: SortBy, +) => { + if (sortBy === sortByTarget) { + if (sortOrder === 'Asc') { return 'angle-up'; } @@ -23,7 +27,8 @@ type Props = { export default function TracksListHeader(props: Props) { const { enableSort } = props; - const sort = useLibraryStore((state) => state.sort); + const sortBy = useLibraryStore((state) => state.sortBy); + const sortOrder = useLibraryStore((state) => state.sortOrder); return (
@@ -34,32 +39,32 @@ export default function TracksListHeader(props: Props) {
); diff --git a/src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.module.css b/src/components/TracksListHeaderCell/TracksListHeaderCell.module.css similarity index 100% rename from src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.module.css rename to src/components/TracksListHeaderCell/TracksListHeaderCell.module.css diff --git a/src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.tsx b/src/components/TracksListHeaderCell/TracksListHeaderCell.tsx similarity index 95% rename from src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.tsx rename to src/components/TracksListHeaderCell/TracksListHeaderCell.tsx index 3b15f3ed7..ddb79a956 100644 --- a/src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.tsx +++ b/src/components/TracksListHeaderCell/TracksListHeaderCell.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import cx from 'classnames'; import Icon from 'react-fontawesome'; -import { SortBy } from '../../../shared/types/museeks'; +import { SortBy } from '../../generated/typings'; import { useLibraryAPI } from '../../stores/useLibraryStore'; import styles from './TracksListHeaderCell.module.css'; diff --git a/src/renderer/components/VolumeControl/VolumeControl.module.css b/src/components/VolumeControl/VolumeControl.module.css similarity index 97% rename from src/renderer/components/VolumeControl/VolumeControl.module.css rename to src/components/VolumeControl/VolumeControl.module.css index 8725192b1..bb3ea9b5f 100644 --- a/src/renderer/components/VolumeControl/VolumeControl.module.css +++ b/src/components/VolumeControl/VolumeControl.module.css @@ -4,7 +4,6 @@ } .volumeControl { - -webkit-app-region: no-drag; background-color: var(--header-bg); position: absolute; z-index: 10; diff --git a/src/renderer/components/VolumeControl/VolumeControl.tsx b/src/components/VolumeControl/VolumeControl.tsx similarity index 100% rename from src/renderer/components/VolumeControl/VolumeControl.tsx rename to src/components/VolumeControl/VolumeControl.tsx diff --git a/src/renderer/elements/Button/Button.module.css b/src/elements/Button/Button.module.css similarity index 90% rename from src/renderer/elements/Button/Button.module.css rename to src/elements/Button/Button.module.css index 9d98ca285..b93fe9175 100644 --- a/src/renderer/elements/Button/Button.module.css +++ b/src/elements/Button/Button.module.css @@ -7,6 +7,10 @@ cursor: pointer; &:active { + opacity: 0.7; + } + + &[disabled] { opacity: 0.5; } } diff --git a/src/renderer/elements/Button/Button.tsx b/src/elements/Button/Button.tsx similarity index 100% rename from src/renderer/elements/Button/Button.tsx rename to src/elements/Button/Button.tsx diff --git a/src/renderer/elements/ExternalLink/ExternalLink.module.css b/src/elements/ExternalLink/ExternalLink.module.css similarity index 100% rename from src/renderer/elements/ExternalLink/ExternalLink.module.css rename to src/elements/ExternalLink/ExternalLink.module.css diff --git a/src/renderer/elements/ExternalLink/ExternalLink.tsx b/src/elements/ExternalLink/ExternalLink.tsx similarity index 85% rename from src/renderer/elements/ExternalLink/ExternalLink.tsx rename to src/elements/ExternalLink/ExternalLink.tsx index 635819c5a..d97cdac59 100644 --- a/src/renderer/elements/ExternalLink/ExternalLink.tsx +++ b/src/elements/ExternalLink/ExternalLink.tsx @@ -1,9 +1,8 @@ import React, { useCallback } from 'react'; +import { open } from '@tauri-apps/plugin-shell'; import styles from './ExternalLink.module.css'; -const { shell } = window.MuseeksAPI; - type Props = { children: string; href: string; @@ -13,7 +12,7 @@ export default function ExternalLink(props: Props) { const openLink = useCallback( (e: React.SyntheticEvent) => { e.preventDefault(); - shell.openExternal(props.href); + open(props.href); }, [props.href], ); diff --git a/src/renderer/elements/Heart/Heart.module.css b/src/elements/Heart/Heart.module.css similarity index 100% rename from src/renderer/elements/Heart/Heart.module.css rename to src/elements/Heart/Heart.module.css diff --git a/src/renderer/elements/Heart/Heart.tsx b/src/elements/Heart/Heart.tsx similarity index 100% rename from src/renderer/elements/Heart/Heart.tsx rename to src/elements/Heart/Heart.tsx diff --git a/src/renderer/elements/Nav/Nav.module.css b/src/elements/Nav/Nav.module.css similarity index 100% rename from src/renderer/elements/Nav/Nav.module.css rename to src/elements/Nav/Nav.module.css diff --git a/src/renderer/elements/Nav/Nav.tsx b/src/elements/Nav/Nav.tsx similarity index 100% rename from src/renderer/elements/Nav/Nav.tsx rename to src/elements/Nav/Nav.tsx diff --git a/src/renderer/elements/Toast/Toast.module.css b/src/elements/Toast/Toast.module.css similarity index 93% rename from src/renderer/elements/Toast/Toast.module.css rename to src/elements/Toast/Toast.module.css index 896491f39..1707a6bdb 100644 --- a/src/renderer/elements/Toast/Toast.module.css +++ b/src/elements/Toast/Toast.module.css @@ -5,6 +5,7 @@ border-top-color: var(--border-color); border-right-color: var(--border-color); border-bottom-color: var(--border-color); + border-radius: var(--border-radius); color: var(--text); box-shadow: 0 5px 3px -5px rgba(0 0 0 0.5); padding: 15px; diff --git a/src/renderer/elements/Toast/Toast.tsx b/src/elements/Toast/Toast.tsx similarity index 100% rename from src/renderer/elements/Toast/Toast.tsx rename to src/elements/Toast/Toast.tsx diff --git a/src/renderer/elements/ViewMessage/ViewMessage.module.css b/src/elements/ViewMessage/ViewMessage.module.css similarity index 100% rename from src/renderer/elements/ViewMessage/ViewMessage.module.css rename to src/elements/ViewMessage/ViewMessage.module.css diff --git a/src/renderer/elements/ViewMessage/ViewMessage.tsx b/src/elements/ViewMessage/ViewMessage.tsx similarity index 100% rename from src/renderer/elements/ViewMessage/ViewMessage.tsx rename to src/elements/ViewMessage/ViewMessage.tsx diff --git a/src/generated/typings/index.ts b/src/generated/typings/index.ts new file mode 100644 index 000000000..b77364840 --- /dev/null +++ b/src/generated/typings/index.ts @@ -0,0 +1,13 @@ +/** + * Re-export of types generates by ts-rs + */ +export type { Config } from './Config'; +export type { DefaultView } from './DefaultView'; +export type { IPCEvent } from './IPCEvent'; +export type { NumberOf } from './NumberOf'; +export type { Playlist } from './Playlist'; +export type { Progress } from './Progress'; +export type { Repeat } from './Repeat'; +export type { SortBy } from './SortBy'; +export type { SortOrder } from './SortOrder'; +export type { Track } from './Track'; diff --git a/src/hooks/useCover.ts b/src/hooks/useCover.ts new file mode 100644 index 000000000..df2d6e48f --- /dev/null +++ b/src/hooks/useCover.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +import { Track } from '../generated/typings'; +import library from '../lib/library'; + +/** + * Given a track, get its associated cover as an Image src + */ +export default function useCover(track: Track): string | null { + const [coverPath, setCoverPath] = useState(null); + + useEffect(() => { + const refreshCover = async () => { + const cover = await library.getCover(track.path); + setCoverPath(cover); + }; + + refreshCover(); + }, [track.path]); + + return coverPath; +} diff --git a/src/renderer/hooks/useDebounce.ts b/src/hooks/useDebounce.ts similarity index 100% rename from src/renderer/hooks/useDebounce.ts rename to src/hooks/useDebounce.ts diff --git a/src/hooks/useFilteredTracks.ts b/src/hooks/useFilteredTracks.ts new file mode 100644 index 000000000..4150e7cea --- /dev/null +++ b/src/hooks/useFilteredTracks.ts @@ -0,0 +1,23 @@ +import { useMemo } from 'react'; + +import { filterTracks, sortTracks } from '../lib/utils-library'; +import SORT_ORDERS from '../lib/sort-orders'; +import useLibraryStore from '../stores/useLibraryStore'; +import { Track } from '../generated/typings'; +import { stripAccents } from '../lib/utils-id3'; + +export default function useFilteredTracks(tracks: Track[]): Track[] { + const search = useLibraryStore((state) => stripAccents(state.search)); + const sortBy = useLibraryStore((state) => state.sortBy); + const sortOrder = useLibraryStore((state) => state.sortOrder); + + // Filter and sort TracksList + // sorting being a costly operation, do it after filtering + const filteredTracks = useMemo( + () => + sortTracks(filterTracks(tracks, search), SORT_ORDERS[sortBy][sortOrder]), + [tracks, search, sortBy, sortOrder], + ); + + return filteredTracks; +} diff --git a/src/renderer/hooks/usePlayingTrack.ts b/src/hooks/usePlayingTrack.ts similarity index 66% rename from src/renderer/hooks/usePlayingTrack.ts rename to src/hooks/usePlayingTrack.ts index 6153b3c01..7ce434f40 100644 --- a/src/renderer/hooks/usePlayingTrack.ts +++ b/src/hooks/usePlayingTrack.ts @@ -1,7 +1,7 @@ -import { TrackModel } from '../../shared/types/museeks'; +import { Track } from '../generated/typings'; import usePlayerStore from '../stores/usePlayerStore'; -export default function usePlayingTrack(): TrackModel | null { +export default function usePlayingTrack(): Track | null { return usePlayerStore((state) => { if (state.queue.length > 0 && state.queueCursor !== null) { return state.queue[state.queueCursor]; diff --git a/src/renderer/hooks/usePlayingTrackCurrentTime.ts b/src/hooks/usePlayingTrackCurrentTime.ts similarity index 100% rename from src/renderer/hooks/usePlayingTrackCurrentTime.ts rename to src/hooks/usePlayingTrackCurrentTime.ts diff --git a/src/renderer/hooks/usePlayingTrackID.ts b/src/hooks/usePlayingTrackID.ts similarity index 100% rename from src/renderer/hooks/usePlayingTrackID.ts rename to src/hooks/usePlayingTrackID.ts diff --git a/src/shared/lib/__tests__/ipc-channels.test.ts b/src/lib/__tests__/ipc-channels.test.ts similarity index 100% rename from src/shared/lib/__tests__/ipc-channels.test.ts rename to src/lib/__tests__/ipc-channels.test.ts diff --git a/src/shared/lib/__tests__/themes.test.ts b/src/lib/__tests__/themes.test.ts similarity index 90% rename from src/shared/lib/__tests__/themes.test.ts rename to src/lib/__tests__/themes.test.ts index da0d293b8..653b425e0 100644 --- a/src/shared/lib/__tests__/themes.test.ts +++ b/src/lib/__tests__/themes.test.ts @@ -1,6 +1,8 @@ import { describe, test, expect } from 'vitest'; -import { themes } from '../themes'; +import { themes as themesMap } from '../themes'; + +const themes = Object.values(themesMap); describe('themes', () => { test('themes should have a unique identifier', () => { diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 000000000..c503358d3 --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,49 @@ +import { invoke } from '@tauri-apps/api/core'; + +import type { Config } from '../generated/typings'; + +import { logAndNotifyError } from './utils'; + +/** + * Config Bridge for the UI to communicate with the backend + */ +class ConfigManager { + initialConfig: Config | null = null; + + /** + * Get the initial value of the config at the time of instantiation. + * Should only be used when starting the app. + */ + getInitial(key: T): Config[T] { + if (window.__MUSEEKS_INITIAL_CONFIG == undefined) { + throw new Error('Config has not been injected from the back-end'); + } + + return window.__MUSEEKS_INITIAL_CONFIG[key]; + } + + async getAll(): Promise { + // TODO: check data shape? + return invoke('plugin:config|get_config'); + } + + async get(key: T): Promise { + const config = await this.getAll(); + return config[key]; + } + + async set(key: T, value: Config[T]): Promise { + const config = await this.getAll(); + config[key] = value; + + try { + invoke('plugin:config|set_config', { config }); + } catch (err) { + logAndNotifyError(err); + } + + return; + } +} + +export default new ConfigManager(); diff --git a/src/lib/icons.ts b/src/lib/icons.ts new file mode 100644 index 000000000..392129a83 --- /dev/null +++ b/src/lib/icons.ts @@ -0,0 +1,23 @@ +/* eslint-disable import/no-unresolved */ + +import NEXT from '../assets/icons/player-next.svg?raw'; +import PAUSE from '../assets/icons/player-pause.svg?raw'; +import PLAY from '../assets/icons/player-play.svg?raw'; +import PREVIOUS from '../assets/icons/player-previous.svg?raw'; +import QUEUE from '../assets/icons/player-queue.svg?raw'; +import REPEAT_ONE from '../assets/icons/player-repeat-one.svg?raw'; +import REPEAT from '../assets/icons/player-repeat.svg?raw'; +import SHUFFLE from '../assets/icons/player-shuffle.svg?raw'; + +const icons = { + NEXT, + PAUSE, + PLAY, + PREVIOUS, + QUEUE, + REPEAT_ONE, + REPEAT, + SHUFFLE, +}; + +export default icons; diff --git a/src/shared/lib/ipc-channels.ts b/src/lib/ipc-channels.ts similarity index 100% rename from src/shared/lib/ipc-channels.ts rename to src/lib/ipc-channels.ts diff --git a/src/lib/library.ts b/src/lib/library.ts new file mode 100644 index 000000000..7995e9d06 --- /dev/null +++ b/src/lib/library.ts @@ -0,0 +1,106 @@ +import { convertFileSrc, invoke } from '@tauri-apps/api/core'; + +import type { Playlist, Track } from '../generated/typings'; + +/** + * Library Bridge for the UI to communicate with the backend + */ +const library = { + // --------------------------------------------------------------------------- + // Playlists read/write actions + // --------------------------------------------------------------------------- + + async getAllTracks(): Promise> { + return invoke('plugin:database|get_all_tracks'); + }, + + async getTracks(trackIDs: Array): Promise> { + return invoke('plugin:database|get_tracks', { + ids: trackIDs, + }); + }, + + async updateTrack(track: Track) { + // TODO: + throw new Error('Not implemented'); + }, + + async removeTracks(trackIDs: Array): Promise> { + return invoke('plugin:database|remove_tracks', { + ids: trackIDs, + }); + }, + + async importTracks(importPaths: Array): Promise { + return invoke('plugin:database|import_tracks_to_library', { + importPaths, + }); + }, + + // --------------------------------------------------------------------------- + // Playlists read/write actions + // --------------------------------------------------------------------------- + + async getAllPlaylists(): Promise> { + return invoke('plugin:database|get_all_playlists'); + }, + + async getPlaylist(id: string): Promise { + return invoke('plugin:database|get_playlist', { + id, + }); + }, + + async createPlaylist(name: string, tracks: Array): Promise { + return invoke('plugin:database|create_playlist', { + name, + tracks, + }); + }, + + async renamePlaylist(id: string, name: string): Promise { + return invoke('plugin:database|rename_playlist', { + id, + name, + }); + }, + + async setPlaylistTracks( + id: string, + tracks: Array, + ): Promise { + return invoke('plugin:database|set_playlist_tracks', { + id, + tracks, + }); + }, + + async deletePlaylist(id: string): Promise { + return invoke('plugin:database|delete_playlist', { + id, + }); + }, + + // --------------------------------------------------------------------------- + // Misc. + // --------------------------------------------------------------------------- + async reset(): Promise { + return invoke('plugin:database|reset'); + }, + + async getCover(path: string): Promise { + const cover = await invoke('plugin:cover|get_cover', { + path, + }); + + if (cover === null) { + return null; + } + + return cover.startsWith('data:') ? cover : convertFileSrc(cover); + }, + + // removeTracks +}; + +export default library; diff --git a/src/shared/lib/logger.ts b/src/lib/logger.ts similarity index 100% rename from src/shared/lib/logger.ts rename to src/lib/logger.ts diff --git a/src/renderer/lib/player.ts b/src/lib/player.ts similarity index 65% rename from src/renderer/lib/player.ts rename to src/lib/player.ts index 3114973ed..971afdf56 100644 --- a/src/renderer/lib/player.ts +++ b/src/lib/player.ts @@ -1,4 +1,9 @@ -import { TrackModel } from '../../shared/types/museeks'; +import { convertFileSrc } from '@tauri-apps/api/core'; + +import { Track } from '../generated/typings'; + +import config from './config'; +import { logAndNotifyError } from './utils'; interface PlayerOptions { playbackRate?: number; @@ -17,9 +22,7 @@ interface PlayerOptions { */ class Player { private audio: HTMLAudioElement; - private durationThresholdReached: boolean; - private track: TrackModel | null; - public threshold: number; + private track: Track | null; constructor(options?: PlayerOptions) { const mergedOptions = { @@ -36,13 +39,11 @@ class Player { this.audio.defaultPlaybackRate = mergedOptions.playbackRate; // eslint-disable-next-line // @ts-ignore - this.audio.setSinkId(mergedOptions.audioOutputDevice); + // TODO: + // this.audio.setSinkId(mergedOptions.audioOutputDevice); this.audio.playbackRate = mergedOptions.playbackRate; this.audio.volume = mergedOptions.volume; this.audio.muted = mergedOptions.muted; - - this.threshold = 0.75; - this.durationThresholdReached = false; } async play() { @@ -90,21 +91,22 @@ class Player { } async setOutputDevice(deviceID: string) { - // eslint-disable-next-line - // @ts-ignore - await this.audio.setSinkId(deviceID); + try { + // eslint-disable-next-line + // @ts-ignore + await this.audio.setSinkId(deviceID); + } catch (err) { + logAndNotifyError(err); + } } getTrack() { return this.track; } - setTrack(track: TrackModel) { + setTrack(track: Track) { this.track = track; - this.audio.src = window.MuseeksAPI.library.parseUri(track.path); - - // When we change song, need to update the thresholdReached indicator. - this.durationThresholdReached = false; + this.audio.src = convertFileSrc(track.path); } setCurrentTime(currentTime: number) { @@ -118,28 +120,16 @@ class Player { isPaused() { return this.audio.paused; } - - isThresholdReached() { - if ( - !this.durationThresholdReached && - this.audio.currentTime >= this.audio.duration * this.threshold - ) { - this.durationThresholdReached = true; - } - - return this.durationThresholdReached; - } } /** * Export a singleton by default, for the sake of simplicity (and we only need * one anyway) */ -const { config } = window.MuseeksAPI; export default new Player({ - volume: config.__initialConfig['audioVolume'], - playbackRate: config.__initialConfig['audioPlaybackRate'], - audioOutputDevice: config.__initialConfig['audioOutputDevice'], - muted: config.__initialConfig['audioMuted'], + volume: config.getInitial('audio_volume'), + playbackRate: config.getInitial('audio_playback_rate'), + audioOutputDevice: config.getInitial('audio_output_device'), + muted: config.getInitial('audio_muted'), }); diff --git a/src/lib/query.ts b/src/lib/query.ts new file mode 100644 index 000000000..3382ebccb --- /dev/null +++ b/src/lib/query.ts @@ -0,0 +1,17 @@ +import { QueryClient } from '@tanstack/react-query'; + +import router from '../views/router'; + +export const queryClient = new QueryClient(); + +export function invalidate() { + // Need to call mutate with undefined to make sure stale-while-revalidate is + // reset (otherwise, we'd see a "no tracks in the library" instead of "loading") + queryClient.invalidateQueries({ + exact: true, + queryKey: ['tracks'], + }); + + // Reload the route data + router.revalidate(); +} diff --git a/src/lib/sort-orders.ts b/src/lib/sort-orders.ts new file mode 100644 index 000000000..43c4013ab --- /dev/null +++ b/src/lib/sort-orders.ts @@ -0,0 +1,68 @@ +import { SortOrder, SortBy, Track } from '../generated/typings'; +import { Path } from '../types/museeks'; + +import { stripAccents } from './utils-id3'; + +// For perforances reasons, otherwise _.orderBy will perform weird check +// the is far more resource/time impactful +const getArtist = (t: Track): string => + stripAccents(t.artists.toString().toLowerCase()); +const getGenre = (t: Track): string => + stripAccents(t.genres.toString().toLowerCase()); +const getAlbum = (t: Track): string => stripAccents(t.album.toLowerCase()); +const getTitle = (t: Track): string => stripAccents(t.title.toLowerCase()); + +type TrackKeys = Path; +type IterateeFunction = (track: Track) => string; +export type SortTuple = [ + Array, + Array<'asc' | 'desc'>, +]; + +// Declarations +const sortOrders: Record> = { + Artist: { + Asc: [ + // Default + [getArtist, 'year', getAlbum, 'disk.no', 'track.no'], + ['asc'], + ], + Dsc: [[getArtist, 'year', getAlbum, 'disk.no', 'track.no'], ['desc']], + }, + Title: { + Asc: [ + [getTitle, getArtist, 'year', getAlbum, 'disk.no', 'track.no'], + ['asc'], + ], + Dsc: [ + [getTitle, getArtist, 'year', getAlbum, 'disk.no', 'track.no'], + ['desc'], + ], + }, + Duration: { + Asc: [ + ['duration', getArtist, 'year', getAlbum, 'disk.no', 'track.no'], + ['asc'], + ], + Dsc: [ + ['duration', getArtist, 'year', getAlbum, 'disk.no', 'track.no'], + ['desc'], + ], + }, + Album: { + Asc: [[getAlbum, getArtist, 'year', 'disk.no', 'track.no'], ['asc']], + Dsc: [[getAlbum, getArtist, 'year', 'disk.no', 'track.no'], ['desc']], + }, + Genre: { + Asc: [ + [getGenre, getArtist, 'year', getAlbum, 'disk.no', 'track.no'], + ['asc'], + ], + Dsc: [ + [getGenre, getArtist, 'year', getAlbum, 'disk.no', 'track.no'], + ['desc'], + ], + }, +}; + +export default sortOrders; diff --git a/src/shared/lib/themes.ts b/src/lib/themes.ts similarity index 53% rename from src/shared/lib/themes.ts rename to src/lib/themes.ts index 239bec13a..c2b67e862 100644 --- a/src/shared/lib/themes.ts +++ b/src/lib/themes.ts @@ -1,6 +1,11 @@ -import { Theme } from '../types/museeks'; +import { Theme as ThemeID } from '@tauri-apps/api/window'; + // IMPROVE ME: scan the directory for all json files instead +import { Theme } from '../types/museeks'; import lightTheme from '../themes/light.json'; import darkTheme from '../themes/dark.json'; -export const themes = [lightTheme as Theme, darkTheme as Theme]; +export const themes: Record = { + light: lightTheme as Theme, + dark: darkTheme as Theme, +}; diff --git a/src/renderer/lib/utils-events.ts b/src/lib/utils-events.ts similarity index 77% rename from src/renderer/lib/utils-events.ts rename to src/lib/utils-events.ts index 6bb4a93d6..4619479a4 100644 --- a/src/renderer/lib/utils-events.ts +++ b/src/lib/utils-events.ts @@ -15,6 +15,13 @@ export function preventNativeDefault(e: Event) { e.preventDefault(); } +/** + * Check if we are running in a dev environment + */ +export function isDev() { + return window.location.host.startsWith('localhost:'); +} + /** * Returns true if * - the control key was pressed on a non-mac platform @@ -23,7 +30,7 @@ export function preventNativeDefault(e: Event) { export function isCtrlKey( e: React.KeyboardEvent | React.MouseEvent | KeyboardEvent, ): boolean { - const isMacOS = window.MuseeksAPI.platform === 'darwin'; + const isMacOS = window.__MUSEEKS_PLATFORM === 'macos'; return (isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey); } @@ -31,7 +38,7 @@ export function isCtrlKey( export function isAltKey( e: React.KeyboardEvent | React.MouseEvent | KeyboardEvent, ): boolean { - const isMacOS = window.MuseeksAPI.platform === 'darwin'; + const isMacOS = window.__MUSEEKS_PLATFORM === 'macos'; return (isMacOS && e.ctrlKey) || (!isMacOS && e.metaKey); } diff --git a/src/lib/utils-id3.ts b/src/lib/utils-id3.ts new file mode 100644 index 000000000..9d6130835 --- /dev/null +++ b/src/lib/utils-id3.ts @@ -0,0 +1,31 @@ +import { Track } from '../generated/typings'; +import { TrackSearchableFields } from '../types/museeks'; + +const ACCENTS = + 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž'; +const ACCENT_REPLACEMENTS = + 'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz'; + +/** + * Strip accent from String. From https://jsperf.com/strip-accents + */ +export const stripAccents = (str: string): string => { + const split = ACCENTS.split('').join('|'); + const reg = new RegExp(`(${split})`, 'g'); + + function replacement(a: string) { + return ACCENT_REPLACEMENTS[ACCENTS.indexOf(a)] || ''; + } + + return str.replace(reg, replacement).toLowerCase(); +}; + +/** + * Take a track a returns its lowered metadata (used for search) + */ +export const getLoweredMeta = (track: Track): TrackSearchableFields => ({ + artists: track.artists.map((v) => stripAccents(v.toLowerCase())), + album: stripAccents(track.album.toLowerCase()), + title: stripAccents(track.title.toLowerCase()), + genres: track.genres.map((v) => stripAccents(v.toLowerCase())), +}); diff --git a/src/lib/utils-library.ts b/src/lib/utils-library.ts new file mode 100644 index 000000000..b8df81c01 --- /dev/null +++ b/src/lib/utils-library.ts @@ -0,0 +1,42 @@ +import orderBy from 'lodash/orderBy'; + +import { Track } from '../generated/typings'; + +import { SortTuple } from './sort-orders'; +import * as utils from './utils'; +import { stripAccents } from './utils-id3'; + +/** + * Filter an array of tracks by string + */ +export const filterTracks = (tracks: Track[], search: string): Track[] => { + // Avoid performing useless searches + if (search.length === 0) return tracks; + + // Unoptimized, bad + return tracks.filter( + (track) => + stripAccents(track.artists.toString().toLowerCase()).includes(search) || + stripAccents(track.album.toLowerCase()).includes(search) || + stripAccents(track.genres.toString().toLowerCase()).includes(search) || + stripAccents(track.title.toLowerCase()).includes(search), + ); +}; + +/** + * Sort an array of tracks (alias to lodash.orderby) + */ +export const sortTracks = (tracks: Track[], sort: SortTuple): Track[] => { + const [columns, order] = sort; + return orderBy(tracks, columns, order); +}; + +/** + * Format a list of tracks to a nice status + */ +export const getStatus = (tracks: Track[]): string => { + const status = utils.parseDuration( + tracks.map((d) => d.duration).reduce((a, b) => a + b, 0), + ); + return `${tracks.length} tracks, ${status}`; +}; diff --git a/src/renderer/lib/utils-player.ts b/src/lib/utils-player.ts similarity index 79% rename from src/renderer/lib/utils-player.ts rename to src/lib/utils-player.ts index 239438da4..67cb85c4a 100644 --- a/src/renderer/lib/utils-player.ts +++ b/src/lib/utils-player.ts @@ -1,13 +1,10 @@ -import { TrackModel } from '../../shared/types/museeks'; +import { Track } from '../generated/typings'; /** * Shuffle an array with a Player behavior in mind: * the currently-playing track should remain the same, */ -export const shuffleTracks = ( - tracks: TrackModel[], - index: number, -): TrackModel[] => { +export const shuffleTracks = (tracks: Track[], index: number): Track[] => { const shuffledTracks = [...tracks]; const currentTrack = shuffledTracks.splice(index, 1)[0]; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 000000000..5167f1824 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,42 @@ +import { error } from '@tauri-apps/plugin-log'; + +import useToastsStore from '../stores/useToastsStore'; + +/** + * Parse an int to a more readable string + */ +export const parseDuration = (duration: number | null): string => { + if (duration !== null) { + const hours = Math.trunc(duration / 3600); + const minutes = Math.trunc(duration / 60) % 60; + const seconds = Math.trunc(duration) % 60; + + const hoursStringified = hours < 10 ? `0${hours}` : hours; + const minutesStringified = minutes < 10 ? `0${minutes}` : minutes; + const secondsStringified = seconds < 10 ? `0${seconds}` : seconds; + + let result = hours > 0 ? `${hoursStringified}:` : ''; + result += `${minutesStringified}:${secondsStringified}`; + + return result; + } + + return '00:00'; +}; + +/** + * Friendly logging for caught errors + * https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript + */ +export const logAndNotifyError = (err: unknown, pre?: string): void => { + let message; + if (err instanceof Error) message = err.message; + else message = String(err); + + if (pre != null) { + message = `${pre}: ${message}`; + } + + error(message); + useToastsStore.getState().api.add('danger', message); +}; diff --git a/src/renderer/entrypoint.tsx b/src/main.tsx similarity index 74% rename from src/renderer/entrypoint.tsx rename to src/main.tsx index e524cbf87..756353c89 100644 --- a/src/renderer/entrypoint.tsx +++ b/src/main.tsx @@ -6,11 +6,13 @@ import React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; import { RouterProvider } from 'react-router-dom'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { attachConsole } from '@tauri-apps/plugin-log'; import router from './views/router'; +import { queryClient } from './lib/query'; + /* |-------------------------------------------------------------------------- | Styles @@ -26,15 +28,19 @@ import './styles/main.module.css'; |-------------------------------------------------------------------------- */ +attachConsole(); + const wrap = document.getElementById('wrap'); if (wrap) { const root = ReactDOM.createRoot(wrap); root.render( - + - + , ); +} else { + document.body.innerHTML = '
x_x
'; } diff --git a/src/main/entrypoint.ts b/src/main/entrypoint.ts deleted file mode 100644 index 554d8c998..000000000 --- a/src/main/entrypoint.ts +++ /dev/null @@ -1,129 +0,0 @@ -import path from 'path'; - -import { app, BrowserWindow } from 'electron'; -import remote from '@electron/remote/main'; - -import logger from '../shared/lib/logger'; - -import AppModule from './modules/AppModule'; -import ApplicationMenuModule from './modules/ApplicationMenuModule'; -import ConfigModule from './modules/ConfigModule'; -import PowerModule from './modules/PowerMonitorModule'; -import ThumbarModule from './modules/ThumbarModule'; -import DockMenuModule from './modules/DockMenuDarwinModule'; -import SleepBlockerModule from './modules/SleepBlockerModule'; -import DialogsModule from './modules/DialogsModule'; -import NativeThemeModule from './modules/NativeThemeModule'; -import DevtoolsModule from './modules/DevtoolsModule'; -import WindowPositionModule from './modules/WindowPositionModule'; -import IPCCoverModule from './modules/IPCCoverModule'; -import IPCLibraryModule from './modules/IPCLibraryModule'; -import IPCNotificationsModule from './modules/IPCNotificationsModule'; -import IPCPlaylistsModule from './modules/IPCPlaylistsModule'; -import * as ModulesManager from './lib/modules-manager'; -import { checkBounds } from './lib/utils'; - -const appRoot = path.resolve(__dirname, '..'); // Careful, not future-proof -const rendererDistPath = path.join(appRoot, 'renderer'); -const preloadDistPath = path.join(appRoot, 'preload'); - -// @deprecated Remove all usage of remote in the app -remote.initialize(); - -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the javascript object is GCed. -let mainWindow: Electron.BrowserWindow | null = null; - -// Quit when all windows are closed -app.on('window-all-closed', () => { - app.quit(); -}); - -// This method will be called when Electron has finished its -// initialization and ready to create browser windows. -app.on('ready', async () => { - const configModule = new ConfigModule(); - await ModulesManager.init(configModule); - const config = configModule.getConfig(); - - const bounds = checkBounds(config.get('bounds')); - - // Create the browser window - mainWindow = new BrowserWindow({ - title: 'Museeks', - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - minWidth: 900, - minHeight: 550, - frame: true, - autoHideMenuBar: true, - titleBarStyle: 'hiddenInset', // MacOS polished window - show: false, - webPreferences: { - // sandbox cannot be removed before we remove all those usage from preload: - // https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts - sandbox: false, - allowRunningInsecureContent: false, - autoplayPolicy: 'no-user-gesture-required', - webSecurity: process.env.ELECTRON_RENDERER_URL == null, // Cannot load local resources without that - preload: path.join(preloadDistPath, 'entrypoint.js'), - }, - }); - - // Open dev tools if museeks runs in debug or development mode - if ( - process.argv.includes('--devtools') || - process.env.NODE_ENV === 'development' || - process.env.VITE_DEV_SERVER_URL - ) { - mainWindow.webContents.openDevTools({ mode: 'detach' }); - } - - mainWindow.on('closed', () => { - // Dereference the window object - mainWindow = null; - }); - - // Prevent webContents from opening new windows (e.g ctrl-click on link) - mainWindow.webContents.setWindowOpenHandler(() => { - return { action: 'deny' }; - }); - - // @deprecated Remove all usage of remote in the app - remote.enable(mainWindow.webContents); - - // ... and load the html page generated by Vite - const viewSuffix = `#/${config.get('defaultView')}`; - - let url: string; - - if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) { - url = `${process.env['ELECTRON_RENDERER_URL']}${viewSuffix}`; - } else { - url = `file://${rendererDistPath}/index.html${viewSuffix}`; - } - - logger.info(`Loading main window with "${url}"`); - mainWindow.loadURL(url); - - // Let's list the list of modules we will use for Museeks - ModulesManager.init( - new AppModule(mainWindow, config), - new PowerModule(mainWindow), - new ApplicationMenuModule(mainWindow), - new ThumbarModule(mainWindow), - new DockMenuModule(mainWindow), - new SleepBlockerModule(mainWindow), - new DialogsModule(mainWindow), - new NativeThemeModule(mainWindow, config), - new DevtoolsModule(mainWindow), - new WindowPositionModule(mainWindow, config), - // Modules used to handle IPC APIs - new IPCCoverModule(mainWindow), - new IPCLibraryModule(mainWindow), - new IPCNotificationsModule(mainWindow, config), - new IPCPlaylistsModule(mainWindow), - ).catch(logger.error); -}); diff --git a/src/main/lib/modules-manager.ts b/src/main/lib/modules-manager.ts deleted file mode 100644 index 8d0499e47..000000000 --- a/src/main/lib/modules-manager.ts +++ /dev/null @@ -1,16 +0,0 @@ -import logger from '../../shared/lib/logger'; -import Module from '../modules/BaseModule'; - -export const init = async (...modules: Module[]): Promise => { - await Promise.allSettled( - modules.map((module) => - module.init().catch((err) => { - throw err; - }), - ), - ).catch((err) => { - logger.error( - `An error occured when loading ${module.constructor.name} could not be loaded:\n${err}`, - ); - }); -}; diff --git a/src/main/lib/utils-cover.ts b/src/main/lib/utils-cover.ts deleted file mode 100644 index 6c84a2489..000000000 --- a/src/main/lib/utils-cover.ts +++ /dev/null @@ -1,88 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -import * as mmd from 'music-metadata'; -import { globby } from 'globby'; - -import logger from '../../shared/lib/logger'; - -const SUPPORTED_COVER_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.bmp', '.gif']; -const SUPPORTED_COVER_NAMES = ['album', 'albumart', 'folder', 'cover', 'front']; - -/** - * Parse data to be used by img/background-image with base64 - */ -export const parseBase64 = (format: string, data: string): string => { - return `data:${format};base64,${data}`; -}; - -/** - * Determine if a file is a valid cover or not - */ -const isValidFilename = (pathname: path.ParsedPath): boolean => { - const isExtensionValid = SUPPORTED_COVER_EXTENSIONS.includes( - pathname.ext.toLowerCase(), - ); - const isNameValid = SUPPORTED_COVER_NAMES.some((name) => { - return pathname.name.toLowerCase().includes(name); - }); - - return isExtensionValid && isNameValid; -}; - -/** - * Smart fetch cover (from id3 or file directory) - */ -export const fetchCover = async ( - trackPath: string, - ignoreID3 = false, - base64 = false, -): Promise => { - if (!trackPath) { - return null; - } - - if (!ignoreID3) { - const data = await mmd.parseFile(trackPath); - const picture = data.common.picture && data.common.picture[0]; - - if (picture) { - // If cover in id3 - return parseBase64(picture.format, picture.data.toString('base64')); - } - } - - // scan folder for any cover image - const folder = path.dirname(trackPath); - const pattern = `${folder.replace(/\\/g, '/')}/*`; - - const matches = await globby(pattern, { followSymbolicLinks: false }); - - const match = matches.find((elem) => { - return isValidFilename(path.parse(elem)); - }); - - if (match) { - if (base64) return getFileAsBase64(match); - - return `file://${match}`; - } - - return null; -}; - -/** - * Returns the given file as a base64 string - */ -export const getFileAsBase64 = async ( - filePath: string, -): Promise => { - try { - const content = fs.readFileSync(filePath, { encoding: 'base64' }); - return parseBase64(path.extname(filePath).substr(1), content); - } catch (err) { - logger.warn('Could not get cover as base64:'); - logger.warn(err); - return null; - } -}; diff --git a/src/main/lib/utils-m3u.ts b/src/main/lib/utils-m3u.ts deleted file mode 100644 index 3681f2a6f..000000000 --- a/src/main/lib/utils-m3u.ts +++ /dev/null @@ -1,55 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -import chardet from 'chardet'; -import iconv from 'iconv-lite'; - -import logger from '../../shared/lib/logger'; - -const isFile = (path: string) => fs.lstatSync(path).isFile(); - -/** - * Analyze a .m3u file and returns the resolved path of each song from it - */ -export const parse = (filePath: string): string[] => { - try { - const baseDir = path.parse(filePath).dir; - const content = fs.readFileSync(filePath); - const encoding = chardet.detect(content); - - if (typeof encoding !== 'string') { - throw new Error(`could not guess the file encoding (${filePath})`); - } - - const decodedContent = iconv.decode(content, encoding); - - const files = decodedContent.split(/\r?\n/).reduce((acc, line) => { - if (line.length === 0) { - return acc; - } - - // If absolute path - if (fs.existsSync(path.resolve(line)) && isFile(path.resolve(line))) { - acc.push(path.resolve(line)); - return acc; - } - - // If relative Path - if ( - fs.existsSync(path.resolve(baseDir, line)) && - isFile(path.resolve(baseDir, line)) - ) { - acc.push(path.resolve(baseDir, line)); - return acc; - } - - return acc; - }, [] as string[]); - - return files; - } catch (err) { - logger.warn(err); - } - - return []; -}; diff --git a/src/main/lib/utils.ts b/src/main/lib/utils.ts deleted file mode 100644 index 1b1237d31..000000000 --- a/src/main/lib/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import electron from 'electron'; - -import { ConfigBounds } from '../../shared/types/museeks'; - -const DEFAULT_WIDTH = 900; -const DEFAULT_HEIGHT = 550; - -export const checkBounds = function ( - bounds: ConfigBounds | undefined, -): ConfigBounds { - if (bounds === undefined) { - bounds = { - x: 0, - y: 0, - height: 0, - width: 0, - }; - } - - // check if the browser window is offscreen - const display = electron.screen.getDisplayNearestPoint({ - x: Math.round(bounds.x), - y: Math.round(bounds.y), - }).workArea; - - const onScreen = - bounds.x >= display.x && - bounds.x + bounds.width <= display.x + display.width && - bounds.y >= display.y && - bounds.y + bounds.height <= display.y + display.height; - - if (!onScreen) { - return { - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - x: display.width / 2 - DEFAULT_WIDTH / 2, - y: display.height / 2 - DEFAULT_HEIGHT / 2, - }; - } - - return bounds; -}; diff --git a/src/main/modules/AppModule.ts b/src/main/modules/AppModule.ts deleted file mode 100644 index 2b93bfdc3..000000000 --- a/src/main/modules/AppModule.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Module in charge of handling the different window behavior based on platforms - */ - -import os from 'os'; - -import type Store from 'electron-store'; -import { ipcMain, app } from 'electron'; - -import logger from '../../shared/lib/logger'; -import channels from '../../shared/lib/ipc-channels'; -import { Config } from '../../shared/types/museeks'; - -import ModuleWindow from './BaseWindowModule'; - -export default class AppModule extends ModuleWindow { - protected config: Store; - protected forceQuit: boolean; - - constructor(window: Electron.BrowserWindow, config: Store) { - super(window); - - this.config = config; - this.forceQuit = false; - } - - async load(): Promise { - // Make the app a single-instance app - this.ensureSingleInstance(); - - // Shows app only once it is loaded (avoid initial white flash) - ipcMain.once(channels.APP_READY, () => { - if (this.window) { - this.window.show(); - } - }); - - // Restart the app with the same arguments - ipcMain.on(channels.APP_RESTART, () => { - app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) }); - app.exit(0); - }); - - // Prevent the window to be closed, hide it instead (to continue audio playback) - this.window.on('close', (e) => { - this.close(e); - }); - - ipcMain.on(channels.APP_CLOSE, (e) => { - this.close(e); - }); - - // Click on the dock icon to show the app again on macOS - app.on('activate', () => { - if (this.window) { - this.window.show(); - this.window.focus(); - } - }); - - // Small hack to check on MacOS if the dock close action has been clicked - // https://stackoverflow.com/questions/35008347/electron-close-w-x-vs-right-click-dock-and-quit#35782702 - app.on('before-quit', () => { - this.forceQuit = true; - }); - } - - close(e: Electron.Event): void { - if (this.forceQuit || os.platform() !== 'darwin') { - app.quit(); - this.window.destroy(); - } else { - e.preventDefault(); - this.window.hide(); - } - } - - ensureSingleInstance(): void { - const gotTheLock = app.requestSingleInstanceLock(); - - app.on('second-instance', () => { - // Someone tried to run a second instance, we should focus our window. - if (this.window) { - if (this.window.isMinimized()) this.window.restore(); - this.window.focus(); - } - }); - - if (!gotTheLock) { - logger.info( - 'Another instance of Museeks is already running, quitting...', - ); - app.quit(); - } - } -} diff --git a/src/main/modules/ApplicationMenuModule.ts b/src/main/modules/ApplicationMenuModule.ts deleted file mode 100644 index 12a22eb94..000000000 --- a/src/main/modules/ApplicationMenuModule.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Module in charge of the app menu - * Litteraly stolen from: https://electronjs.org/docs/api/menu#examples - */ - -import { Menu, shell } from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; - -import ModuleWindow from './BaseWindowModule'; - -export default class ApplicationMenuModule extends ModuleWindow { - async load(): Promise { - const template: Electron.MenuItemConstructorOptions[] = [ - { role: 'appMenu' }, - { role: 'fileMenu' }, - { role: 'editMenu' }, - { - label: 'View', - submenu: [ - { - label: 'Jump to playing track', - accelerator: 'CmdOrCtrl+T', - click: () => { - this.window.webContents.send(channels.MENU_JUMP_TO_PLAYING_TRACK); - }, - }, - { type: 'separator' }, - { - label: 'Go to library', - accelerator: 'CmdOrCtrl+L', - click: () => { - this.window.webContents.send(channels.MENU_GO_TO_LIBRARY); - }, - }, - { - label: 'Go to playlists', - accelerator: 'CmdOrCtrl+P', - click: () => { - this.window.webContents.send(channels.MENU_GO_TO_PLAYLISTS); - }, - }, - { type: 'separator' }, - { role: 'reload' }, - { role: 'forceReload' }, - { role: 'toggleDevTools' }, - { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ], - }, - { role: 'windowMenu' }, - { - role: 'help', - submenu: [ - { - label: 'Learn More', - click: async () => { - await shell.openExternal('https://museeks.io'); - }, - }, - ], - }, - ]; - - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - } -} diff --git a/src/main/modules/BaseModule.ts b/src/main/modules/BaseModule.ts deleted file mode 100644 index 20b76a9e5..000000000 --- a/src/main/modules/BaseModule.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Example of Module, other modules should extent this class - */ - -import os from 'os'; - -import logger from '../../shared/lib/logger'; - -export default class Module { - protected loaded: boolean; - protected platforms: NodeJS.Platform[]; - - constructor() { - this.loaded = false; - this.platforms = ['win32', 'linux', 'darwin']; - } - - // To not be overriden - async init(): Promise { - if (this.loaded) - throw new TypeError(`Module ${this.constructor.name} is already loaded`); - - if (this.platforms.includes(os.platform())) { - await this.load().catch((err) => { - throw err; - }); - this.loaded = true; - logger.info(`Loaded ${this.constructor.name}`); - } else { - logger.info( - `Skipping load of ${ - this.constructor.name - } (supported platform: ${this.platforms.join(', ')})`, - ); - } - } - - // Can (now) be an asynchronous method - async load(): Promise { - throw new TypeError( - `Module ${this.constructor.name} should have a load() method`, - ); - // Do whatever you want here :) - } -} diff --git a/src/main/modules/BaseWindowModule.ts b/src/main/modules/BaseWindowModule.ts deleted file mode 100644 index f28c42694..000000000 --- a/src/main/modules/BaseWindowModule.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Example of Module, other modules should extent this class - */ - -import Module from './BaseModule'; - -export default class ModuleWindow extends Module { - protected window: Electron.BrowserWindow; - - constructor(window: Electron.BrowserWindow) { - super(); - this.window = window; - } - - getWindow(): Electron.BrowserWindow { - return this.window; - } -} diff --git a/src/main/modules/ConfigModule.ts b/src/main/modules/ConfigModule.ts deleted file mode 100644 index 7b639a214..000000000 --- a/src/main/modules/ConfigModule.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Essential module for creating/loading the app config - */ - -import electron, { app, ipcMain } from 'electron'; -import Store from 'electron-store'; - -import { Config, Repeat, SortBy, SortOrder } from '../../shared/types/museeks'; -import channels from '../../shared/lib/ipc-channels'; -import logger from '../../shared/lib/logger'; - -import Module from './BaseModule'; - -export default class ConfigModule extends Module { - private workArea: Electron.Rectangle; - private config: Store; - - constructor() { - super(); - - logger.info(`Using "${app.getPath('userData')}" as config path`); - - this.workArea = electron.screen.getPrimaryDisplay().workArea; - this.config = new Store({ - name: 'config', - defaults: this.getDefaultConfig(), - }); - - // A few manual migrations, electron-store migratons don't seem to play well - // new setting - if (this.config.get('tracksDensity') == undefined) { - logger.info('Config: setting "tracksDensity" option'); - this.config.set('tracksDensity', 'normal'); - } - - logger.child; - - // dark-legacy is gone - if (this.config.get('theme') === 'dark-legacy') { - logger.info('Config: disabling dark-legacy theme'); - this.config.set('theme', 'dark'); - } - } - - async load(): Promise { - ipcMain.on(channels.CONFIG_GET_ALL, (event) => { - event.returnValue = this.config.store; - }); - - ipcMain.handle(channels.CONFIG_GET_ALL, (): Config => this.config.store); - - ipcMain.handle( - channels.CONFIG_GET, - (_e: Electron.Event, key: T): Config[T] => { - logger.debug('Config get', key); - return this.config.get(key); - }, - ); - - ipcMain.handle( - channels.CONFIG_SET, - ( - _e: Electron.Event, - key: T, - value: Config[T], - ): void => { - logger.debug('Config set', key, value); - this.config.set(key, value); - }, - ); - } - - getConfig(): Store { - const config = this.config; - - if (config === undefined) { - throw new Error('Config is not defined, has it been loaded?'); - } - - return config; - } - - getDefaultConfig(): Config { - const config: Config = { - theme: '__system', - audioVolume: 1, - audioPlaybackRate: 1, - audioOutputDevice: 'default', - audioMuted: false, - audioShuffle: false, - audioRepeat: Repeat.NONE, - tracksDensity: 'normal', - defaultView: 'library', - librarySort: { - by: SortBy.ARTIST, - order: SortOrder.ASC, - }, - // musicFolders: [], - sleepBlocker: false, - autoUpdateChecker: true, - displayNotifications: true, - bounds: { - width: 1000, - height: 600, - x: Math.round(this.workArea.width / 2), - y: Math.round(this.workArea.height / 2), - }, - }; - - return config; - } -} diff --git a/src/main/modules/DevtoolsModule.ts b/src/main/modules/DevtoolsModule.ts deleted file mode 100644 index 88da558a6..000000000 --- a/src/main/modules/DevtoolsModule.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Module in charge of loading devtools extensions to ensure a good developer - * experience - */ - -import installExtensions, { - REDUX_DEVTOOLS, - REACT_DEVELOPER_TOOLS, -} from 'electron-devtools-assembler'; - -import logger from '../../shared/lib/logger'; - -import ModuleWindow from './BaseWindowModule'; - -export default class DevtoolsModule extends ModuleWindow { - async load(): Promise { - const isProduction = process.env.NODE_ENV === 'production'; - - // Let's install some extensions so it's easier for us to debug things - if (!isProduction) { - try { - await installExtensions([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS], { - loadExtensionOptions: { - allowFileAccess: true, - }, - }); - logger.info('Added devtools extensions'); - } catch (err) { - logger.warn('An error occurred while trying to add extensions:\n', err); - } - } - } -} diff --git a/src/main/modules/DialogsModule.ts b/src/main/modules/DialogsModule.ts deleted file mode 100644 index 0e654dd3d..000000000 --- a/src/main/modules/DialogsModule.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Module in charge of receiving dialogs requests from the renderer process - * and returning data if needed - */ - -import { dialog, ipcMain } from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; - -import ModuleWindow from './BaseWindowModule'; - -export default class DialogsModule extends ModuleWindow { - async load(): Promise { - /** - * showMessageBox - */ - ipcMain.handle( - channels.DIALOG_MESSAGE_BOX, - async (_event, options: Electron.MessageBoxOptions) => { - const result = await dialog.showMessageBox(this.window, options); - - return result; - }, - ); - - /** - * showOpenDialog - */ - ipcMain.handle( - channels.DIALOG_OPEN, - async (_event, options: Electron.OpenDialogOptions) => { - const result = await dialog.showOpenDialog(options); - - return result; - }, - ); - } -} diff --git a/src/main/modules/DockMenuDarwinModule.ts b/src/main/modules/DockMenuDarwinModule.ts deleted file mode 100644 index 61277f5f3..000000000 --- a/src/main/modules/DockMenuDarwinModule.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Module in charge of the dock menu on macOS - */ - -import { IpcMainEvent, Menu, app, ipcMain } from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; -import { PlayerStatus, TrackModel } from '../../shared/types/museeks'; - -import ModuleWindow from './BaseWindowModule'; - -export default class DockMenuDarwinModule extends ModuleWindow { - protected menu: Electron.MenuItemConstructorOptions[]; - protected songDetails: Electron.MenuItemConstructorOptions[]; - protected playToggle: Electron.MenuItemConstructorOptions[]; - protected pauseToggle: Electron.MenuItemConstructorOptions[]; - - constructor(window: Electron.BrowserWindow) { - super(window); - this.platforms = ['darwin']; - - this.menu = []; - this.songDetails = []; - this.playToggle = []; - this.pauseToggle = []; - } - - async load(): Promise { - this.songDetails = [ - { - label: 'Not playing', - enabled: false, - }, - { - type: 'separator', - }, - ]; - - this.playToggle = [ - { - label: 'Play', - click: () => { - this.window.webContents.send(channels.PLAYBACK_PLAY); - }, - }, - ]; - - this.pauseToggle = [ - { - label: 'Pause', - click: () => { - this.window.webContents.send(channels.PLAYBACK_PAUSE); - }, - }, - ]; - - this.menu = [ - { - label: 'Previous', - click: () => { - this.window.webContents.send(channels.PLAYBACK_PREVIOUS); - }, - }, - { - label: 'Next', - click: () => { - this.window.webContents.send(channels.PLAYBACK_NEXT); - }, - }, - ]; - - // Load events listener for player actions - ipcMain.on(channels.PLAYBACK_PLAY, () => { - this.setDockMenu(PlayerStatus.PLAY); - }); - - ipcMain.on(channels.PLAYBACK_PAUSE, () => { - this.setDockMenu(PlayerStatus.PAUSE); - }); - - ipcMain.on( - channels.PLAYBACK_TRACK_CHANGE, - (_e: IpcMainEvent, track: TrackModel) => { - this.updateDockMenu(track); - this.setDockMenu(PlayerStatus.PLAY); - }, - ); - - this.setDockMenu(PlayerStatus.PAUSE); - } - - setDockMenu(state: PlayerStatus): void { - const playPauseItem = state === 'play' ? this.pauseToggle : this.playToggle; - const menuTemplate = [...this.songDetails, ...playPauseItem, ...this.menu]; - app.dock.setMenu(Menu.buildFromTemplate(menuTemplate)); - } - - updateDockMenu(metadata: TrackModel): void { - this.songDetails = [ - { - label: `${metadata.title}`, - enabled: false, - }, - { - label: `by ${metadata.artist}`, - enabled: false, - }, - { - label: `on ${metadata.album}`, - enabled: false, - }, - { - type: 'separator', - }, - ]; - } -} diff --git a/src/main/modules/IPCCoverModule.ts b/src/main/modules/IPCCoverModule.ts deleted file mode 100644 index f49bc8507..000000000 --- a/src/main/modules/IPCCoverModule.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ipcMain } from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; -import { fetchCover } from '../lib/utils-cover'; - -import ModuleWindow from './BaseWindowModule'; - -/** - * Module in charge of returning the cover data for a given track - */ -export default class IPCCoverModule extends ModuleWindow { - async load(): Promise { - ipcMain.handle( - channels.COVER_GET, - (_e, path: string): Promise => { - return fetchCover(path, false, true); - }, - ); - } -} diff --git a/src/main/modules/IPCLibraryModule.ts b/src/main/modules/IPCLibraryModule.ts deleted file mode 100644 index 301be4329..000000000 --- a/src/main/modules/IPCLibraryModule.ts +++ /dev/null @@ -1,331 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron'; -import { globby } from 'globby'; -import * as mmd from 'music-metadata'; -import queue from 'queue'; -import pickBy from 'lodash/pickBy'; - -import * as m3u from '../lib/utils-m3u'; -import channels from '../../shared/lib/ipc-channels'; -import { Track } from '../../shared/types/museeks'; -import logger, { loggerExtras } from '../../shared/lib/logger'; -import { getLoweredMeta } from '../../shared/lib/utils-id3'; - -import ModuleWindow from './BaseWindowModule'; - -interface ScanFile { - path: string; - stat: fs.Stats; -} - -/* -|-------------------------------------------------------------------------- -| supported Formats -|-------------------------------------------------------------------------- -*/ - -const SUPPORTED_TRACKS_EXTENSIONS = [ - // MP3 / MP4 - '.mp3', - '.mp4', - '.aac', - '.m4a', - '.3gp', - '.wav', - // Opus - '.ogg', - '.ogv', - '.ogm', - '.opus', - // Flac - '.flac', - // web media - '.webm', -]; - -const SUPPORTED_PLAYLISTS_EXTENSIONS = ['.m3u']; - -/** - * Module in charge of renderer <> main processes communication regarding - * library management, covers, playlists etc. - */ -class IPCLibraryModule extends ModuleWindow { - public import: { - processed: number; - total: number; - }; - - constructor(window: BrowserWindow) { - super(window); - - this.import = { - processed: 0, - total: 0, - }; - } - - async load(): Promise { - ipcMain.handle( - channels.LIBRARY_IMPORT_TRACKS, - this.importTracks.bind(this), - ); - ipcMain.handle(channels.LIBRARY_LOOKUP, this.libraryLookup.bind(this)); - ipcMain.handle(channels.PLAYLISTS_RESOLVE_M3U, this.resolveM3u.bind(this)); - } - - // --------------------------------------------------------------------------- - // IPC Events - // --------------------------------------------------------------------------- - - /** - * Scan the file system and return all music files and playlists that may be - * safely imported to Museeks. - */ - private async libraryLookup( - _e: IpcMainInvokeEvent, - pathsToScan: string[], - ): Promise<[string[], string[]]> { - logger.info('Starting tracks lookup', pathsToScan); - loggerExtras.time('Library lookup'); - - // 1. Get the stats for all the files/paths - const paths = await Promise.all(pathsToScan.map(this.getStats)); - - // 2. Split directories and files - const files: string[] = []; - const folders: string[] = []; - - paths.forEach((elem) => { - if (elem.stat.isFile()) files.push(elem.path); - if (elem.stat.isDirectory() || elem.stat.isSymbolicLink()) - folders.push(elem.path); - }); - - // 3. Scan all the directories with globby - const globbies = folders.map((folder) => { - // Normalize slashes and escape regex special characters - const pattern = `${folder - .replace(/\\/g, '/') - // I'm not sure about this eslint-ignore - // eslint-disable-next-line no-useless-escape - .replace(/([$^*+?()\[\]])/g, '\\$1')}/**/*.*`; - - return globby(pattern, { followSymbolicLinks: true }); - }); - - const subDirectoriesFiles = await Promise.all(globbies); - // Scan folders and add files to library - - // 4. Merge all path arrays together and filter them with the extensions we support - const allFiles = subDirectoriesFiles - .reduce((acc, array) => acc.concat(array), [] as string[]) - .concat(files); // Add the initial files - - const supportedTrackFiles = allFiles.filter((filePath) => { - const extension = path.extname(filePath).toLowerCase(); - return SUPPORTED_TRACKS_EXTENSIONS.includes(extension); - }); - - const supportedPlaylistsFiles = allFiles.filter((filePath) => { - const extension = path.extname(filePath).toLowerCase(); - return SUPPORTED_PLAYLISTS_EXTENSIONS.includes(extension); - }); - - loggerExtras.timeEnd('Library lookup'); - - return [supportedTrackFiles, supportedPlaylistsFiles]; - } - - private async resolveM3u( - _e: IpcMainInvokeEvent, - path: string, - ): Promise { - return m3u.parse(path); - } - - /** - * Now: returns the id3 tags of all the given tracks path - * Tomorrow: do DB insertion here - */ - async importTracks( - _e: IpcMainInvokeEvent, - tracksPath: string[], - ): Promise { - logger.info(`Starting import of ${tracksPath.length} tracks`); - loggerExtras.time('Tracks scan'); - - return new Promise((resolve, reject) => { - if (tracksPath.length === 0) return; - - try { - // Instantiate queue - const scannedFiles: Track[] = []; - - const scanQueue = new queue(); - scanQueue.concurrency = 32; - scanQueue.autostart = false; - - scanQueue.addEventListener('end', () => { - this.import.processed = 0; - this.import.total = 0; - - loggerExtras.timeEnd('Tracks scan'); - resolve(scannedFiles); - }); - // End queue instantiation - - this.import.total += tracksPath.length; - - // Add all the items to the queue - tracksPath.forEach((filePath, index) => { - scanQueue.push(async (callback) => { - try { - // Normalize (back)slashes on Windows - filePath = path.resolve(filePath); - - // Check if there is an existing record in the DB - // const existingDoc = await db.tracks.findOnlyByPath(filePath); - - // If there is existing document - // if (!existingDoc) { - // Get metadata - const track = await this.getMetadata(filePath); - // const insertedDoc = await db.tracks.insert(track); - scannedFiles.push(track); - // } - - this.import.processed++; - } catch (err) { - logger.warn(err); - } finally { - if (index % 50 == 0) { - logger.debug(`Finished scanning ${index} tracks`); - } - } - - if (callback) callback(); - }); - }); - - scanQueue.start(); - } catch (err) { - reject(err); - } - }); - } - - // --------------------------------------------------------------------------- - // Helpers - // --------------------------------------------------------------------------- - - private getDefaultMetadata(): Track { - return { - album: 'Unknown', - artist: ['Unknown artist'], - disk: { - no: 0, - of: 0, - }, - duration: 0, - genre: [], - loweredMetas: { - artist: ['unknown artist'], - album: 'unknown', - title: '', - genre: [], - }, - path: '', - playCount: 0, - title: '', - track: { - no: 0, - of: 0, - }, - year: null, - }; - } - - private parseMusicMetadata( - data: mmd.IAudioMetadata, - trackPath: string, - ): Partial { - const { common, format } = data; - - const metadata = { - album: common.album, - artist: - common.artists || - (common.artist && [common.artist]) || - (common.albumartist && [common.albumartist]), - disk: common.disk, - duration: format.duration, - genre: common.genre, - title: common.title || path.parse(trackPath).base, - track: common.track, - year: common.year, - }; - - return pickBy(metadata); - } - - /** - * Get a file ID3 metadata - */ - private async getMetadata(trackPath: string): Promise { - const defaultMetadata = this.getDefaultMetadata(); - - const basicMetadata: Track = { - ...defaultMetadata, - path: trackPath, - }; - - try { - const data = await mmd.parseFile(trackPath, { - skipCovers: true, - duration: true, - }); - - // Let's try to define something with what we got so far... - const parsedData = this.parseMusicMetadata(data, trackPath); - - const metadata: Track = { - ...defaultMetadata, - ...parsedData, - path: trackPath, - }; - - metadata.loweredMetas = getLoweredMeta(metadata); - - // Let's try another wat to retrieve a track duration - // if (metadata.duration < 0.5) { - // try { - // metadata.duration = await getAudioDuration(trackPath); - // } catch (err) { - // logger.warn(`An error occured while getting ${trackPath} duration: ${err}`); - // } - // } - - return metadata; - } catch (err) { - logger.warn( - `An error occured while reading ${trackPath} id3 tags: ${err}`, - ); - } - - return basicMetadata; - } - - /** - * Get stats of a file while keeping the path itself - */ - private async getStats(folderPath: string): Promise { - return { - path: folderPath, - stat: await fs.promises.stat(folderPath), - }; - } -} - -export default IPCLibraryModule; diff --git a/src/main/modules/IPCNotificationsModule.ts b/src/main/modules/IPCNotificationsModule.ts deleted file mode 100644 index 9e9933bb1..000000000 --- a/src/main/modules/IPCNotificationsModule.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type Store from 'electron-store'; -import { - ipcMain, - IpcMainEvent, - NativeImage, - nativeImage, - Notification, -} from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; -import { Config, TrackModel } from '../../shared/types/museeks'; -import * as utilsCover from '../lib/utils-cover'; - -import ModuleWindow from './BaseWindowModule'; - -/** - * Module in charge of displaying native notifications on certain user actions - */ -export default class IPCNotificationsModule extends ModuleWindow { - protected config: Store; - - constructor(window: Electron.BrowserWindow, config: Store) { - super(window); - this.config = config; - } - - async load(): Promise { - ipcMain.on( - channels.PLAYBACK_PLAY, - (_e: IpcMainEvent, track: TrackModel) => { - this.sendPlaybackNotification(track); - }, - ); - } - - private async sendPlaybackNotification(track: TrackModel): Promise { - if ( - this.window.isFocused() || - this.config.get('displayNotifications') === false - ) { - return; - } - - const cover = await utilsCover.fetchCover(track.path); - - let icon: NativeImage | undefined = undefined; - - if (cover !== null) { - icon = nativeImage.createFromDataURL(cover); - } - - const notification = new Notification({ - title: track.title, - body: `${track.artist}\n${track.album}`, - icon, - silent: true, - }); - - notification.on('click', () => { - this.window.show(); - this.window.focus(); - }); - - notification.show(); - } -} diff --git a/src/main/modules/IPCPlaylistsModule.ts b/src/main/modules/IPCPlaylistsModule.ts deleted file mode 100644 index e40335631..000000000 --- a/src/main/modules/IPCPlaylistsModule.ts +++ /dev/null @@ -1,70 +0,0 @@ -import path from 'path'; - -import { app, dialog, ipcMain } from 'electron'; -import * as m3u from 'm3ujs'; - -import channels from '../../shared/lib/ipc-channels'; -import logger from '../../shared/lib/logger'; - -import ModuleWindow from './BaseWindowModule'; - -/** - * Module in charge of returning the cover data for a given track - */ -class IPCPlaylistsModule extends ModuleWindow { - async load(): Promise { - ipcMain.on( - channels.PLAYLIST_EXPORT, - async (_e, name: string, tracksPath: string[]) => { - const { filePath } = await dialog.showSaveDialog(this.window, { - title: 'Export playlist', - defaultPath: path.resolve(app.getPath('music'), name), - filters: [ - { - extensions: ['m3u'], - name: 'random', - }, - ], - }); - - if (filePath) { - try { - const playlistExport = new m3u.Playlist( - new m3u.TypeEXTM3U((entry) => { - if (entry instanceof m3u.Mp3Entry) { - return `${entry.artist} - ${entry.album} - ${entry.track} - ${entry.title}`; - } - return entry.displayName; - }), - ); - - tracksPath.forEach((trackPath) => { - try { - playlistExport.add(new m3u.Mp3Entry(trackPath)); - } catch (err) { - logger.warn(err); - } - }); - - playlistExport.write(filePath); - } catch (err: unknown) { - logger.warn(err); - if (err instanceof Error) { - dialog.showErrorBox( - `An error occured when exporting the playlist "${name}"`, - err.message, - ); - } else { - dialog.showErrorBox( - `An error occured when exporting the playlist "${name}"`, - 'Unknown error', - ); - } - } - } - }, - ); - } -} - -export default IPCPlaylistsModule; diff --git a/src/main/modules/NativeThemeModule.ts b/src/main/modules/NativeThemeModule.ts deleted file mode 100644 index 98953be42..000000000 --- a/src/main/modules/NativeThemeModule.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Module in charge of telling the renderer process which native theme is used - */ - -import type Store from 'electron-store'; -import { ipcMain, nativeTheme } from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; -import { themes } from '../../shared/lib/themes'; -import { Config, Theme } from '../../shared/types/museeks'; - -import ModuleWindow from './BaseWindowModule'; - -export default class NativeThemeModule extends ModuleWindow { - protected config: Store; - - constructor(window: Electron.BrowserWindow, config: Store) { - super(window); - this.config = config; - - const theme = this.getTheme(); - nativeTheme.themeSource = theme.themeSource; - } - - async load(): Promise { - /** - * Update the UI when someone changes the global theme settings - */ - nativeTheme.on('updated', () => { - if (this.getThemeID() === '__system') { - this.applyTheme(this.getSystemThemeID()); - } - - // Otherwise, we don't care - }); - - ipcMain.handle(channels.THEME_GET_ID, () => this.getThemeID()); - - /** - * Handle themeSource update and returns the theme variables for a given - * themeID - */ - ipcMain.handle( - channels.THEME_SET_ID, - (_event, themeID: Config['theme']) => { - this.setThemeID(themeID); - }, - ); - - ipcMain.handle(channels.THEME_GET, () => { - let themeID = this.getThemeID(); - - if (themeID === '__system') themeID = this.getSystemThemeID(); - - const theme = themes.find((theme) => theme._id === themeID); - - if (!theme) throw new RangeError(`No theme found with ID ${themeID}`); - - return theme; - }); - } - - getThemeID(): Config['theme'] { - return this.config.get('theme') ?? '__system'; - } - - getTheme(): Theme { - let themeID = this.getThemeID(); - - if (themeID === '__system') { - themeID = this.getSystemThemeID(); - } - - const theme = themes.find((theme) => theme._id === themeID); - - if (!theme) throw new RangeError(`No theme found with ID ${themeID}`); - - return theme; - } - - setThemeID(themeID: Config['theme']): void { - this.config.set('theme', themeID); - - if (themeID === '__system') { - nativeTheme.themeSource = 'system'; - this.applyTheme(this.getSystemThemeID()); - } else { - const theme = themes.find((theme) => theme._id === themeID); - - if (!theme) throw new RangeError(`No theme found with ID ${themeID}`); - - nativeTheme.themeSource = theme.themeSource; - this.applyTheme(theme._id); - } - } - - applyTheme(themeID: Theme['_id']): void { - const theme = themes.find((theme) => theme._id === themeID); - - if (!theme) throw new RangeError(`No theme found with ID ${themeID}`); - - this.window.webContents.send(channels.THEME_APPLY, theme); - } - - getSystemThemeID(): Theme['_id'] { - return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; - } -} diff --git a/src/main/modules/PowerMonitorModule.ts b/src/main/modules/PowerMonitorModule.ts deleted file mode 100644 index ad9fc5a0f..000000000 --- a/src/main/modules/PowerMonitorModule.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Module in charge of pausing the player when going into sleep - */ - -import electron from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; - -import ModuleWindow from './BaseWindowModule'; - -export default class PowerMonitorModule extends ModuleWindow { - async load(): Promise { - const { powerMonitor } = electron; - const { window } = this; - - powerMonitor.on('suspend', () => { - window.webContents.send(channels.PLAYBACK_PAUSE); - }); - } -} diff --git a/src/main/modules/SleepBlockerModule.ts b/src/main/modules/SleepBlockerModule.ts deleted file mode 100644 index 788820195..000000000 --- a/src/main/modules/SleepBlockerModule.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Module in charge of preventing the computer to go to sleep - */ - -import { powerSaveBlocker, ipcMain, IpcMainEvent } from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; - -import ModuleWindow from './BaseWindowModule'; - -export default class SleepBlocker extends ModuleWindow { - protected sleepBlockerID: number; - protected enabled: boolean; - - constructor(window: Electron.BrowserWindow) { - super(window); - - this.enabled = false; - this.sleepBlockerID = 0; - this.platforms = ['win32', 'darwin', 'linux']; - } - - onStartPlayback = (): void => { - if (this.enabled && !powerSaveBlocker.isStarted(this.sleepBlockerID)) { - // or 'prevent-display-sleep' - this.sleepBlockerID = powerSaveBlocker.start('prevent-app-suspension'); - } - }; - - onStopPlayback = (): void => { - if (powerSaveBlocker.isStarted(this.sleepBlockerID)) { - powerSaveBlocker.stop(this.sleepBlockerID); - } - }; - - toggleSleepBlocker = (_event: IpcMainEvent, value: boolean): void => { - this.enabled = value; - }; - - async load(): Promise { - ipcMain.on(channels.SETTINGS_TOGGLE_SLEEP_BLOCKER, this.toggleSleepBlocker); - ipcMain.on(channels.PLAYBACK_PLAY, this.onStartPlayback); - ipcMain.on(channels.PLAYBACK_PAUSE, this.onStopPlayback); - ipcMain.on(channels.PLAYBACK_STOP, this.onStopPlayback); - } -} diff --git a/src/main/modules/ThumbarModule.ts b/src/main/modules/ThumbarModule.ts deleted file mode 100644 index 6ee6182cb..000000000 --- a/src/main/modules/ThumbarModule.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Module in charge of the Windows Thumbar - * (buttons appearing on Museeks preview when hovering the icon) - * - * Windows only - */ - -import path from 'path'; - -import { nativeImage, ipcMain } from 'electron'; - -import channels from '../../shared/lib/ipc-channels'; - -import ModuleWindow from './BaseWindowModule'; - -const { createFromPath } = nativeImage; - -const iconsDirectory = path.resolve( - path.join(__dirname, '../../src/shared/assets/icons/windows'), -); - -export default class ThumbarModule extends ModuleWindow { - constructor(window: Electron.BrowserWindow) { - super(window); - this.platforms = ['win32']; - } - - async load(): Promise { - const { window } = this; - - const icons = { - play: createFromPath(path.join(iconsDirectory, 'play.ico')), - // playDisabled: createFromPath(path.join(iconsDirectory, 'play-disabled.ico')), - pause: createFromPath(path.join(iconsDirectory, 'pause.ico')), - pauseDisabled: createFromPath( - path.join(iconsDirectory, 'pause-disabled.ico'), - ), - prev: createFromPath(path.join(iconsDirectory, 'backward.ico')), - prevDisabled: createFromPath( - path.join(iconsDirectory, 'backward-disabled.ico'), - ), - next: createFromPath(path.join(iconsDirectory, 'forward.ico')), - nextDisabled: createFromPath( - path.join(iconsDirectory, 'forward-disabled.ico'), - ), - }; - - const thumbarButtons = { - play: { - tooltip: 'Play', - icon: icons.play, - click: () => { - window.webContents.send(channels.PLAYBACK_PLAY); - }, - }, - /* playDisabled: { - tooltip: 'Play', - flags: ['disabled'], - icon: icons.playDisabled, - click: () => { return null; } // Electron's TypeScript definition issue - }, */ - pause: { - tooltip: 'Pause', - icon: icons.pause, - click: () => { - window.webContents.send(channels.PLAYBACK_PAUSE); - }, - }, - pauseDisabled: { - tooltip: 'Pause', - flags: ['disabled'], - icon: icons.pauseDisabled, - click: () => { - return null; - }, // Electron's TypeScript definition issue - }, - prev: { - tooltip: 'Prev', - icon: icons.prev, - click: () => { - window.webContents.send(channels.PLAYBACK_PREVIOUS); - }, - }, - prevDisabled: { - tooltip: 'Prev', - flags: ['disabled'], - icon: icons.prevDisabled, - click: () => { - return null; - }, // Electron's TypeScript definition issue - }, - next: { - tooltip: 'Next', - icon: icons.next, - click: () => { - window.webContents.send(channels.PLAYBACK_NEXT); - }, - }, - nextDisabled: { - tooltip: 'Next', - flags: ['disabled'], - icon: icons.nextDisabled, - click: () => { - return null; - }, // Electron's TypeScript definition issue - }, - }; - - ipcMain.once(channels.APP_READY, () => { - window.setThumbarButtons([ - thumbarButtons.prevDisabled, - thumbarButtons.play, - thumbarButtons.nextDisabled, - ]); - }); - - ipcMain.on(channels.PLAYBACK_PLAY, () => { - window.setThumbarButtons([ - thumbarButtons.prev, - thumbarButtons.pause, - thumbarButtons.next, - ]); - }); - - ipcMain.on(channels.PLAYBACK_PAUSE, () => { - window.setThumbarButtons([ - thumbarButtons.prev, - thumbarButtons.play, - thumbarButtons.next, - ]); - }); - - ipcMain.on(channels.PLAYBACK_STOP, () => { - window.setThumbarButtons([ - thumbarButtons.prevDisabled, - thumbarButtons.play, - thumbarButtons.nextDisabled, - ]); - }); - } -} diff --git a/src/main/modules/WindowPositionModule.ts b/src/main/modules/WindowPositionModule.ts deleted file mode 100644 index a7db693cc..000000000 --- a/src/main/modules/WindowPositionModule.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Module in charge of remembering the window position, width and height - */ - -import type Store from 'electron-store'; -import debounce from 'lodash/debounce'; - -import { Config } from '../../shared/types/museeks'; - -import ModuleWindow from './BaseWindowModule'; - -export default class WindowPositionModule extends ModuleWindow { - protected config: Store; - - constructor(window: Electron.BrowserWindow, config: Store) { - super(window); - this.config = config; - } - - async load(): Promise { - this.window.on('resize', debounce(this.saveBounds, 250).bind(this)); - this.window.on('move', debounce(this.saveBounds, 250).bind(this)); - } - - saveBounds() { - const bounds = this.window.getBounds(); - this.config.set('bounds', bounds); - } -} diff --git a/src/main/tsconfig.json b/src/main/tsconfig.json deleted file mode 100644 index 61fe9549c..000000000 --- a/src/main/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext"] - }, - "include": ["../main", "../shared"], - // This imports preloads, which will fail as it requires DOM lib - "exclude": ["../shared/types/declarations/preload.d.ts"] -} diff --git a/src/preload/db.ts b/src/preload/db.ts deleted file mode 100644 index 1bba894a4..000000000 --- a/src/preload/db.ts +++ /dev/null @@ -1,217 +0,0 @@ -import path from 'path'; - -import { app } from '@electron/remote'; -import PouchDB from 'pouchdb'; -import PouchDBFind from 'pouchdb-find'; - -import { - Playlist, - PlaylistModel, - Track, - TrackModel, -} from '../shared/types/museeks'; - -/** - * This will ultimately move to the main process. Here for legacy purpose until - * we find a more suitable database as linvodb is not supported anymore. - */ -const pathUserData = app.getPath('userData'); - -PouchDB.plugin(PouchDBFind); - -const Tracks = new PouchDB(path.join(pathUserData, 'TracksDB'), { - adapter: 'leveldb', - auto_compaction: true, -}); - -Tracks.createIndex({ - index: { - fields: ['path'], - }, -}); - -const Playlists = new PouchDB( - path.join(pathUserData, 'PlaylistsDB'), - { - adapter: 'leveldb', - auto_compaction: true, - }, -); - -Playlists.createIndex({ - index: { - fields: ['importPath'], - }, -}); - -/** ---------------------------------------------------------------------------- - * Shared helpers - * -------------------------------------------------------------------------- */ - -async function reset(): Promise { - // We cannot use destroy() as it literally destroys it, when we just want to - // empty it - const [allTracks, allPlaylists] = await Promise.all([ - tracks.getAll(), - playlists.getAll(), - ]); - - const deletedTracks = allTracks.map((track) => ({ - ...track, - _deleted: true, - })); - - const deletedPlaylists = allPlaylists.map((playlist) => ({ - ...playlist, - _deleted: true, - })); - - await Promise.all([ - Tracks.bulkDocs(deletedTracks), - Playlists.bulkDocs(deletedPlaylists), - ]); -} - -/** ---------------------------------------------------------------------------- - * Tracks - * -------------------------------------------------------------------------- */ - -const tracks = { - async getAll(): Promise { - // Use custom IDs instead? - const [firstResponse, secondResponse] = await Promise.all([ - Tracks.allDocs({ include_docs: true, endkey: '_design' }), - Tracks.allDocs({ include_docs: true, startkey: '_design\uffff' }), - ]); - - const tracks = [...firstResponse.rows, ...secondResponse.rows] - .map((record) => record.doc) - .filter(Boolean); - - return tracks; - }, - - async insertMultiple(tracks: Track[]) { - return Tracks.bulkDocs(tracks); - }, - - async update(track: TrackModel) { - return Tracks.put(track); - }, - - async incrementPlayCount(track: TrackModel) { - const doc = await Tracks.get(track._id); - await Tracks.put({ - ...doc, - playCount: doc.playCount + 1, - }); - }, - - async remove(trackIDs: string[]): Promise { - const response = await Tracks.find({ - selector: { _id: { $in: trackIDs } }, - }); - const tracks = response.docs; - const deletedTracks = tracks.map((track) => ({ - ...track, - _deleted: true, - })); - - await Tracks.bulkDocs(deletedTracks); - }, - - async findByID(trackIDs: string[]): Promise { - const response = await Tracks.find({ - selector: { _id: { $in: trackIDs } }, - }); - return response.docs; - }, - - async findOnlyByID(trackID: string): Promise { - return Tracks.get(trackID); - }, - - async findByPath(paths: string[]): Promise { - const response = await Tracks.find({ selector: { path: { $in: paths } } }); - return response.docs; - }, - - async findOnlyByPath(path: string): Promise { - const response = await Tracks.find({ selector: { path } }); - const [track] = response.docs; - return track; - }, -}; - -/** ---------------------------------------------------------------------------- - * Playlists helpers - * -------------------------------------------------------------------------- */ - -const playlists = { - async getAll(): Promise { - // Use custom IDs instead? - const [firstResponse, secondResponse] = await Promise.all([ - Playlists.allDocs({ include_docs: true, endkey: '_design' }), - Playlists.allDocs({ include_docs: true, startkey: '_design\uffff' }), - ]); - - const playlists = [...firstResponse.rows, ...secondResponse.rows] - .map((record) => record.doc) - .filter(Boolean); - - return playlists; - }, - - async insert(playlist: Playlist) { - return Playlists.post(playlist); - }, - - async rename(playlistID: string, name: string) { - const playlist = await Playlists.get(playlistID); - Playlists.put({ - ...playlist, - name, - }); - }, - - async remove(playlistID: string) { - const playlist = await Playlists.get(playlistID); - - await Playlists.put({ - ...playlist, - _deleted: true, - }); - }, - - async findByID(playlistIDs: string[]): Promise { - const response = await Playlists.find({ - selector: { _id: { $in: playlistIDs } }, - }); - return response.docs; - }, - - async findOnlyByID(playlistID: string): Promise { - return Playlists.get(playlistID); - }, - - async setTracks(playlistID: string, tracksIDs: string[]) { - const playlist = await Playlists.get(playlistID); - - await Playlists.put({ - ...playlist, - tracks: tracksIDs, - }); - }, -}; - -/** ---------------------------------------------------------------------------- - * Export all the way! - * -------------------------------------------------------------------------- */ - -const db = { - reset, - tracks, - playlists, -}; - -export default db; diff --git a/src/preload/entrypoint.ts b/src/preload/entrypoint.ts deleted file mode 100644 index 2cb765b05..000000000 --- a/src/preload/entrypoint.ts +++ /dev/null @@ -1,150 +0,0 @@ -import os from 'os'; -import path from 'path'; - -import '@total-typescript/ts-reset'; -import { Menu, app } from '@electron/remote'; -import { IpcRendererEvent, contextBridge, ipcRenderer, shell } from 'electron'; - -import { Config, Track } from '../shared/types/museeks'; -import channels from '../shared/lib/ipc-channels'; -import { parseUri } from '../shared/lib/utils-uri'; - -import db from './db'; - -/** - * Ok, so what is there exactly? - * - * Preload is still very new to Museeks as I'm trying to upgrade Museeks to - * Electron's best practices, but basically: - * - * - We were using XActions in the past as an "API" - * - I am moving parts of those APIs here - * - Those APIs should at some point get rid of Node.js to enable contextbridge - * - Some of those APIs implementations may need to go to the main process instead - * - There's many things to refactor, some things will look weird as they are - * in an in-between state. - */ - -/* -|-------------------------------------------------------------------------- -| File association - make it work one day -|-------------------------------------------------------------------------- -*/ - -// TODO: only working on macOS, issue to follow: -// https://github.com/electron/electron/issues/27116 -// app.on('open-file', (event, path) => { -// event.preventDefault(); -// logger.info(path); // absolute path to file -// }); - -/* -|-------------------------------------------------------------------------- -| Config API: the config lives in the main process and we communicate with -| it via IPC -|-------------------------------------------------------------------------- -*/ - -const config = { - __initialConfig: ipcRenderer.sendSync(channels.CONFIG_GET_ALL), - getAll(): Promise { - return ipcRenderer.invoke(channels.CONFIG_GET_ALL); - }, - get(key: T): Promise { - return ipcRenderer.invoke(channels.CONFIG_GET, key); - }, - set(key: T, value: Config[T]): Promise { - return ipcRenderer.invoke(channels.CONFIG_SET, key, value); - }, -}; - -/* -|-------------------------------------------------------------------------- -| Window object extension -| TODO: some of these should go to the main process and be converted to use -| contextBridge.exposeToMainWorld + sandboxed renderer -|-------------------------------------------------------------------------- -*/ - -const ElectronAPI = { - ipcRenderer: { - // FIXME unsafe - // All these usage should probably go to the main process, or we should - // expose explicit APIs for what those usages are trying to solve - on: ( - channel: string, - listener: (event: IpcRendererEvent, value: unknown) => void, - ) => { - const listenerCount = ipcRenderer.listenerCount(channel); - if (listenerCount === 0) { - ipcRenderer.on(channel, listener); - } else { - // eslint-disable-next-line no-console - console.warn( - `Event "${channel}" already has ${listenerCount} listeners, aborting.`, - ); - } - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - off: ( - channel: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _listener: (event: IpcRendererEvent, value: unknown) => void, - ) => { - // Because we function cannot be passed between preload / renderer, - // ipcRenderer.off does not work. Until we fix the FIXME unsafe above - ipcRenderer.removeAllListeners(channel); - // ipcRenderer.off(channel, listener); - }, - send: ipcRenderer.send, - sendSync: ipcRenderer.sendSync, - invoke: ipcRenderer.invoke, - }, - menu: { - showContextMenu: (template: Electron.MenuItemConstructorOptions[]) => { - const context = Menu.buildFromTemplate(template); - context.popup({}); - }, - }, -}; - -// When editing something here, please update museeks.d.ts to extend the -// window.MuseeksAPI global object. -const MuseeksAPI = { - platform: os.platform(), - version: app.getVersion(), - config, - app: { - ready: () => ipcRenderer.send(channels.APP_READY), - restart: () => ipcRenderer.send(channels.APP_RESTART), - clone: () => ipcRenderer.send(channels.APP_CLOSE), - }, - db, - library: { - showTrackInFolder: (track: Track) => shell.showItemInFolder(track.path), - parseUri, - }, - playlists: { - resolveM3u: async (path: string): Promise => - await ipcRenderer.invoke(channels.PLAYLISTS_RESOLVE_M3U, path), - }, - covers: { - getCoverAsBase64: (track: Track) => - ipcRenderer.invoke(channels.COVER_GET, track.path), - }, - // TODO: all of the things below should be removed - path: { - parse: path.parse, - resolve: path.resolve, - }, - shell: { - openExternal: shell.openExternal, - openUserDataDirectory: () => shell.openPath(app.getPath('userData')), - }, -}; - -contextBridge.exposeInMainWorld('ElectronAPI', ElectronAPI); -contextBridge.exposeInMainWorld('MuseeksAPI', MuseeksAPI); - -export type ElectronAPI = typeof ElectronAPI; -export type MuseeksAPI = typeof MuseeksAPI; diff --git a/src/preload/tsconfig.json b/src/preload/tsconfig.json deleted file mode 100644 index 32d3e25d0..000000000 --- a/src/preload/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext", "DOM"] - }, - "include": ["../preload", "../shared"] -} diff --git a/src/renderer/components/Cover/Cover.tsx b/src/renderer/components/Cover/Cover.tsx deleted file mode 100644 index ce2d1f324..000000000 --- a/src/renderer/components/Cover/Cover.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useEffect, useState } from 'react'; -import * as AspectRatio from '@radix-ui/react-aspect-ratio'; - -import { Track } from '../../../shared/types/museeks'; - -import styles from './Cover.module.css'; - -type Props = { - track: Track; -}; - -export default function Cover(props: Props) { - const [coverPath, setCoverPath] = useState(null); - - useEffect(() => { - const refreshCover = async () => { - const coverPath = await window.MuseeksAPI.covers.getCoverAsBase64( - props.track, - ); - setCoverPath(coverPath); - }; - - refreshCover(); - }, [props.track]); - - if (coverPath) { - const encodedCoverPath = encodeURI(coverPath) - .replace(/'/g, "\\'") - .replace(/"/g, '\\"'); - - return ( - - Album cover - - ); - } - - return ( - -
- {/** billion dollar problem: convert emoji to text, good luck 🎵 */} -
-
-
- ); -} diff --git a/src/renderer/components/DropzoneImport/DropzoneImport.tsx b/src/renderer/components/DropzoneImport/DropzoneImport.tsx deleted file mode 100644 index 9e5ba2292..000000000 --- a/src/renderer/components/DropzoneImport/DropzoneImport.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import styles from './DropzoneImport.module.css'; - -type Props = { - title: string; - subtitle: string; - shown: boolean; -}; - -export default function DropzoneImport(props: Props) { - return ( -
-
{props.title}
-
{props.subtitle}
-
- ); -} diff --git a/src/renderer/components/Header/Header.tsx b/src/renderer/components/Header/Header.tsx deleted file mode 100644 index 98613f661..000000000 --- a/src/renderer/components/Header/Header.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import Icon from 'react-fontawesome'; -import * as Popover from '@radix-ui/react-popover'; - -import Queue from '../Queue/Queue'; -import PlayingBar from '../PlayingBar/PlayingBar'; -import PlayerControls from '../PlayerControls/PlayerControls'; -import Search from '../Search/Search'; -import usePlayerStore from '../../stores/usePlayerStore'; - -import styles from './Header.module.css'; - -export default function Header() { - const queue = usePlayerStore((state) => state.queue); - const queueCursor = usePlayerStore((state) => state.queueCursor); - - return ( -
-
- -
-
- -
-
- - - - - - - - - - -
-
- -
-
- ); -} diff --git a/src/renderer/components/ProgressBar/ProgressBar.module.css b/src/renderer/components/ProgressBar/ProgressBar.module.css deleted file mode 100644 index 91d288a0f..000000000 --- a/src/renderer/components/ProgressBar/ProgressBar.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.progress { - --progress-height: 6px; - - flex: 1; /* REMOVE */ - display: block; - height: var(--progress-height); - background-color: var(--progress-bg); - - &.animated { - .progressBar { - background-image: linear-gradient( - 45deg, - rgba(255 255 255 0.25) 25%, - transparent 25%, - transparent 50%, - rgba(255 255 255 0.25) 50%, - rgba(255 255 255 0.25) 75%, - transparent 75%, - transparent - ); - background-size: 10px 10px; - animation: progress-bar-animation 1s linear infinite; - } - } -} - -.progressBar { - height: var(--progress-height); - background-color: var(--main-color); -} - -@keyframes progress-bar-animation { - from { - background-position: 10px 0; - } - - to { - background-position: 0 0; - } -} diff --git a/src/renderer/constants/sort-orders.ts b/src/renderer/constants/sort-orders.ts deleted file mode 100644 index dab1ee881..000000000 --- a/src/renderer/constants/sort-orders.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Track, SortOrder, SortBy, Path } from '../../shared/types/museeks'; - -// For perforances reasons, otherwise _.orderBy will perform weird check -// the is far more resource/time impactful -const parseArtist = (t: Track): string => t.loweredMetas.artist[0].toString(); -const parseGenre = (t: Track): string => t.loweredMetas.genre.toString(); - -type TrackKeys = Path; -type IterateeFunction = (track: Track) => string; -export type SortTuple = [ - Array, - Array<'asc' | 'desc'>, -]; - -// Declarations -const sortOrders: Record> = { - [SortBy.ARTIST]: { - [SortOrder.ASC]: [ - // Default - [parseArtist, 'year', 'loweredMetas.album', 'disk.no', 'track.no'], - ['asc'], - ], - [SortOrder.DSC]: [ - [parseArtist, 'year', 'loweredMetas.album', 'disk.no', 'track.no'], - ['desc'], - ], - }, - [SortBy.TITLE]: { - [SortOrder.ASC]: [ - [ - 'loweredMetas.title', - parseArtist, - 'year', - 'loweredMetas.album', - 'disk.no', - 'track.no', - ], - ['asc'], - ], - [SortOrder.DSC]: [ - [ - 'loweredMetas.title', - parseArtist, - 'year', - 'loweredMetas.album', - 'disk.no', - 'track.no', - ], - ['desc'], - ], - }, - [SortBy.DURATION]: { - [SortOrder.ASC]: [ - [ - 'duration', - parseArtist, - 'year', - 'loweredMetas.album', - 'disk.no', - 'track.no', - ], - ['asc'], - ], - [SortOrder.DSC]: [ - [ - 'duration', - parseArtist, - 'year', - 'loweredMetas.album', - 'disk.no', - 'track.no', - ], - ['desc'], - ], - }, - [SortBy.ALBUM]: { - [SortOrder.ASC]: [ - ['loweredMetas.album', parseArtist, 'year', 'disk.no', 'track.no'], - ['asc'], - ], - [SortOrder.DSC]: [ - ['loweredMetas.album', parseArtist, 'year', 'disk.no', 'track.no'], - ['desc'], - ], - }, - [SortBy.GENRE]: { - [SortOrder.ASC]: [ - [ - parseGenre, - parseArtist, - 'year', - 'loweredMetas.album', - 'disk.no', - 'track.no', - ], - ['asc'], - ], - [SortOrder.DSC]: [ - [ - parseGenre, - parseArtist, - 'year', - 'loweredMetas.album', - 'disk.no', - 'track.no', - ], - ['desc'], - ], - }, -}; - -export default sortOrders; diff --git a/src/renderer/hooks/useCurrentViewTracks.ts b/src/renderer/hooks/useCurrentViewTracks.ts deleted file mode 100644 index 7828390de..000000000 --- a/src/renderer/hooks/useCurrentViewTracks.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useRouteLoaderData } from 'react-router-dom'; -import { useMemo } from 'react'; - -import { TrackModel } from '../../shared/types/museeks'; -import { RootLoaderData } from '../views/Root'; -import { PlaylistLoaderData } from '../views/ViewPlaylistDetails'; - -import useFilteredTracks from './useFilteredTracks'; - -type Maybe = T | undefined; - -/** - * Hook that returns the current view tracks (library or playlist) - */ -export default function useCurrentViewTracks(): TrackModel[] { - // TODO: how to support Settings page? Should we? - const rootData = useRouteLoaderData('root') as Maybe; - const filteredLibraryTracks = useFilteredTracks( - (rootData && rootData.tracks) ?? [], - ); - const playlistData = useRouteLoaderData( - 'playlist-details', - ) as Maybe; - - const tracks = useMemo(() => { - if (playlistData) { - return playlistData.playlistTracks; - } - - if (rootData) { - return filteredLibraryTracks; - } - - return []; - }, [filteredLibraryTracks, rootData, playlistData]); - - return tracks; -} diff --git a/src/renderer/hooks/useFilteredTracks.ts b/src/renderer/hooks/useFilteredTracks.ts deleted file mode 100644 index 2d62360c5..000000000 --- a/src/renderer/hooks/useFilteredTracks.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useMemo } from 'react'; - -import { TrackModel } from '../../shared/types/museeks'; -import { filterTracks, sortTracks } from '../lib/utils-library'; -import SORT_ORDERS from '../constants/sort-orders'; -import useLibraryStore from '../stores/useLibraryStore'; - -export default function useFilteredTracks(tracks: TrackModel[]): TrackModel[] { - const search = useLibraryStore((state) => state.search); - const sort = useLibraryStore((state) => state.sort); - - // Filter and sort TracksList - // sorting being a costly operation, do it after filtering - const filteredTracks = useMemo( - () => - sortTracks( - filterTracks(tracks, search), - SORT_ORDERS[sort.by][sort.order], - ), - [tracks, search, sort], - ); - - return filteredTracks; -} diff --git a/src/renderer/index.html b/src/renderer/index.html deleted file mode 100644 index 988682f2b..000000000 --- a/src/renderer/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Museeks - - - - -
- -
- - diff --git a/src/renderer/lib/dnd-types.ts b/src/renderer/lib/dnd-types.ts deleted file mode 100644 index 825129303..000000000 --- a/src/renderer/lib/dnd-types.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* -|-------------------------------------------------------------------------- -| The different kind of drag-and-drops, used to prevent conflicts between -| different drag-and-drops -|-------------------------------------------------------------------------- -*/ - -export const DND_TYPES = { - QUEUE_REORDER: 'QUEUE_REORDER', - PLAYLIST_REORDER: 'PLAYLIST_REORDER', -}; diff --git a/src/renderer/lib/icons.ts b/src/renderer/lib/icons.ts deleted file mode 100644 index 718b1c3c5..000000000 --- a/src/renderer/lib/icons.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable import/no-unresolved */ - -import NEXT from '../../shared/assets/icons/player-next.svg?raw'; -import PAUSE from '../../shared/assets/icons/player-pause.svg?raw'; -import PLAY from '../../shared/assets/icons/player-play.svg?raw'; -import PREVIOUS from '../../shared/assets/icons/player-previous.svg?raw'; -import QUEUE from '../../shared/assets/icons/player-queue.svg?raw'; -import REPEAT_ONE from '../../shared/assets/icons/player-repeat-one.svg?raw'; -import REPEAT from '../../shared/assets/icons/player-repeat.svg?raw'; -import SHUFFLE from '../../shared/assets/icons/player-shuffle.svg?raw'; - -const icons = { - NEXT, - PAUSE, - PLAY, - PREVIOUS, - QUEUE, - REPEAT_ONE, - REPEAT, - SHUFFLE, -}; - -export default icons; diff --git a/src/renderer/lib/utils-library.ts b/src/renderer/lib/utils-library.ts deleted file mode 100644 index 01922587a..000000000 --- a/src/renderer/lib/utils-library.ts +++ /dev/null @@ -1,46 +0,0 @@ -import orderBy from 'lodash/orderBy'; - -import { TrackModel } from '../../shared/types/museeks'; -import { SortTuple } from '../constants/sort-orders'; - -import * as utils from './utils'; - -/** - * Filter an array of tracks by string - */ -export const filterTracks = ( - tracks: TrackModel[], - search: string, -): TrackModel[] => { - // Avoid performing useless searches - if (search.length === 0) return tracks; - - return tracks.filter( - (track) => - track.loweredMetas.artist.toString().includes(search) || - track.loweredMetas.album.includes(search) || - track.loweredMetas.genre.toString().includes(search) || - track.loweredMetas.title.includes(search), - ); -}; - -/** - * Sort an array of tracks (alias to lodash.orderby) - */ -export const sortTracks = ( - tracks: TrackModel[], - sort: SortTuple, -): TrackModel[] => { - const [columns, order] = sort; - return orderBy(tracks, columns, order); -}; - -/** - * Format a list of tracks to a nice status - */ -export const getStatus = (tracks: TrackModel[]): string => { - const status = utils.parseDuration( - tracks.map((d) => d.duration).reduce((a, b) => a + b, 0), - ); - return `${tracks.length} tracks, ${status}`; -}; diff --git a/src/renderer/lib/utils.ts b/src/renderer/lib/utils.ts deleted file mode 100644 index 9d506caef..000000000 --- a/src/renderer/lib/utils.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Parse an int to a more readable string - */ -export const parseDuration = (duration: number | null): string => { - if (duration !== null) { - const hours = Math.trunc(duration / 3600); - const minutes = Math.trunc(duration / 60) % 60; - const seconds = Math.trunc(duration) % 60; - - const hoursStringified = hours < 10 ? `0${hours}` : hours; - const minutesStringified = minutes < 10 ? `0${minutes}` : minutes; - const secondsStringified = seconds < 10 ? `0${seconds}` : seconds; - - let result = hours > 0 ? `${hoursStringified}:` : ''; - result += `${minutesStringified}:${secondsStringified}`; - - return result; - } - - return '00:00'; -}; - -/** - * Remove duplicates (realpath) and useless children folders - */ -export const removeUselessFolders = (folders: string[]): string[] => { - // Remove duplicates - let filteredFolders = folders.filter( - (elem, index) => folders.indexOf(elem) === index, - ); - - const foldersToBeRemoved: string[] = []; - - filteredFolders.forEach((folder, i) => { - filteredFolders.forEach((subfolder, j) => { - if ( - subfolder.includes(folder) && - i !== j && - !foldersToBeRemoved.includes(folder) - ) { - foldersToBeRemoved.push(subfolder); - } - }); - }); - - filteredFolders = filteredFolders.filter( - (elem) => !foldersToBeRemoved.includes(elem), - ); - - return filteredFolders; -}; - -// export const getAudioDuration = (trackPath: string): Promise => { -// const audio = new Audio(); - -// return new Promise((resolve, reject) => { -// audio.addEventListener('loadedmetadata', () => { -// resolve(audio.duration); -// }); - -// audio.addEventListener('error', (e) => { -// // eslint-disable-next-line -// // @ts-ignore error event typing is wrong -// const message = `Error getting audio duration: (${e.currentTarget.error.code}) ${trackPath}`; -// reject(new Error(message)); -// }); - -// audio.preload = 'metadata'; -// // HACK no idea what other caracters could fuck things up -// audio.src = encodeURI(trackPath).replace('#', '%23'); -// }); -// }; diff --git a/src/renderer/stores/AppAPI.ts b/src/renderer/stores/AppAPI.ts deleted file mode 100644 index f18f89858..000000000 --- a/src/renderer/stores/AppAPI.ts +++ /dev/null @@ -1,15 +0,0 @@ -import SettingsAPI from './SettingsAPI'; - -const init = async (): Promise => { - await SettingsAPI.check(); - - // Tell the main process to show the window - window.MuseeksAPI.app.ready(); -}; - -// Should we use something else to harmonize between zustand and non-store APIs? -const AppAPI = { - init, -}; - -export default AppAPI; diff --git a/src/renderer/stores/useLibraryStore.ts b/src/renderer/stores/useLibraryStore.ts deleted file mode 100644 index 76675f0bd..000000000 --- a/src/renderer/stores/useLibraryStore.ts +++ /dev/null @@ -1,345 +0,0 @@ -import chunk from 'lodash/chunk'; -import type { MessageBoxReturnValue } from 'electron'; - -import { - LibrarySort, - SortBy, - SortOrder, - Track, - TrackEditableFields, - TrackModel, -} from '../../shared/types/museeks'; -import logger from '../../shared/lib/logger'; -import router from '../views/router'; -import channels from '../../shared/lib/ipc-channels'; -import { getLoweredMeta, stripAccents } from '../../shared/lib/utils-id3'; - -import PlaylistsAPI from './PlaylistsAPI'; -import { createStore } from './store-helpers'; -import useToastsStore from './useToastsStore'; -import usePlayerStore from './usePlayerStore'; - -const { path, db, config } = window.MuseeksAPI; -const { ipcRenderer } = window.ElectronAPI; - -type LibraryState = { - search: string; - sort: LibrarySort; - refreshing: boolean; - refresh: { - processed: number; - total: number; - }; - highlightPlayingTrack: boolean; - api: { - search: (value: string) => void; - sort: (sortBy: SortBy) => void; - scanPlaylists: (paths: string[]) => Promise; - add: (pathsToScan: string[]) => Promise; - remove: (tracksIDs: string[]) => Promise; - reset: () => Promise; - incrementPlayCount: (track: TrackModel) => Promise; - updateTrackMetadata: ( - trackID: string, - newFields: TrackEditableFields, - ) => Promise; - highlightPlayingTrack: (highlight: boolean) => void; - }; -}; - -const useLibraryStore = createStore((set, get) => ({ - search: '', - sort: config.__initialConfig['librarySort'], - refreshing: false, - refresh: { - processed: 0, - total: 0, - }, - highlightPlayingTrack: false, // hacky, fixme - - api: { - /** - * Filter tracks by search - */ - search: (search): void => { - set({ search: stripAccents(search) }); - }, - - /** - * Filter tracks by sort query - */ - sort: async (sortBy): Promise => { - const prevSort = get().sort; - - let sort: LibrarySort | null = null; - - // If same sort by, just reverse the order - if (sortBy === prevSort.by) { - sort = { - ...prevSort, - order: - prevSort.order === SortOrder.ASC ? SortOrder.DSC : SortOrder.ASC, - }; - } - // If it's different, then we assume the user needs ASC order by default - else { - sort = { - by: sortBy, - order: SortOrder.ASC, - }; - } - - await config.set('librarySort', sort); - - set({ sort }); - }, - - /** - * Scan a directory for playlists and import them - * TODO: probaly move to "main"? - */ - scanPlaylists: async (paths) => { - await Promise.all( - paths.map(async (filePath) => { - try { - const playlistFiles = - await window.MuseeksAPI.playlists.resolveM3u(filePath); - const playlistName = path.parse(filePath).name; - - const existingTracks: TrackModel[] = - await db.tracks.findByPath(playlistFiles); - - await PlaylistsAPI.create( - playlistName, - existingTracks.map((track) => track._id), - filePath, - ); - } catch (err) { - logger.warn(err); - } - }), - ); - }, - - /** - * Add tracks to Library - */ - add: async (pathsToScan): Promise => { - set({ refreshing: true }); - - try { - // Get all valid track paths - // TODO move this whole function to main process - const [supportedTrackFiles, supportedPlaylistsFiles] = - await ipcRenderer.invoke(channels.LIBRARY_LOOKUP, pathsToScan); - - if ( - supportedTrackFiles.length === 0 && - supportedPlaylistsFiles.length === 0 - ) { - set({ - refreshing: false, - refresh: { processed: 0, total: 0 }, - }); - return; - } - - // 5. Import the music tracks found the directories - const tracks: Track[] = await ipcRenderer.invoke( - channels.LIBRARY_IMPORT_TRACKS, - supportedTrackFiles, - ); - - const batchSize = 100; - const chunkedTracks = chunk(tracks, batchSize); - let processed = 0; - - await Promise.allSettled( - chunkedTracks.map(async (chunk) => { - // First, let's see if some of those files are already inserted - const insertedChunk = await db.tracks.insertMultiple(chunk); - - processed += batchSize; - - // Progress bar update - set({ - refresh: { - processed, - total: tracks.length, - }, - }); - - return insertedChunk; - }), - ); - - // TODO: do not re-import existing tracks - - // Import playlists found in the directories - await get().api.scanPlaylists(supportedPlaylistsFiles); - - router.revalidate(); - return; - } catch (err) { - useToastsStore - .getState() - .api.add('danger', 'An error occured when scanning the library'); - logger.warn(err); - return; - } finally { - set({ - refreshing: false, - refresh: { processed: 0, total: 0 }, - }); - } - }, - - /** - * remove tracks from library - */ - remove: async (tracksIDs) => { - // not calling await on it as it calls the synchonous message box - const options: Electron.MessageBoxOptions = { - buttons: ['Cancel', 'Remove'], - title: 'Remove tracks from library?', - message: `Are you sure you want to remove ${tracksIDs.length} element(s) from your library?`, - type: 'warning', - }; - - const result: MessageBoxReturnValue = await ipcRenderer.invoke( - channels.DIALOG_MESSAGE_BOX, - options, - ); - - if (result.response === 1) { - // button possition, here 'remove' - // Remove tracks from the Track collection - await db.tracks.remove(tracksIDs); - - router.revalidate(); - // That would be great to remove those ids from all the playlists, but it's not easy - // and should not cause strange behaviors, all PR for that would be really appreciated - // TODO: see if it's possible to remove the IDs from the selected state of TracksList as it "could" lead to strange behaviors - } - }, - - /** - * Reset the library - */ - reset: async (): Promise => { - usePlayerStore.getState().api.stop(); - try { - const options: Electron.MessageBoxOptions = { - buttons: ['Cancel', 'Reset'], - title: 'Reset library?', - message: - 'Are you sure you want to reset your library? All your tracks and playlists will be cleared.', - type: 'warning', - }; - - const result = await ipcRenderer.invoke( - channels.DIALOG_MESSAGE_BOX, - options, - ); - - if (result.response === 1) { - set({ refreshing: true }); - await db.reset(); - set({ refreshing: false }); - - router.revalidate(); - } - } catch (err) { - logger.error(err); - } - }, - - /** - * Update the play count attribute. - */ - incrementPlayCount: async (track: TrackModel): Promise => { - try { - await db.tracks.incrementPlayCount(track); - } catch (err) { - logger.warn(err); - } - }, - - /** - * Update the id3 attributes. - * IMPROVE ME: add support for writing metadata (hint: node-id3 does not work - * well). - * - * @param trackID The ID of the track to update - * @param newFields The fields to be updated and their new value - */ - updateTrackMetadata: async ( - trackID: string, - newFields: TrackEditableFields, - ): Promise => { - let track = await db.tracks.findOnlyByID(trackID); - - track = { - ...track, - ...newFields, - loweredMetas: getLoweredMeta(newFields), - }; - - if (!track) { - throw new Error('No track found while trying to update track metadata'); - } - - await db.tracks.update(track); - - router.revalidate(); - }, - - /** - * Set highlight trigger for a track - * FIXME: very hacky, and not great, should be done another way - */ - highlightPlayingTrack: (highlight: boolean): void => { - set({ highlightPlayingTrack: highlight }); - }, - }, - - // Old code used to manage folders to be scanned, to be re-enabled one day - // case (types.LIBRARY_ADD_FOLDERS): { // TODO Redux -> move to a thunk - // const { folders } = action.payload; - // let musicFolders = window.MuseeksAPI.config.get('musicFolders'); - - // // Check if we received folders - // if (folders !== undefined) { - // musicFolders = musicFolders.concat(folders); - - // // Remove duplicates, useless children, ect... - // musicFolders = utils.removeUselessFolders(musicFolders); - - // musicFolders.sort(); - - // config.set('musicFolders', musicFolders); - // } - - // return { ...state }; - // } - - // case (types.LIBRARY_REMOVE_FOLDER): { // TODO Redux -> move to a thunk - // if (!state.library.refreshing) { - // const musicFolders = window.MuseeksAPI.config.get('musicFolders'); - - // musicFolders.splice(action.index, 1); - - // config.set('musicFolders', musicFolders); - - // return { ...state }; - // } - - // return state; - // } -})); - -export default useLibraryStore; - -export function useLibraryAPI() { - return useLibraryStore((state) => state.api); -} diff --git a/src/renderer/tsconfig.json b/src/renderer/tsconfig.json deleted file mode 100644 index b81ef858e..000000000 --- a/src/renderer/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "plugins": [{ "name": "typescript-plugin-css-modules" }] - }, - "include": ["../renderer", "../shared"] -} diff --git a/src/renderer/views/Root.tsx b/src/renderer/views/Root.tsx deleted file mode 100644 index 61332171d..000000000 --- a/src/renderer/views/Root.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useEffect } from 'react'; -import { Outlet } from 'react-router-dom'; -import { useDrop } from 'react-dnd'; -import { NativeTypes } from 'react-dnd-html5-backend'; - -import logger from '../../shared/lib/logger'; -import Header from '../components/Header/Header'; -import Footer from '../components/Footer/Footer'; -import Toasts from '../components/Toasts/Toasts'; -import AppActions from '../stores/AppAPI'; -import DropzoneImport from '../components/DropzoneImport/DropzoneImport'; -import MediaSessionEvents from '../components/Events/MediaSessionEvents'; -import AppEvents from '../components/Events/AppEvents'; -import PlayerEvents from '../components/Events/PlayerEvents'; -import IPCPlayerEvents from '../components/Events/IPCPlayerEvents'; -import IPCNavigationEvents from '../components/Events/IPCNavigationEvents'; -import GlobalKeyBindings from '../components/Events/GlobalKeyBindings'; -import { useLibraryAPI } from '../stores/useLibraryStore'; - -import styles from './Root.module.css'; -import { LoaderData } from './router'; - -const { db } = window.MuseeksAPI; - -export default function ViewRoot() { - useEffect(() => { - AppActions.init(); - }, []); - - const libraryAPI = useLibraryAPI(); - - // Drop behavior to add tracks to the library from any string - const [{ isOver }, drop] = useDrop(() => { - return { - accept: [NativeTypes.FILE], - drop(item: { files: Array }) { - const files = item.files.map((file) => file.path); - - libraryAPI - .add(files) - .then((/* _importedTracks */) => { - // TODO: Import to playlist here - }) - .catch((err) => { - logger.warn(err); - }); - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - }), - }; - }); - - return ( -
- {/** Bunch of global event handlers */} - - - - - - - {/** The actual app */} -
-
- -
-
- - -
- ); -} - -export type RootLoaderData = LoaderData; - -ViewRoot.loader = async () => { - // this can be slow, think about caching it or something, especially when - // we revalidate routing - const tracks = await db.tracks.getAll(); - return { tracks }; -}; diff --git a/src/shared/lib/utils-id3.ts b/src/shared/lib/utils-id3.ts deleted file mode 100644 index b5dd869ca..000000000 --- a/src/shared/lib/utils-id3.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Track, TrackEditableFields } from '../types/museeks'; - -/** - * Strip accent from String. From https://jsperf.com/strip-accents - */ -export const stripAccents = (str: string): string => { - const accents = - 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž'; - const fixes = - 'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz'; - const split = accents.split('').join('|'); - const reg = new RegExp(`(${split})`, 'g'); - - function replacement(a: string) { - return fixes[accents.indexOf(a)] || ''; - } - - return str.replace(reg, replacement).toLowerCase(); -}; - -/** - * Take a track a returns its lowered metadata (used for search) - */ -export const getLoweredMeta = ( - metadata: TrackEditableFields, -): Track['loweredMetas'] => ({ - artist: metadata.artist.map((meta) => stripAccents(meta.toLowerCase())), - album: stripAccents(metadata.album.toLowerCase()), - title: stripAccents(metadata.title.toLowerCase()), - genre: metadata.genre.map((meta) => stripAccents(meta.toLowerCase())), -}); diff --git a/src/shared/lib/utils-uri.ts b/src/shared/lib/utils-uri.ts deleted file mode 100644 index d9c226768..000000000 --- a/src/shared/lib/utils-uri.ts +++ /dev/null @@ -1,18 +0,0 @@ -import os from 'os'; -import path from 'path'; - -/** - * Parse an URI, encoding some characters - */ -export const parseUri = (uri: string): string => { - // path and os should not be used - const root = os.platform() === 'win32' ? '' : path.parse(uri).root; - - const location = path - .resolve(uri) - .split(path.sep) - .map((d, i) => (i === 0 ? d : encodeURIComponent(d))) - .reduce((a, b) => path.join(a, b)); - - return `file://${root}${location}`; -}; diff --git a/src/shared/types/declarations/preload.d.ts b/src/shared/types/declarations/preload.d.ts deleted file mode 100644 index 6ccf3b62a..000000000 --- a/src/shared/types/declarations/preload.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// eslint-disable-next-line spaced-comment -/// - -import type { ElectronAPI, MuseeksAPI } from '../../../preload/entrypoint'; - -declare global { - interface Window { - MuseeksAPI: MuseeksAPI; - ElectronAPI: ElectronAPI; - } - - // Once context bridge is enabled: - // const MuseeksAPI: MuseeksAPI; -} - -export {}; diff --git a/src/shared/types/museeks.ts b/src/shared/types/museeks.ts deleted file mode 100644 index d790c842a..000000000 --- a/src/shared/types/museeks.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Player related stuff - */ -export enum PlayerStatus { - PLAY = 'play', - PAUSE = 'pause', - STOP = 'stop', -} - -export enum Repeat { - ALL = 'all', - ONE = 'one', - NONE = 'none', -} - -export enum SortBy { - ARTIST = 'artist', - ALBUM = 'album', - TITLE = 'title', - DURATION = 'duration', - GENRE = 'genre', -} - -export enum SortOrder { - ASC = 'asc', - DSC = 'dsc', -} - -/** - * App models - */ -export interface Track { - album: string; - artist: string[]; - disk: { - no: number; - of: number; - }; - duration: number; - genre: string[]; - loweredMetas: { - artist: string[]; - album: string; - title: string; - genre: string[]; - }; - path: string; - playCount: number; - title: string; - track: { - no: number; - of: number; - }; - year: number | null; -} - -export interface Playlist { - name: string; - tracks: string[]; - importPath?: string; // associated m3u file -} - -/** - * Database schemes - */ -export type TrackModel = PouchDB.Core.ExistingDocument< - Track & PouchDB.Core.AllDocsMeta ->; - -export type PlaylistModel = PouchDB.Core.ExistingDocument< - Playlist & PouchDB.Core.AllDocsMeta ->; - -/** - * Editable track fields (via right-click -> edit track) - */ -export type TrackEditableFields = Pick< - TrackModel, - 'title' | 'artist' | 'album' | 'genre' ->; - -/** - * Various - */ -export interface Toast { - id: string; - content: string; - type: ToastType; -} - -export type ToastType = 'success' | 'danger' | 'warning'; - -export interface LibrarySort { - by: SortBy; - order: SortOrder; -} - -/** - * Config - */ - -export interface ConfigBounds { - width: number; - height: number; - x: number; - y: number; -} - -// https://github.com/microsoft/TypeScript/issues/29729#issuecomment-460346421 -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnyThemeID = string & { whatever?: any }; -// TODO: how to automate this? Maybe losen types to "string" -type ThemeIDs = 'dark' | 'light' | AnyThemeID; - -export interface Config { - theme: ThemeIDs | '__system'; - audioVolume: number; - audioPlaybackRate: number; - audioOutputDevice: string; - audioMuted: boolean; - audioShuffle: boolean; - audioRepeat: Repeat; - tracksDensity: 'normal' | 'compact'; - defaultView: string; - librarySort: { - by: SortBy; - order: SortOrder; - }; - // musicFolders: string[], - sleepBlocker: boolean; - autoUpdateChecker: boolean; - displayNotifications: boolean; - bounds: ConfigBounds; -} - -/** - * Themes - */ - -export interface Theme { - _id: ThemeIDs; - name: string; - themeSource: Electron.NativeTheme['themeSource']; - variables: Record; -} - -/** - * Helpers - */ - -type StringableKey = T extends readonly unknown[] - ? number extends T['length'] - ? number - : `${number}` - : string | number; - -export type Path = T extends object - ? { - [P in keyof T & StringableKey]: `${P}` | `${P}.${Path}`; - }[keyof T & StringableKey] - : never; diff --git a/src/renderer/stores/PlaylistsAPI.ts b/src/stores/PlaylistsAPI.ts similarity index 57% rename from src/renderer/stores/PlaylistsAPI.ts rename to src/stores/PlaylistsAPI.ts index 74c7fbb5f..af1dd3353 100644 --- a/src/renderer/stores/PlaylistsAPI.ts +++ b/src/stores/PlaylistsAPI.ts @@ -1,28 +1,24 @@ -import { - Playlist, - TrackModel, - PlaylistModel, -} from '../../shared/types/museeks'; -import logger from '../../shared/lib/logger'; -import channels from '../../shared/lib/ipc-channels'; +import { Playlist, Track } from '../generated/typings'; +import logger from '../lib/logger'; +import channels from '../lib/ipc-channels'; import router from '../views/router'; +import library from '../lib/library'; +import { logAndNotifyError } from '../lib/utils'; +import { invalidate } from '../lib/query'; import useToastsStore from './useToastsStore'; import usePlayerStore from './usePlayerStore'; -const { db } = window.MuseeksAPI; -const { ipcRenderer } = window.ElectronAPI; - /** * Start playing playlist (on double click) */ const play = async (playlistID: string): Promise => { try { - const playlist: PlaylistModel = await db.playlists.findOnlyByID(playlistID); - const tracks: TrackModel[] = await db.tracks.findByID(playlist.tracks); + const playlist = await library.getPlaylist(playlistID); + const tracks = await library.getTracks(playlist.tracks); usePlayerStore.getState().api.start(tracks).catch(logger.warn); } catch (err) { - logger.warn(err); + logAndNotifyError(err); } }; @@ -31,35 +27,20 @@ const play = async (playlistID: string): Promise => { */ const create = async ( name: string, - tracks: string[] = [], - importPath: string | false = false, + trackIDs: string[] = [], redirect = false, -): Promise => { +) => { try { - const playlist: Playlist = { - name, - tracks, - }; - - if (importPath) playlist.importPath = importPath; - - const doc = await db.playlists.insert(playlist); - router.revalidate(); + const playlist = await library.createPlaylist(name, trackIDs); + invalidate(); - if (redirect) router.navigate(`/playlists/${doc.id}`); + if (redirect) router.navigate(`/playlists/${playlist._id}`); else useToastsStore .getState() .api.add('success', `The playlist "${name}" was created`); - - return doc.id; } catch (err) { - logger.error(err); - useToastsStore - .getState() - .api.add('danger', `The playlist coult not be created.`); - - return null; + logAndNotifyError(err); } }; @@ -68,10 +49,10 @@ const create = async ( */ const rename = async (playlistID: string, name: string): Promise => { try { - await db.playlists.rename(playlistID, name); - router.revalidate(); + await library.renamePlaylist(playlistID, name); + invalidate(); } catch (err) { - logger.warn(err); + logAndNotifyError(err); } }; @@ -80,11 +61,11 @@ const rename = async (playlistID: string, name: string): Promise => { */ const remove = async (playlistID: string): Promise => { try { - await db.playlists.remove(playlistID); + await library.deletePlaylist(playlistID); // FIX these when there is no more playlists - router.revalidate(); + invalidate(); } catch (err) { - logger.warn(err); + logAndNotifyError(err); } }; @@ -102,24 +83,16 @@ const addTracks = async ( const toastsAPI = useToastsStore.getState().api; try { - const playlist = await db.playlists.findOnlyByID(playlistID); + const playlist = await library.getPlaylist(playlistID); const playlistTracks = playlist.tracks.concat(tracksIDs); - await db.playlists.setTracks(playlistID, playlistTracks); - router.revalidate(); + await library.setPlaylistTracks(playlistID, playlistTracks); + invalidate(); toastsAPI.add( 'success', `${tracksIDs.length} tracks were successfully added to "${playlist.name}"`, ); } catch (err) { - logger.warn(err); - if (err instanceof Error) { - toastsAPI.add('danger', err.message); - } else { - toastsAPI.add( - 'danger', - 'An unknown error happened while trying to add tracks.', - ); - } + logAndNotifyError(err); } }; @@ -131,14 +104,14 @@ const removeTracks = async ( tracksIDs: string[], ): Promise => { try { - const playlist = await db.playlists.findOnlyByID(playlistID); + const playlist = await library.getPlaylist(playlistID); const playlistTracks = playlist.tracks.filter( (elem: string) => !tracksIDs.includes(elem), ); - await db.playlists.setTracks(playlistID, playlistTracks); - router.revalidate(); + await library.setPlaylistTracks(playlistID, playlistTracks); + invalidate(); } catch (err) { - logger.warn(err); + logAndNotifyError(err); } }; @@ -147,18 +120,11 @@ const removeTracks = async ( */ const duplicate = async (playlistID: string): Promise => { try { - const playlist = await db.playlists.findOnlyByID(playlistID); - const { tracks } = playlist; - - const newPlaylist: Playlist = { - name: `Copy of ${playlist.name}`, - tracks: tracks, - }; - - await db.playlists.insert(newPlaylist); - router.revalidate(); + const playlist = await library.getPlaylist(playlistID); + await library.createPlaylist(`Copy of ${playlist.name}`, playlist.tracks); + invalidate(); } catch (err) { - logger.warn(err); + logAndNotifyError(err); } }; @@ -176,7 +142,7 @@ const reorderTracks = async ( if (tracksIDs.includes(targetTrackID)) return; try { - const playlist: Playlist = await db.playlists.findOnlyByID(playlistID); + const playlist: Playlist = await library.getPlaylist(playlistID); const newTracks = playlist.tracks.filter((id) => !tracksIDs.includes(id)); let targetIndex = newTracks.indexOf(targetTrackID); @@ -193,10 +159,10 @@ const reorderTracks = async ( newTracks.splice(targetIndex + 1, 0, ...tracksIDs); - await db.playlists.setTracks(playlistID, newTracks); - router.revalidate(); + await library.setPlaylistTracks(playlistID, newTracks); + invalidate(); } catch (err) { - logger.warn(err); + logAndNotifyError(err); } }; @@ -205,8 +171,8 @@ const reorderTracks = async ( * TODO: investigate why the playlist path are relative, and not absolute */ const exportToM3u = async (playlistID: string): Promise => { - const playlist: PlaylistModel = await db.playlists.findOnlyByID(playlistID); - const tracks: TrackModel[] = await db.tracks.findByID(playlist.tracks); + const playlist: Playlist = await library.getPlaylist(playlistID); + const tracks: Track[] = await library.getTracks(playlist.tracks); ipcRenderer.send( channels.PLAYLIST_EXPORT, diff --git a/src/renderer/stores/SettingsAPI.ts b/src/stores/SettingsAPI.ts similarity index 62% rename from src/renderer/stores/SettingsAPI.ts rename to src/stores/SettingsAPI.ts index 6614fd2ae..02b2a8616 100644 --- a/src/renderer/stores/SettingsAPI.ts +++ b/src/stores/SettingsAPI.ts @@ -1,26 +1,21 @@ import * as semver from 'semver'; +import { getVersion } from '@tauri-apps/api/app'; +import { getCurrent } from '@tauri-apps/api/window'; +import { invoke } from '@tauri-apps/api/core'; -import channels from '../../shared/lib/ipc-channels'; -import { Config, Theme } from '../../shared/types/museeks'; -import logger from '../../shared/lib/logger'; +import { Config, DefaultView } from '../generated/typings'; +import { Theme } from '../types/museeks'; +import { themes } from '../lib/themes'; +import config from '../lib/config'; import useToastsStore from './useToastsStore'; -const { ipcRenderer } = window.ElectronAPI; -const { config } = window.MuseeksAPI; - interface UpdateCheckOptions { silentFail?: boolean; } -const getTheme = async (): Promise => { - const themeID = await ipcRenderer.invoke(channels.THEME_GET_ID); - - return themeID; -}; - const setTheme = async (themeID: string): Promise => { - await ipcRenderer.invoke(channels.THEME_SET_ID, themeID); + await config.set('theme', themeID); // TODO: own plugin? await checkTheme(); }; @@ -29,30 +24,41 @@ const setTheme = async (themeID: string): Promise => { */ const applyThemeToUI = async (theme: Theme): Promise => { // TODO think about variables validity? - const root = document.documentElement; + // TODO: update the window theme dynamically + const root = document.documentElement; Object.entries(theme.variables).forEach(([property, value]) => { root.style.setProperty(property, value); }); }; const checkTheme = async (): Promise => { - const theme: Theme = await ipcRenderer.invoke(channels.THEME_GET); + // TODO: Tauri offers no API to query the system system preference,getCurrent().theme() + // that is used when a window is created with no assigned theme. + // So we are bypassing the user choice for now. + // const themeID: string = await config.get("theme"); + const themeID = (await getCurrent().theme()) ?? 'light'; + const theme = themes[themeID]; + + if (theme == null) { + throw new Error(`Theme ${themeID} not found`); + } + applyThemeToUI(theme); }; const setTracksDensity = async ( - density: Config['tracksDensity'], + density: Config['track_view_density'], ): Promise => { - await window.MuseeksAPI.config.set('tracksDensity', density); + await config.set('track_view_density', density); }; /** * Check and enable sleep blocker if needed */ const checkSleepBlocker = async (): Promise => { - if (await config.get('sleepBlocker')) { - ipcRenderer.send('settings:toggleSleepBlocker', true); + if (await config.get('sleepblocker')) { + invoke('plugin:sleepblocker|enable'); } }; @@ -62,7 +68,13 @@ const checkSleepBlocker = async (): Promise => { const checkForUpdate = async ( options: UpdateCheckOptions = {}, ): Promise => { - const currentVersion = window.MuseeksAPI.version; + const shouldCheck = await config.get('auto_update_checker'); + + if (!shouldCheck) { + return; + } + + const currentVersion = await getVersion(); try { const response = await fetch( @@ -101,48 +113,49 @@ const checkForUpdate = async ( * Init all settings */ const check = async (): Promise => { - await checkTheme(); - checkSleepBlocker(); - if (await config.get('autoUpdateChecker')) { - checkForUpdate({ silentFail: true }).catch((err) => { - logger.error(err); - }); - } + await Promise.allSettled([ + checkTheme(), + checkSleepBlocker(), + checkForUpdate({ silentFail: true }), + ]); }; /** * Toggle sleep blocker */ const toggleSleepBlocker = async (value: boolean): Promise => { - await config.set('sleepBlocker', value); - - ipcRenderer.send('settings:toggleSleepBlocker', value); + if (value == true) { + invoke('plugin:sleepblocker|enable'); + } else { + invoke('plugin:sleepblocker|disable'); + } }; /** * Set the default view of the app */ -const setDefaultView = async (value: string): Promise => { - await config.set('defaultView', value); +const setDefaultView = async (defaultView: DefaultView): Promise => { + await invoke('plugin:default-view|set', { + defaultView, + }); }; /** * Toggle update check on startup */ const toggleAutoUpdateChecker = async (value: boolean): Promise => { - await config.set('autoUpdateChecker', value); + await config.set('auto_update_checker', value); }; /** * Toggle native notifications display */ const toggleDisplayNotifications = async (value: boolean): Promise => { - await config.set('displayNotifications', value); + await config.set('notifications', value); }; // Should we use something else to harmonize between zustand and non-store APIs? const SettingsAPI = { - getTheme, setTheme, applyThemeToUI, setTracksDensity, diff --git a/src/renderer/stores/store-helpers.ts b/src/stores/store-helpers.ts similarity index 100% rename from src/renderer/stores/store-helpers.ts rename to src/stores/store-helpers.ts diff --git a/src/stores/useLibraryStore.ts b/src/stores/useLibraryStore.ts new file mode 100644 index 000000000..6a65bcdc5 --- /dev/null +++ b/src/stores/useLibraryStore.ts @@ -0,0 +1,257 @@ +import { ask, open } from '@tauri-apps/plugin-dialog'; + +import { SortBy, SortOrder, Track } from '../generated/typings'; +import config from '../lib/config'; +import library from '../lib/library'; +import { logAndNotifyError } from '../lib/utils'; +import { invalidate } from '../lib/query'; + +import { createStore } from './store-helpers'; +import usePlayerStore from './usePlayerStore'; +import useToastsStore from './useToastsStore'; + +type LibraryState = { + search: string; + sortBy: SortBy; + sortOrder: SortOrder; + refreshing: boolean; + refresh: { + current: number; + total: number; + }; + highlightPlayingTrack: boolean; + api: { + search: (value: string) => void; + sort: (sortBy: SortBy) => void; + add: () => Promise; + remove: (tracksIDs: string[]) => Promise; + reset: () => Promise; + setRefresh: (processed: number, total: number) => void; + updateTrackMetadata: ( + trackID: string, + fields: Pick, + ) => Promise; + highlightPlayingTrack: (highlight: boolean) => void; + }; +}; + +const useLibraryStore = createStore((set, get) => ({ + search: '', + sortBy: config.getInitial('library_sort_by'), + sortOrder: config.getInitial('library_sort_order'), + refreshing: false, + refresh: { + current: 0, + total: 0, + }, + highlightPlayingTrack: false, // hacky, fixme + + api: { + /** + * Filter tracks by search + */ + search: (search): void => { + set({ search }); + }, + + /** + * Filter tracks by sort query + */ + sort: async (sortBy): Promise => { + const prevSortBy = get().sortBy; + const prevSortOrder = get().sortOrder; + + let sortOrder: SortOrder; + + // If same sort by, just reverse the order + if (sortBy === prevSortBy) { + sortOrder = prevSortOrder === 'Asc' ? 'Dsc' : 'Asc'; + } + + // If it's different, then we assume the user needs ASC order by default + else { + sortOrder = 'Asc'; + } + + await config.set('library_sort_by', sortBy); + await config.set('library_sort_order', sortOrder); + + set({ sortBy, sortOrder }); + }, + + /** + * Add tracks to Library + */ + add: async (): Promise => { + try { + const result = await open({ + multiple: true, + directory: true, + }); + + if (result == null) { + return; + } + + set({ refreshing: true }); + await library.importTracks(result); + // TODO: re-implement progress + invalidate(); + return; + } catch (err) { + logAndNotifyError(err); + } finally { + set({ + refreshing: false, + refresh: { current: 0, total: 0 }, + }); + } + }, + + setRefresh: (current: number, total: number) => { + set({ + refresh: { + current, + total, + }, + }); + }, + + /** + * remove tracks from library + */ + remove: async (tracksIDs) => { + const confirmed = await ask( + `Are you sure you want to remove ${tracksIDs.length} element(s) from your library?`, + { + title: 'Remove tracks from library?', + kind: 'warning', + cancelLabel: 'Cancel', + okLabel: 'Remove', + }, + ); + + if (confirmed) { + // button possition, here 'remove' + // Remove tracks from the Track collection + await library.removeTracks(tracksIDs); + + invalidate(); + // That would be great to remove those ids from all the playlists, but it's not easy + // and should not cause strange behaviors, all PR for that would be really appreciated + // TODO: see if it's possible to remove the IDs from the selected state of TracksList as it "could" lead to strange behaviors + } + }, + + /** + * Reset the library + */ + reset: async (): Promise => { + usePlayerStore.getState().api.stop(); + try { + const confirmed = await ask( + 'All your tracks and playlists will be deleted from Museeks.', + { + title: 'Reset library?', + kind: 'warning', + cancelLabel: 'Cancel', + okLabel: 'Reset', + }, + ); + + if (confirmed) { + await library.reset(); + useToastsStore.getState().api.add('success', 'Library was reset'); + invalidate(); + } + } catch (err) { + logAndNotifyError(err); + } + }, + + /** + * Update the id3 attributes. + * IMPROVE ME: add support for writing metadata to disk (and not only update + * the DB). + * + * @param trackID The ID of the track to update + * @param newFields The fields to be updated and their new value + */ + updateTrackMetadata: async ( + trackID: string, + newFields: Pick, + ): Promise => { + try { + let [track] = await library.getTracks([trackID]); + + if (!track) { + throw new Error( + 'No track found while trying to update track metadata', + ); + } + + track = { + ...track, + ...newFields, + }; + + await library.updateTrack(track); + + invalidate(); + } catch (err) { + logAndNotifyError( + err, + 'Something wrong happened when updating the track', + ); + } + }, + + /** + * Set highlight trigger for a track + * FIXME: very hacky, and not great, should be done another way + */ + highlightPlayingTrack: (highlight: boolean): void => { + set({ highlightPlayingTrack: highlight }); + }, + }, + + // Old code used to manage folders to be scanned, to be re-enabled one day + // case (types.LIBRARY_ADD_FOLDERS): { // TODO Redux -> move to a thunk + // const { folders } = action.payload; + // let musicFolders = window.MuseeksAPI.config.get('musicFolders'); + + // // Check if we received folders + // if (folders !== undefined) { + // musicFolders = musicFolders.concat(folders); + + // // Remove duplicates, useless children, ect... + // musicFolders = utils.removeUselessFolders(musicFolders); + + // musicFolders.sort(); + + // config.set('musicFolders', musicFolders); + // } + + // return { ...state }; + // } + + // case (types.LIBRARY_REMOVE_FOLDER): { // TODO Redux -> move to a thunk + // if (!state.library.refreshing) { + // const musicFolders = window.MuseeksAPI.config.get('musicFolders'); + + // musicFolders.splice(action.index, 1); + + // config.set('musicFolders', musicFolders); + + // return { ...state }; + // } + + // return state; + // } +})); + +export default useLibraryStore; + +export function useLibraryAPI() { + return useLibraryStore((state) => state.api); +} diff --git a/src/renderer/stores/usePlayerStore.ts b/src/stores/usePlayerStore.ts similarity index 84% rename from src/renderer/stores/usePlayerStore.ts rename to src/stores/usePlayerStore.ts index b965515b8..0762efd71 100644 --- a/src/renderer/stores/usePlayerStore.ts +++ b/src/stores/usePlayerStore.ts @@ -2,26 +2,29 @@ import debounce from 'lodash/debounce'; import { StateCreator } from 'zustand'; import { persist } from 'zustand/middleware'; -import { PlayerStatus, Repeat, TrackModel } from '../../shared/types/museeks'; +import { PlayerStatus } from '../types/museeks'; import { shuffleTracks } from '../lib/utils-player'; -import logger from '../../shared/lib/logger'; +import logger from '../lib/logger'; import router from '../views/router'; import player from '../lib/player'; +import { Track, Repeat } from '../generated/typings'; +import config from '../lib/config'; +import library from '../lib/library'; +import { logAndNotifyError } from '../lib/utils'; import { createStore } from './store-helpers'; -import useToastsStore from './useToastsStore'; import useLibraryStore from './useLibraryStore'; type PlayerState = { - queue: TrackModel[]; - oldQueue: TrackModel[]; + queue: Track[]; + oldQueue: Track[]; queueCursor: number | null; queueOrigin: null | string; repeat: Repeat; shuffle: boolean; playerStatus: PlayerStatus; api: { - start: (queue: TrackModel[], _id?: string) => Promise; + start: (queue: Track[], id?: string) => Promise; play: () => Promise; pause: () => void; playPause: () => Promise; @@ -41,19 +44,17 @@ type PlayerState = { removeFromQueue: (index: number) => void; addInQueue: (tracksIDs: string[]) => Promise; addNextInQueue: (tracksIDs: string[]) => Promise; - setQueue: (tracks: TrackModel[]) => void; + setQueue: (tracks: Track[]) => void; }; }; -const { config } = window.MuseeksAPI; - const usePlayerStore = createPlayerStore((set, get) => ({ queue: [], // Tracks to be played oldQueue: [], // Queue backup (in case of shuffle) queueCursor: null, // The cursor of the queue queueOrigin: null, // URL of the queue when it was started - repeat: config.__initialConfig['audioRepeat'], // the current repeat state (one, all, none) - shuffle: config.__initialConfig['audioShuffle'], // If shuffle mode is enabled + repeat: config.getInitial('audio_repeat'), // the current repeat state (one, all, none) + shuffle: config.getInitial('audio_shuffle'), // If shuffle mode is enabled playerStatus: PlayerStatus.STOP, // Player status api: { @@ -62,6 +63,27 @@ const usePlayerStore = createPlayerStore((set, get) => ({ * TODO: this function ~could probably~ needs to be refactored ~a bit~ */ start: async (queue, _id): Promise => { + // TODO: implement start with no queue + // // If no queue is provided, we create it based on the screen the user is on + // if (!queue) { + // if (location.hash.startsWith('#/playlists')) { + // queue = library.tracks.playlist; + // queue = []; + // } else { + // // we are either on the library or the settings view + // // so let's play the whole library + // // Because the tracks in the store are not ordered, let's filter + // // and sort everything + // const { sort, search } = library; + // queue = library.tracks; + + // queue = sortTracks( + // filterTracks(newQueue, search), + // SORT_ORDERS[sort.by][sort.order], + // ); + // } + // } + if (queue.length === 0) return; const state = get(); @@ -90,7 +112,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ const track = newQueue[queuePosition]; player.setTrack(track); - await player.play(); + await player.play().catch(logAndNotifyError); let queueCursor = queuePosition; // Clean that variable mess later @@ -121,6 +143,8 @@ const usePlayerStore = createPlayerStore((set, get) => ({ * Play/resume audio */ play: async () => { + // TODO: if there is no queue / no audio set, get the data of the current view + // and start a queue with it await player.play(); set({ playerStatus: PlayerStatus.PLAY }); @@ -175,9 +199,9 @@ const usePlayerStore = createPlayerStore((set, get) => ({ let newQueueCursor; if (queueCursor !== null) { - if (repeat === Repeat.ONE) { + if (repeat === 'One') { newQueueCursor = queueCursor; - } else if (repeat === Repeat.ALL && queueCursor === queue.length - 1) { + } else if (repeat === 'All' && queueCursor === queue.length - 1) { // is last track newQueueCursor = 0; // start with new track } else { @@ -239,7 +263,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ toggleShuffle: async (shuffle) => { shuffle = shuffle ?? !get().shuffle; - await config.set('audioShuffle', shuffle); + await config.set('audio_shuffle', shuffle); const { queue, queueCursor, oldQueue } = get(); @@ -280,19 +304,19 @@ const usePlayerStore = createPlayerStore((set, get) => ({ // Get to the next repeat type if none is specified if (repeat == undefined) { switch (get().repeat) { - case Repeat.NONE: - repeat = Repeat.ALL; + case 'None': + repeat = 'All'; break; - case Repeat.ALL: - repeat = Repeat.ONE; + case 'All': + repeat = 'One'; break; - case Repeat.ONE: - repeat = Repeat.NONE; + case 'One': + repeat = 'None'; break; } } - await config.set('audioRepeat', repeat); + await config.set('audio_repeat', repeat); set({ repeat }); }, @@ -311,7 +335,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ if (muted) player.mute(); else player.unmute(); - await config.set('audioMuted', muted); + await config.set('audio_muted', muted); }, /** @@ -322,7 +346,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ // if in allowed range player.setPlaybackRate(value); - await config.set('audioPlaybackRate', value); + await config.set('audio_playback_rate', value); } }, @@ -333,15 +357,9 @@ const usePlayerStore = createPlayerStore((set, get) => ({ if (deviceID) { try { await player.setOutputDevice(deviceID); - await config.set('audioOutputDevice', deviceID); + await config.set('audio_output_device', deviceID); } catch (err) { - logger.warn(err); - useToastsStore - .getState() - .api.add( - 'danger', - 'An error occured when trying to switch to the new output device', - ); + logAndNotifyError(err); } } }, @@ -419,7 +437,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ */ addInQueue: async (tracksIDs) => { const { queue, queueCursor } = get(); - const tracks = await window.MuseeksAPI.db.tracks.findByID(tracksIDs); + const tracks = await library.getTracks(tracksIDs); const newQueue = [...queue, ...tracks]; set({ @@ -433,7 +451,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ * Add tracks at the beginning of the queue */ addNextInQueue: async (tracksIDs) => { - const tracks = await window.MuseeksAPI.db.tracks.findByID(tracksIDs); + const tracks = await library.getTracks(tracksIDs); const { queueCursor } = get(); const queue = [...get().queue]; @@ -454,7 +472,7 @@ const usePlayerStore = createPlayerStore((set, get) => ({ /** * Set the queue */ - setQueue: (tracks: TrackModel[]) => { + setQueue: (tracks: Track[]) => { set({ queue: tracks, }); @@ -508,8 +526,6 @@ function createPlayerStore(store: StateCreator) { ...(persistedState as Partial), // API should never be persisted api: currentState.api, - // Instantiated should never be true - instantiated: false, // If player status was playing, set it to pause, as it makes no sense // to auto-start playing a song when Museeks starts playerStatus: @@ -526,5 +542,5 @@ function createPlayerStore(store: StateCreator) { * Make sure we don't save audio volume to the file system too often */ const saveVolume = debounce(async (volume: number) => { - await config.set('audioVolume', volume); + await config.set('audio_volume', volume); }, 500); diff --git a/src/renderer/stores/useToastsStore.ts b/src/stores/useToastsStore.ts similarity index 81% rename from src/renderer/stores/useToastsStore.ts rename to src/stores/useToastsStore.ts index 42ff9d4a3..8ef326347 100644 --- a/src/renderer/stores/useToastsStore.ts +++ b/src/stores/useToastsStore.ts @@ -1,6 +1,6 @@ import { nanoid } from 'nanoid'; -import { Toast, ToastType } from '../../shared/types/museeks'; +import { Toast, ToastType } from '../types/museeks'; import { createStore } from './store-helpers'; @@ -18,7 +18,7 @@ const useToastsStore = createStore((set, get) => ({ api: { add: (type, content, duration = 3000) => { const id = nanoid(); - const toast: Toast = { id, type, content }; + const toast: Toast = { _id: id, type, content }; set((state) => ({ toasts: [...state.toasts, toast] })); @@ -29,7 +29,7 @@ const useToastsStore = createStore((set, get) => ({ remove: (id) => { set((state) => ({ - toasts: [...state.toasts].filter((t) => t.id !== id), + toasts: [...state.toasts].filter((t) => t._id !== id), })); }, }, diff --git a/src/renderer/styles/general.module.css b/src/styles/general.module.css similarity index 68% rename from src/renderer/styles/general.module.css rename to src/styles/general.module.css index 5745f882e..309357658 100644 --- a/src/renderer/styles/general.module.css +++ b/src/styles/general.module.css @@ -1,6 +1,8 @@ /* Colors (default theme) */ :root { --main-color: #459ce7; + --main-color-darker: #3a73a4; + --main-color-lighter: #63aff0; --link-color: #459ce7; --link-color-hover: #52afff; --bold: 600; @@ -41,8 +43,7 @@ a { &:hover, &:focus, - &:active, - &:visited { + &:active { color: var(--link-color-hover); } } @@ -63,3 +64,19 @@ strong { :global #wrap { height: 100vh; } + +/** + * Generic class that can be used on call-to-actions to show the interactibility + * of an element + */ +:global [data-museeks-action] { + transition: transform 0.1s ease-in-out; +} + +:global [data-museeks-action]:hover { + transform: scale(1.2); +} + +:global [data-museeks-action]:active { + transform: scale(1); +} diff --git a/src/renderer/styles/main.module.css b/src/styles/main.module.css similarity index 100% rename from src/renderer/styles/main.module.css rename to src/styles/main.module.css diff --git a/src/renderer/styles/utils.module.css b/src/styles/utils.module.css similarity index 100% rename from src/renderer/styles/utils.module.css rename to src/styles/utils.module.css diff --git a/src/shared/themes/dark.json b/src/themes/dark.json similarity index 100% rename from src/shared/themes/dark.json rename to src/themes/dark.json diff --git a/src/shared/themes/light.json b/src/themes/light.json similarity index 100% rename from src/shared/themes/light.json rename to src/themes/light.json diff --git a/src/shared/types/declarations/images.d.ts b/src/types/declarations/images.d.ts similarity index 100% rename from src/shared/types/declarations/images.d.ts rename to src/types/declarations/images.d.ts diff --git a/src/shared/types/declarations/level-js.d.ts b/src/types/declarations/level-js.d.ts similarity index 100% rename from src/shared/types/declarations/level-js.d.ts rename to src/types/declarations/level-js.d.ts diff --git a/src/shared/types/declarations/linvodb3.d.ts b/src/types/declarations/linvodb3.d.ts similarity index 100% rename from src/shared/types/declarations/linvodb3.d.ts rename to src/types/declarations/linvodb3.d.ts diff --git a/src/shared/types/declarations/react-fontawesome.d.ts b/src/types/declarations/react-fontawesome.d.ts similarity index 100% rename from src/shared/types/declarations/react-fontawesome.d.ts rename to src/types/declarations/react-fontawesome.d.ts diff --git a/src/shared/types/declarations/react-onclickout.d.ts b/src/types/declarations/react-onclickout.d.ts similarity index 100% rename from src/shared/types/declarations/react-onclickout.d.ts rename to src/types/declarations/react-onclickout.d.ts diff --git a/src/shared/types/declarations/react-simple-input.d.ts b/src/types/declarations/react-simple-input.d.ts similarity index 100% rename from src/shared/types/declarations/react-simple-input.d.ts rename to src/types/declarations/react-simple-input.d.ts diff --git a/src/shared/types/declarations/svg-inline-react.d.ts b/src/types/declarations/svg-inline-react.d.ts similarity index 100% rename from src/shared/types/declarations/svg-inline-react.d.ts rename to src/types/declarations/svg-inline-react.d.ts diff --git a/src/types/declarations/window.d.ts b/src/types/declarations/window.d.ts new file mode 100644 index 000000000..70166aa2d --- /dev/null +++ b/src/types/declarations/window.d.ts @@ -0,0 +1,20 @@ +// eslint-disable-next-line spaced-comment +/// + +import { OsType } from '@tauri-apps/plugin-os'; + +import { Config } from '../../generated/typings'; + +declare global { + /** + * Tauri's APIs to inspect platform-specific information is asynchronous, + * which is very inconvenient for small-synchronous utils, so we hack our way + * around by polluting the global namespace + */ + interface Window { + __MUSEEKS_PLATFORM: OsType; + __MUSEEKS_INITIAL_CONFIG: Config; + } +} + +export {}; diff --git a/src/types/museeks.ts b/src/types/museeks.ts new file mode 100644 index 000000000..ca274864b --- /dev/null +++ b/src/types/museeks.ts @@ -0,0 +1,61 @@ +import { Track } from '../generated/typings'; + +/** + * Player related stuff + */ +export enum PlayerStatus { + PLAY = 'play', + PAUSE = 'pause', + STOP = 'stop', +} + +/** + * Editable track fields (via right-click -> edit track) + */ +export type TrackEditableFields = Pick< + Track, + 'title' | 'artists' | 'album' | 'genres' +>; + +export type TrackSearchableFields = Pick< + Track, + 'title' | 'artists' | 'album' | 'genres' +>; + +/** + * Various + */ +export interface Toast { + _id: string; + content: string; + type: ToastType; +} + +export type ToastType = 'success' | 'danger' | 'warning'; + +/** + * Themes + */ + +export interface Theme { + _id: string; + name: string; + themeSource: 'light' | 'dark'; + variables: Record; +} + +/** + * Helpers + */ + +type StringableKey = T extends readonly unknown[] + ? number extends T['length'] + ? number + : `${number}` + : string | number; + +export type Path = T extends object + ? { + [P in keyof T & StringableKey]: `${P}` | `${P}.${Path}`; + }[keyof T & StringableKey] + : never; diff --git a/src/renderer/views/Root.module.css b/src/views/Root.module.css similarity index 100% rename from src/renderer/views/Root.module.css rename to src/views/Root.module.css diff --git a/src/views/Root.tsx b/src/views/Root.tsx new file mode 100644 index 000000000..2a90ab3b5 --- /dev/null +++ b/src/views/Root.tsx @@ -0,0 +1,61 @@ +// import { useEffect } from "react"; +import { Outlet } from 'react-router-dom'; +import { getCurrent } from '@tauri-apps/api/window'; +import { Suspense, useEffect } from 'react'; + +import Header from '../components/Header/Header'; +import Footer from '../components/Footer/Footer'; +import Toasts from '../components/Toasts/Toasts'; +import DropzoneImport from '../components/DropzoneImport/DropzoneImport'; +import MediaSessionEvents from '../components/Events/MediaSessionEvents'; +import AppEvents from '../components/Events/AppEvents'; +import PlayerEvents from '../components/Events/PlayerEvents'; +// import IPCPlayerEvents from "../components/Events/IPCPlayerEvents"; +// import IPCNavigationEvents from "../components/Events/IPCNavigationEvents"; +import GlobalKeyBindings from '../components/Events/GlobalKeyBindings'; +// import { useLibraryAPI } from "../stores/useLibraryStore"; +import SettingsAPI from '../stores/SettingsAPI'; +import IPCNavigationEvents from '../components/Events/IPCNavigationEvents'; +import LibraryEvents from '../components/Events/LibraryEvents'; + +import styles from './Root.module.css'; +import { LoaderData } from './router'; + +export default function ViewRoot() { + useEffect(() => { + SettingsAPI.check() + // Show the app once everything is loaded + .then(() => getCurrent()) + .then((window) => { + window.show(); + }); + }, []); + + return ( +
+ {/** Bunch of global event handlers */} + {/** TODO: */} + + {/* */} + + + + + + {/** The actual app */} +
+
+ +
+
+ + + + +
+ ); +} + +export type RootLoaderData = LoaderData; + +ViewRoot.loader = async () => null; diff --git a/src/renderer/views/ViewLibrary.module.css b/src/views/ViewLibrary.module.css similarity index 100% rename from src/renderer/views/ViewLibrary.module.css rename to src/views/ViewLibrary.module.css diff --git a/src/renderer/views/ViewLibrary.tsx b/src/views/ViewLibrary.tsx similarity index 60% rename from src/renderer/views/ViewLibrary.tsx rename to src/views/ViewLibrary.tsx index c117a90f7..e36945ebd 100644 --- a/src/renderer/views/ViewLibrary.tsx +++ b/src/views/ViewLibrary.tsx @@ -1,43 +1,63 @@ import { useMemo } from 'react'; -import { Link, useLoaderData, useRouteLoaderData } from 'react-router-dom'; +import { Link, useLoaderData } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; import * as ViewMessage from '../elements/ViewMessage/ViewMessage'; import TracksList from '../components/TracksList/TracksList'; import useLibraryStore from '../stores/useLibraryStore'; import usePlayingTrackID from '../hooks/usePlayingTrackID'; import useFilteredTracks from '../hooks/useFilteredTracks'; +import config from '../lib/config'; +import library from '../lib/library'; -import { RootLoaderData } from './Root'; import { LoaderData } from './router'; import appStyles from './Root.module.css'; import styles from './ViewLibrary.module.css'; -const { db, config } = window.MuseeksAPI; - export default function ViewLibrary() { const trackPlayingID = usePlayingTrackID(); const refreshing = useLibraryStore((state) => state.refreshing); const search = useLibraryStore((state) => state.search); const { playlists, tracksDensity } = useLoaderData() as LibraryLoaderData; - const { tracks } = useRouteLoaderData('root') as RootLoaderData; - const filteredTracks = useFilteredTracks(tracks); + + // Some queries when switching routes can be expensive-ish (like getting all tracks), + // while at the same time, the data will most of the time never change. + // Using stale-while-revalidate libraries help us (fake-)loading this page faster + const { data: tracks, isLoading } = useQuery({ + queryKey: ['tracks'], + queryFn: library.getAllTracks, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }); + + const filteredTracks = useFilteredTracks(tracks ?? []); const getLibraryComponent = useMemo(() => { + // Refreshing library + if (isLoading) { + return ( + +

Loading library...

+
+ ); + } + + // Refreshing library + if (refreshing) { + return ( + +

Your library is being scanned =)

+ hold on... +
+ ); + } + // Empty library if (filteredTracks.length === 0 && search === '') { - if (refreshing) { - return ( - -

Your library is being scanned =)

- hold on... -
- ); - } - return ( -

Too bad, there is no music in your library =(

+

There is no music in your library :(

you can always just drop files and folders anywhere or{' '} @@ -74,6 +94,7 @@ export default function ViewLibrary() { playlists, trackPlayingID, tracksDensity, + isLoading, ]); return ( @@ -87,7 +108,9 @@ export type LibraryLoaderData = LoaderData; ViewLibrary.loader = async () => { return { - playlists: await db.playlists.getAll(), - tracksDensity: await config.get('tracksDensity'), + playlists: await library.getAllPlaylists(), + tracksDensity: (await config.get('track_view_density')) as + | 'compact' + | 'normal', }; }; diff --git a/src/renderer/views/ViewPlaylistDetails.tsx b/src/views/ViewPlaylistDetails.tsx similarity index 83% rename from src/renderer/views/ViewPlaylistDetails.tsx rename to src/views/ViewPlaylistDetails.tsx index e12b15252..01d14f4a3 100644 --- a/src/renderer/views/ViewPlaylistDetails.tsx +++ b/src/views/ViewPlaylistDetails.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react'; import { Link, LoaderFunctionArgs, + redirect, useLoaderData, useParams, } from 'react-router-dom'; @@ -12,11 +13,11 @@ import PlaylistsAPI from '../stores/PlaylistsAPI'; import { filterTracks } from '../lib/utils-library'; import useLibraryStore from '../stores/useLibraryStore'; import usePlayingTrackID from '../hooks/usePlayingTrackID'; +import config from '../lib/config'; +import library from '../lib/library'; import { LoaderData } from './router'; -const { db, config } = window.MuseeksAPI; - export default function ViewPlaylistDetails() { const { playlists, playlistTracks, tracksDensity } = useLoaderData() as PlaylistLoaderData; @@ -104,12 +105,19 @@ ViewPlaylistDetails.loader = async ({ params }: LoaderFunctionArgs) => { throw new Error('Playlist ID is not defined'); } - const playlist = await db.playlists.findOnlyByID(params.playlistID); + try { + const playlist = await library.getPlaylist(params.playlistID); + return { + playlists: await library.getAllPlaylists(), + playlistTracks: await library.getTracks(playlist.tracks), + tracksDensity: await config.get('track_view_density'), + }; + } catch (err) { + // https://github.com/tauri-apps/tauri/issues/691 + if (err === '"Playlist not found"') { + return redirect('/playlists'); + } - return { - // TODO: can we re-use parent's data? - playlists: await db.playlists.getAll(), - playlistTracks: await db.tracks.findByID(playlist.tracks), - tracksDensity: await config.get('tracksDensity'), - }; + throw err; + } }; diff --git a/src/renderer/views/ViewPlaylists.module.css b/src/views/ViewPlaylists.module.css similarity index 100% rename from src/renderer/views/ViewPlaylists.module.css rename to src/views/ViewPlaylists.module.css diff --git a/src/renderer/views/ViewPlaylists.tsx b/src/views/ViewPlaylists.tsx similarity index 92% rename from src/renderer/views/ViewPlaylists.tsx rename to src/views/ViewPlaylists.tsx index c728f04d5..4074fd0a4 100644 --- a/src/renderer/views/ViewPlaylists.tsx +++ b/src/views/ViewPlaylists.tsx @@ -9,18 +9,17 @@ import { import PlaylistsNav from '../components/PlaylistsNav/PlaylistsNav'; import * as ViewMessage from '../elements/ViewMessage/ViewMessage'; import PlaylistsAPI from '../stores/PlaylistsAPI'; +import library from '../lib/library'; import { LoaderData } from './router'; import appStyles from './Root.module.css'; import styles from './ViewPlaylists.module.css'; -const { db } = window.MuseeksAPI; - export default function ViewPlaylists() { const { playlists } = useLoaderData() as PlaylistsLoaderData; const createPlaylist = useCallback(async () => { - await PlaylistsAPI.create('New playlist', [], false, true); + await PlaylistsAPI.create('New playlist', [], false); }, []); let playlistContent; @@ -51,7 +50,7 @@ export default function ViewPlaylists() { export type PlaylistsLoaderData = LoaderData; ViewPlaylists.loader = async ({ params }: LoaderFunctionArgs) => { - const playlists = await db.playlists.getAll(); + const playlists = await library.getAllPlaylists(); const [firstPlaylist] = playlists; const { playlistID } = params; diff --git a/src/renderer/views/ViewSettings.module.css b/src/views/ViewSettings.module.css similarity index 90% rename from src/renderer/views/ViewSettings.module.css rename to src/views/ViewSettings.module.css index 40889b59d..d42a6c714 100644 --- a/src/renderer/views/ViewSettings.module.css +++ b/src/views/ViewSettings.module.css @@ -2,6 +2,7 @@ display: flex; justify-content: center; padding-top: 50px; + scrollbar-gutter: stable; } .settings__nav { diff --git a/src/renderer/views/ViewSettings.tsx b/src/views/ViewSettings.tsx similarity index 82% rename from src/renderer/views/ViewSettings.tsx rename to src/views/ViewSettings.tsx index 06e6a326c..d444b9c30 100644 --- a/src/renderer/views/ViewSettings.tsx +++ b/src/views/ViewSettings.tsx @@ -1,6 +1,8 @@ import { Outlet, useMatch, Navigate } from 'react-router-dom'; +import { getTauriVersion, getVersion } from '@tauri-apps/api/app'; import * as Nav from '../elements/Nav/Nav'; +import config from '../lib/config'; import { LoaderData } from './router'; import appStyles from './Root.module.css'; @@ -32,9 +34,9 @@ export default function ViewSettingsView() { export type SettingsLoaderData = LoaderData; ViewSettingsView.loader = async () => { - const config = await window.MuseeksAPI.config.getAll(); - return { - config, + config: await config.getAll(), + version: await getVersion(), + tauriVersion: await getTauriVersion(), }; }; diff --git a/src/renderer/views/ViewSettingsAbout.tsx b/src/views/ViewSettingsAbout.tsx similarity index 69% rename from src/renderer/views/ViewSettingsAbout.tsx rename to src/views/ViewSettingsAbout.tsx index 6a6771baa..c66feab59 100644 --- a/src/renderer/views/ViewSettingsAbout.tsx +++ b/src/views/ViewSettingsAbout.tsx @@ -1,11 +1,15 @@ +import { useLoaderData } from 'react-router-dom'; + import ExternalLink from '../elements/ExternalLink/ExternalLink'; import Heart from '../elements/Heart/Heart'; import * as Setting from '../components/Setting/Setting'; import SettingsAPI from '../stores/SettingsAPI'; import Button from '../elements/Button/Button'; +import { SettingsLoaderData } from './ViewSettings'; + export default function ViewSettingsAbout() { - const version = window.MuseeksAPI.version; + const { version, tauriVersion } = useLoaderData() as SettingsLoaderData; return (
@@ -20,7 +24,7 @@ export default function ViewSettingsAbout() { href={`https://github.com/martpie/museeks/releases/tag/${version}`} > release notes - + {' '}

+

Tauri {tauriVersion}

Contributors

@@ -54,26 +59,6 @@ export default function ViewSettingsAbout() { .

- -

Support me

-

- Maintaining Museeks includes some costs. All the work is done on - contributors' free time, but I still have recurring costs like - domain names and developer certificates. -

-

- If you appreciate my work, and if you can afford it, you can for - example show support by{' '} - - sponsoring me - {' '} - (or just{' '} - - buying me a beer - - ) on GitHub (🙌). -

-
); } diff --git a/src/renderer/views/ViewSettingsAudio.tsx b/src/views/ViewSettingsAudio.tsx similarity index 93% rename from src/renderer/views/ViewSettingsAudio.tsx rename to src/views/ViewSettingsAudio.tsx index e9da1c536..2ee877c2f 100644 --- a/src/renderer/views/ViewSettingsAudio.tsx +++ b/src/views/ViewSettingsAudio.tsx @@ -26,7 +26,7 @@ export default function ViewSettingsAudio() { diff --git a/src/renderer/views/ViewSettingsLibrary.tsx b/src/views/ViewSettingsLibrary.tsx similarity index 60% rename from src/renderer/views/ViewSettingsLibrary.tsx rename to src/views/ViewSettingsLibrary.tsx index 6bf247e18..7ed423aae 100644 --- a/src/renderer/views/ViewSettingsLibrary.tsx +++ b/src/views/ViewSettingsLibrary.tsx @@ -1,31 +1,11 @@ -import { useCallback } from 'react'; - import * as Setting from '../components/Setting/Setting'; import Button from '../elements/Button/Button'; -import channels from '../../shared/lib/ipc-channels'; -import logger from '../../shared/lib/logger'; import useLibraryStore, { useLibraryAPI } from '../stores/useLibraryStore'; -const { ipcRenderer } = window.ElectronAPI; - export default function ViewSettingsLibrary() { const libraryAPI = useLibraryAPI(); const isLibraryRefreshing = useLibraryStore((state) => state.refreshing); - const openFolderSelector = useCallback(async () => { - const options: Electron.OpenDialogOptions = { - properties: ['multiSelections', 'openDirectory', 'openFile'], - }; - - const result = await ipcRenderer.invoke(channels.DIALOG_OPEN, options); - - if (result.filePaths) { - libraryAPI.add(result.filePaths).catch((err) => { - logger.warn(err); - }); - } - }, [libraryAPI]); - return (
@@ -34,7 +14,7 @@ export default function ViewSettingsLibrary() { This will also scan for .m3u files and create corresponding playlists. - diff --git a/src/renderer/views/ViewSettingsUI.tsx b/src/views/ViewSettingsUI.tsx similarity index 80% rename from src/renderer/views/ViewSettingsUI.tsx rename to src/views/ViewSettingsUI.tsx index f9b3ec923..3d669b7cd 100644 --- a/src/renderer/views/ViewSettingsUI.tsx +++ b/src/views/ViewSettingsUI.tsx @@ -4,8 +4,8 @@ import { useLoaderData } from 'react-router-dom'; import SettingsAPI from '../stores/SettingsAPI'; import * as Setting from '../components/Setting/Setting'; import CheckboxSetting from '../components/SettingCheckbox/SettingCheckbox'; -import { themes } from '../../shared/lib/themes'; -import { Config } from '../../shared/types/museeks'; +import { themes } from '../lib/themes'; +import { Config, DefaultView } from '../generated/typings'; import { SettingsLoaderData } from './ViewSettings'; @@ -23,14 +23,14 @@ export default function ViewSettingsUI() { ChangeEventHandler >((e) => { SettingsAPI.setTracksDensity( - e.currentTarget.value as Config['tracksDensity'], + e.currentTarget.value as Config['track_view_density'], ); }, []); const onDefaultViewChange = useCallback< ChangeEventHandler >((e) => { - SettingsAPI.setDefaultView(e.currentTarget.value); + SettingsAPI.setDefaultView(e.currentTarget.value as DefaultView); }, []); return ( @@ -41,9 +41,10 @@ export default function ViewSettingsUI() { defaultValue={config.theme} onChange={onThemeChange} id="setting-theme" + disabled // Issue in Tauri where we cannot easily detect system-wide preferences > - {themes.map((theme) => { + {Object.values(themes).map((theme) => { return (