From 44ef72b646b38c720ef2aa67983ea14fc1f2a424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Thu, 19 Dec 2024 19:50:57 +0100 Subject: [PATCH 01/11] chore: extend monorepo structure, better docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fernando Fernández --- frontend/eslint.config.ts | 3 ++- frontend/package.json | 2 ++ frontend/tsconfig.json | 15 ++++++++----- frontend/uno.config.ts | 2 +- frontend/vite.config.ts | 2 +- package-lock.json | 22 +++++++++++++++++++ packages/configs/README.md | 3 +++ packages/configs/eslint.config.ts | 5 +++-- packages/configs/package.json | 7 +++--- packages/configs/{ => src}/lint/index.ts | 0 packages/configs/{ => src}/lint/rules/base.ts | 4 ++-- packages/configs/{ => src}/lint/rules/env.ts | 0 packages/configs/{ => src}/lint/rules/i18n.ts | 0 packages/configs/{ => src}/lint/rules/json.ts | 0 .../{ => src}/lint/rules/typescript-vue.ts | 0 .../configs/{ => src}/lint/rules/unocss.ts | 0 packages/configs/{ => src}/lint/shared.ts | 0 .../base.json => src/typescript.json} | 3 +-- packages/configs/{unocss.ts => src/uno.ts} | 0 packages/configs/tsconfig.json | 16 +++++++++++--- packages/shared/README.md | 2 ++ packages/shared/eslint.config.ts | 8 +++++++ packages/shared/package.json | 17 ++++++++++++++ packages/shared/tsconfig.json | 17 ++++++++++++++ packages/ui-toolkit/README.md | 16 ++++++++++++++ packages/ui-toolkit/eslint.config.ts | 8 +++++++ packages/ui-toolkit/package.json | 14 ++++++++++++ packages/ui-toolkit/tsconfig.json | 17 ++++++++++++++ packages/ui-toolkit/uno.config.ts | 1 + packages/vite-plugins/README.md | 2 ++ packages/vite-plugins/eslint.config.ts | 3 ++- packages/vite-plugins/package.json | 3 ++- packages/vite-plugins/{ => src}/index.ts | 0 packages/vite-plugins/tsconfig.json | 10 ++++++++- 34 files changed, 178 insertions(+), 24 deletions(-) create mode 100644 packages/configs/README.md rename packages/configs/{ => src}/lint/index.ts (100%) rename packages/configs/{ => src}/lint/rules/base.ts (96%) rename packages/configs/{ => src}/lint/rules/env.ts (100%) rename packages/configs/{ => src}/lint/rules/i18n.ts (100%) rename packages/configs/{ => src}/lint/rules/json.ts (100%) rename packages/configs/{ => src}/lint/rules/typescript-vue.ts (100%) rename packages/configs/{ => src}/lint/rules/unocss.ts (100%) rename packages/configs/{ => src}/lint/shared.ts (100%) rename packages/configs/{typescript/base.json => src/typescript.json} (96%) rename packages/configs/{unocss.ts => src/uno.ts} (100%) create mode 100644 packages/shared/README.md create mode 100644 packages/shared/eslint.config.ts create mode 100644 packages/shared/package.json create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/ui-toolkit/README.md create mode 100644 packages/ui-toolkit/eslint.config.ts create mode 100644 packages/ui-toolkit/package.json create mode 100644 packages/ui-toolkit/tsconfig.json create mode 100644 packages/ui-toolkit/uno.config.ts create mode 100644 packages/vite-plugins/README.md rename packages/vite-plugins/{ => src}/index.ts (100%) diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts index 2089baab5d6..401dbe78074 100644 --- a/frontend/eslint.config.ts +++ b/frontend/eslint.config.ts @@ -1,9 +1,10 @@ import type { Linter } from 'eslint'; import { getBaseConfig, getTSVueConfig, getNodeFiles, unocss, getWorkerFiles } from '@jellyfin-vue/configs/lint'; +import pkg from './package.json' with { type: 'json' }; // TODO: Add missing rules for i18n and json export default [ - ...getBaseConfig('@jellyfin-vue/frontend'), + ...getBaseConfig(pkg.name), ...getTSVueConfig(true, import.meta.dirname), ...unocss, ...getNodeFiles(), diff --git a/frontend/package.json b/frontend/package.json index 7b659a876a0..5b33d5f80b9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,8 @@ }, "dependencies": { "@fontsource-variable/figtree": "5.1.2", + "@jellyfin-vue/shared": "*", + "@jellyfin-vue/ui-toolkit": "*", "@jellyfin/sdk": "0.11.0", "@skirtle/vue-vnode-utils": "0.2.0", "@vueuse/core": "12.3.0", diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index ab663916f4d..20afaf4f5f5 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -17,12 +17,15 @@ ], "baseUrl": "." }, + "exclude": [ + "dist", + "node_modules", + "coverage" + ], "include": [ - "src/**/*.ts", - "src/**/*.d.ts", - "src/**/*.vue", - "**/**/*.json", - "types/**/*.d.ts", - "*.config.*" + "**/*.ts", + "**/*.vue", + "**/*d.ts", + "**/*.json" ] } diff --git a/frontend/uno.config.ts b/frontend/uno.config.ts index b62c5c7662d..c97deea9e76 100644 --- a/frontend/uno.config.ts +++ b/frontend/uno.config.ts @@ -1 +1 @@ -export { defaultConfig as default } from '@jellyfin-vue/configs/unocss'; +export { defaultConfig as default } from '@jellyfin-vue/configs/uno'; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 985bd8c9641..cd16e5adcae 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,7 +15,7 @@ import { defineConfig } from 'vite'; * TODO: Replace with @jellyfin-vue/vite-plugins after https://github.com/vitejs/vite/issues/5370 * is fixed */ -import { BundleAnalysis, BundleChunking, BundleSizeReport } from '../packages/vite-plugins'; +import { BundleAnalysis, BundleChunking, BundleSizeReport } from '../packages/vite-plugins/src'; import { entrypoints, localeFilesFolder, srcRoot } from './scripts/paths'; import virtualModules from './scripts/virtual-modules'; diff --git a/package-lock.json b/package-lock.json index 21e6953192b..80ff6b8bb8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,8 @@ "version": "0.3.1", "dependencies": { "@fontsource-variable/figtree": "5.1.2", + "@jellyfin-vue/shared": "*", + "@jellyfin-vue/ui-toolkit": "*", "@jellyfin/sdk": "0.11.0", "@skirtle/vue-vnode-utils": "0.2.0", "@vueuse/core": "12.3.0", @@ -2974,10 +2976,18 @@ "resolved": "frontend", "link": true }, + "node_modules/@jellyfin-vue/shared": { + "resolved": "packages/shared", + "link": true + }, "node_modules/@jellyfin-vue/tauri-packaging": { "resolved": "packaging/tauri", "link": true }, + "node_modules/@jellyfin-vue/ui-toolkit": { + "resolved": "packages/ui-toolkit", + "link": true + }, "node_modules/@jellyfin-vue/vite-plugins": { "resolved": "packages/vite-plugins", "link": true @@ -13089,6 +13099,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/shared": { + "name": "@jellyfin-vue/shared", + "devDependencies": { + "@jellyfin-vue/configs": "*" + } + }, + "packages/ui-toolkit": { + "name": "@jellyfin-vue/ui-toolkit", + "devDependencies": { + "@jellyfin-vue/configs": "*" + } + }, "packages/vite-plugins": { "name": "@jellyfin-vue/vite-plugins", "devDependencies": { diff --git a/packages/configs/README.md b/packages/configs/README.md new file mode 100644 index 00000000000..726f24d0298 --- /dev/null +++ b/packages/configs/README.md @@ -0,0 +1,3 @@ +This package contains the base for all the configuration files of this monorepo. +All other packages still contain configuration files, but they import their configs +(or extends, like `tsconfig.json` files) from here. diff --git a/packages/configs/eslint.config.ts b/packages/configs/eslint.config.ts index 17e8e99c6d9..8bf04f36224 100644 --- a/packages/configs/eslint.config.ts +++ b/packages/configs/eslint.config.ts @@ -1,8 +1,9 @@ import type { Linter } from 'eslint'; -import { getBaseConfig, getTSVueConfig, getNodeFiles, tsFiles } from './lint/'; +import { getBaseConfig, getTSVueConfig, getNodeFiles, tsFiles } from './src/lint'; +import pkg from './package.json' with { type: 'json' }; export default [ - ...getBaseConfig('@jellyfin-vue/configs'), + ...getBaseConfig(pkg.name), ...getTSVueConfig(false, import.meta.dirname), ...getNodeFiles(tsFiles) ] satisfies Linter.Config[]; diff --git a/packages/configs/package.json b/packages/configs/package.json index ad7d29aa36e..df1a128ec24 100644 --- a/packages/configs/package.json +++ b/packages/configs/package.json @@ -1,10 +1,11 @@ { "name": "@jellyfin-vue/configs", "type": "module", + "description": "Configuration files for Jellyfin Vue packages", "exports": { - "./typescript": "./typescript/base.json", - "./lint": "./lint/index.ts", - "./unocss": "./unocss.ts" + "./typescript": "./src/typescript.json", + "./lint": "./src/lint/index.ts", + "./uno": "./src/uno.ts" }, "scripts": { "lint": "eslint . --flag unstable_ts_config", diff --git a/packages/configs/lint/index.ts b/packages/configs/src/lint/index.ts similarity index 100% rename from packages/configs/lint/index.ts rename to packages/configs/src/lint/index.ts diff --git a/packages/configs/lint/rules/base.ts b/packages/configs/src/lint/rules/base.ts similarity index 96% rename from packages/configs/lint/rules/base.ts rename to packages/configs/src/lint/rules/base.ts index d0517cf8c8c..45c276492d6 100644 --- a/packages/configs/lint/rules/base.ts +++ b/packages/configs/src/lint/rules/base.ts @@ -38,12 +38,12 @@ export function getBaseConfig(packageName: string, forceCache = !CI_environment, const cacheLocation = join(findUpSync('node_modules', { type: 'directory' }) ?? '', '.cache/eslint', packageName.replace('/', '_')); newArgs.push('--cache', '--cache-location', cacheLocation); - console.log('[@jellyfin-vue/configs/lint] Force enabling caching for this run'); + console.log(`[@jellyfin-vue/configs/lint] (${packageName}) Force enabling caching for this run`); } if (warningAsErrors && !newArgs.some(arg => arg.includes('--max-warnings'))) { newArgs.push('--max-warnings=0'); - console.log('[@jellyfin-vue/configs/lint] Force enabling warnings for this run'); + console.log(`[@jellyfin-vue/configs/lint] (${packageName}) Force enabling warnings for this run`); } const argsHaveChanged = new Set(newArgs).difference(new Set(process.argv.slice(1))).size > 0; diff --git a/packages/configs/lint/rules/env.ts b/packages/configs/src/lint/rules/env.ts similarity index 100% rename from packages/configs/lint/rules/env.ts rename to packages/configs/src/lint/rules/env.ts diff --git a/packages/configs/lint/rules/i18n.ts b/packages/configs/src/lint/rules/i18n.ts similarity index 100% rename from packages/configs/lint/rules/i18n.ts rename to packages/configs/src/lint/rules/i18n.ts diff --git a/packages/configs/lint/rules/json.ts b/packages/configs/src/lint/rules/json.ts similarity index 100% rename from packages/configs/lint/rules/json.ts rename to packages/configs/src/lint/rules/json.ts diff --git a/packages/configs/lint/rules/typescript-vue.ts b/packages/configs/src/lint/rules/typescript-vue.ts similarity index 100% rename from packages/configs/lint/rules/typescript-vue.ts rename to packages/configs/src/lint/rules/typescript-vue.ts diff --git a/packages/configs/lint/rules/unocss.ts b/packages/configs/src/lint/rules/unocss.ts similarity index 100% rename from packages/configs/lint/rules/unocss.ts rename to packages/configs/src/lint/rules/unocss.ts diff --git a/packages/configs/lint/shared.ts b/packages/configs/src/lint/shared.ts similarity index 100% rename from packages/configs/lint/shared.ts rename to packages/configs/src/lint/shared.ts diff --git a/packages/configs/typescript/base.json b/packages/configs/src/typescript.json similarity index 96% rename from packages/configs/typescript/base.json rename to packages/configs/src/typescript.json index 12f09af43c4..3e3ec76430d 100644 --- a/packages/configs/typescript/base.json +++ b/packages/configs/src/typescript.json @@ -35,6 +35,5 @@ "strictTemplates": true, "htmlAttributes": ["aria-*", "data-*"], "fallthroughAttributes": true - }, - "exclude": ["node_modules"] + } } diff --git a/packages/configs/unocss.ts b/packages/configs/src/uno.ts similarity index 100% rename from packages/configs/unocss.ts rename to packages/configs/src/uno.ts diff --git a/packages/configs/tsconfig.json b/packages/configs/tsconfig.json index 7db195c278d..10427bb7c23 100644 --- a/packages/configs/tsconfig.json +++ b/packages/configs/tsconfig.json @@ -1,7 +1,17 @@ { - "extends": "./typescript/base.json", + "extends": "./src/typescript.json", + "compilerOptions": { + "baseUrl": "." + }, + "exclude": [ + "dist", + "node_modules", + "coverage" + ], "include": [ - "lint/**/*.ts", - "*.ts" + "**/*.ts", + "**/*.vue", + "**/*d.ts", + "**/*.json" ] } diff --git a/packages/shared/README.md b/packages/shared/README.md new file mode 100644 index 00000000000..59684ce4b10 --- /dev/null +++ b/packages/shared/README.md @@ -0,0 +1,2 @@ +This package contains logic that it's shared across multiple packages of this monorepo +(like validation logic, constants, etc...) diff --git a/packages/shared/eslint.config.ts b/packages/shared/eslint.config.ts new file mode 100644 index 00000000000..8a173a9dae2 --- /dev/null +++ b/packages/shared/eslint.config.ts @@ -0,0 +1,8 @@ +import type { Linter } from 'eslint'; +import { getBaseConfig, getTSVueConfig } from '@jellyfin-vue/configs/lint'; +import pkg from './package.json' with { type: 'json' }; + +export default [ + ...getBaseConfig(pkg.name), + ...getTSVueConfig(false, import.meta.dirname) +] satisfies Linter.Config[]; diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 00000000000..61ce74918f2 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,17 @@ +{ + "name": "@jellyfin-vue/shared", + "type": "module", + "description": "Shared code between Jellyfin Vue packages", + "exports": { + "./validation": "./src/validation.ts" + }, + "scripts": { + "lint": "eslint . --flag unstable_ts_config", + "lint:fix": "eslint . --fix --flag unstable_ts_config", + "lint:inspect": "eslint-config-inspector", + "check:types": "vue-tsc" + }, + "devDependencies": { + "@jellyfin-vue/configs": "*" + } +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 00000000000..431e6b932de --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@jellyfin-vue/configs/typescript", + "compilerOptions": { + "baseUrl": "." + }, + "exclude": [ + "dist", + "node_modules", + "coverage" + ], + "include": [ + "**/*.ts", + "**/*.vue", + "**/*d.ts", + "**/*.json" + ] +} diff --git a/packages/ui-toolkit/README.md b/packages/ui-toolkit/README.md new file mode 100644 index 00000000000..b982eed94ef --- /dev/null +++ b/packages/ui-toolkit/README.md @@ -0,0 +1,16 @@ +This package contains the component library built in-house for Jellyfin Vue. +Here are the low level components used as a base for building the feature components +(like login form, item views, etc) only. If you're looking for those, +you will find them in the `src/components` of the `@jellyfin-vue/frontend` package. + +All of these components: + +* Use exclusively Composition API, so the Options API can gets treeshaken from the final build. +* Use modern CSS features (like [`position-try`](https://developer.mozilla.org/en-US/docs/Web/CSS/position-try)) +and doesn't care about backwards-compatibility like other component libraries have to (this follows our goal +for targeting just evergreen browsers). +* Reusability it's still important, but the main focus is to provide the features we need for our use case first. +This allows us to have quicker development and deal with less edge cases (like those component libraries have to do). + +Great inspiration for (re)building these components have been taken from Vuetify, Radix, PrimeVue and shadcn. +Thank you very much to all the contributors on those projects! diff --git a/packages/ui-toolkit/eslint.config.ts b/packages/ui-toolkit/eslint.config.ts new file mode 100644 index 00000000000..cf8995532d2 --- /dev/null +++ b/packages/ui-toolkit/eslint.config.ts @@ -0,0 +1,8 @@ +import type { Linter } from 'eslint'; +import { getBaseConfig, getTSVueConfig } from '@jellyfin-vue/configs/lint'; +import pkg from './package.json' with { type: 'json' }; + +export default [ + ...getBaseConfig(pkg.name), + ...getTSVueConfig(true, import.meta.dirname) +] satisfies Linter.Config[]; diff --git a/packages/ui-toolkit/package.json b/packages/ui-toolkit/package.json new file mode 100644 index 00000000000..666ee8cd38b --- /dev/null +++ b/packages/ui-toolkit/package.json @@ -0,0 +1,14 @@ +{ + "name": "@jellyfin-vue/ui-toolkit", + "type": "module", + "description": "Base components and UI toolkit for Jellyfin Vue", + "scripts": { + "lint": "eslint . --flag unstable_ts_config", + "lint:fix": "eslint . --fix --flag unstable_ts_config", + "lint:inspect": "eslint-config-inspector", + "check:types": "vue-tsc" + }, + "devDependencies": { + "@jellyfin-vue/configs": "*" + } +} diff --git a/packages/ui-toolkit/tsconfig.json b/packages/ui-toolkit/tsconfig.json new file mode 100644 index 00000000000..431e6b932de --- /dev/null +++ b/packages/ui-toolkit/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@jellyfin-vue/configs/typescript", + "compilerOptions": { + "baseUrl": "." + }, + "exclude": [ + "dist", + "node_modules", + "coverage" + ], + "include": [ + "**/*.ts", + "**/*.vue", + "**/*d.ts", + "**/*.json" + ] +} diff --git a/packages/ui-toolkit/uno.config.ts b/packages/ui-toolkit/uno.config.ts new file mode 100644 index 00000000000..c97deea9e76 --- /dev/null +++ b/packages/ui-toolkit/uno.config.ts @@ -0,0 +1 @@ +export { defaultConfig as default } from '@jellyfin-vue/configs/uno'; diff --git a/packages/vite-plugins/README.md b/packages/vite-plugins/README.md new file mode 100644 index 00000000000..d35860f72b0 --- /dev/null +++ b/packages/vite-plugins/README.md @@ -0,0 +1,2 @@ +This package contains the Vite plugins used for bundle analysis, +chunk splitting and treeshaking diff --git a/packages/vite-plugins/eslint.config.ts b/packages/vite-plugins/eslint.config.ts index 8954595e5dc..8d574d25fa6 100644 --- a/packages/vite-plugins/eslint.config.ts +++ b/packages/vite-plugins/eslint.config.ts @@ -1,8 +1,9 @@ import type { Linter } from 'eslint'; import { getBaseConfig, getTSVueConfig, getNodeFiles, tsFiles } from '@jellyfin-vue/configs/lint'; +import pkg from './package.json' with { type: 'json' }; export default [ - ...getBaseConfig('@jellyfin-vue/vite-plugins'), + ...getBaseConfig(pkg.name), ...getTSVueConfig(false, import.meta.dirname), ...getNodeFiles(tsFiles) ] satisfies Linter.Config[]; diff --git a/packages/vite-plugins/package.json b/packages/vite-plugins/package.json index 4075bd4b302..6cf4e396327 100644 --- a/packages/vite-plugins/package.json +++ b/packages/vite-plugins/package.json @@ -1,7 +1,8 @@ { "name": "@jellyfin-vue/vite-plugins", - "exports": "./index.ts", + "description": "Vite plugins for Jellyfin Vue packages", "type": "module", + "exports": "./src/index.ts", "scripts": { "lint": "eslint . --flag unstable_ts_config", "lint:fix": "eslint . --fix --flag unstable_ts_config", diff --git a/packages/vite-plugins/index.ts b/packages/vite-plugins/src/index.ts similarity index 100% rename from packages/vite-plugins/index.ts rename to packages/vite-plugins/src/index.ts diff --git a/packages/vite-plugins/tsconfig.json b/packages/vite-plugins/tsconfig.json index 9a68d0fc9d0..1b480fe9797 100644 --- a/packages/vite-plugins/tsconfig.json +++ b/packages/vite-plugins/tsconfig.json @@ -1,6 +1,14 @@ { "extends": "@jellyfin-vue/configs/typescript", + "exclude": [ + "dist", + "node_modules", + "coverage" + ], "include": [ - "*.ts" + "**/*.ts", + "**/*.vue", + "**/*d.ts", + "**/*.json" ] } From fd74dbf2fade07391cd1350703f8054cdc51c219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Thu, 19 Dec 2024 20:09:40 +0100 Subject: [PATCH 02/11] refactor(shared): migrate validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fernando Fernández --- .../Playback/PlaybackSettingsButton.vue | 2 +- frontend/src/components/Forms/LoginForm.vue | 1 - .../components/Item/Card/GenericItemCard.vue | 2 +- .../src/components/Item/Card/ItemCard.vue | 2 +- .../Item/Identify/IdentifyDialog.vue | 2 +- frontend/src/components/Item/ItemMenu.vue | 27 ++++++++++--------- .../Item/MediaDetail/MediaDetailAttr.vue | 2 +- .../Item/MediaDetail/MediaDetailContent.vue | 2 +- .../Item/MediaDetail/MediaDetailDialog.vue | 4 +-- .../Item/Metadata/MetadataEditor.vue | 4 +-- .../src/components/Layout/AudioControls.vue | 6 ++--- frontend/src/components/Layout/Backdrop.vue | 2 +- .../Carousel/Item/ItemsCarouselTitle.vue | 2 +- .../src/components/Layout/SettingsPage.vue | 4 +-- .../components/Playback/DraggableQueue.vue | 2 +- .../src/components/Playback/PlayerElement.vue | 2 +- .../src/components/Playback/SubtitleTrack.vue | 2 +- .../src/components/Wizard/WizardLanguage.vue | 2 +- .../src/components/Wizard/WizardMetadata.vue | 2 +- frontend/src/components/lib/JImg.vue | 2 +- frontend/src/components/lib/JView.vue | 2 +- .../src/components/lib/JVirtual/JVirtual.vue | 2 +- .../lib/JVirtual/j-virtual.worker.ts | 2 +- frontend/src/composables/apis.ts | 2 +- frontend/src/composables/page-title.ts | 2 +- frontend/src/composables/use-datefns.ts | 2 +- frontend/src/pages/genre/[itemId].vue | 2 +- frontend/src/pages/index.vue | 2 +- frontend/src/pages/playback/music.vue | 2 +- frontend/src/plugins/remote/auth.ts | 4 +-- frontend/src/plugins/remote/axios.ts | 2 +- frontend/src/plugins/remote/index.ts | 2 +- frontend/src/plugins/remote/sdk/index.ts | 2 +- frontend/src/plugins/remote/socket.ts | 2 +- frontend/src/plugins/router/index.ts | 2 +- .../src/plugins/router/middlewares/login.ts | 2 +- .../plugins/router/middlewares/playback.ts | 2 +- .../plugins/router/middlewares/validate.ts | 2 +- .../workers/blurhash-decoder.worker.ts | 2 +- .../plugins/workers/canvas-drawer.worker.ts | 2 +- .../src/plugins/workers/generic.worker.ts | 2 +- frontend/src/splashscreen.ts | 2 +- frontend/src/store/api.ts | 2 +- frontend/src/store/client-settings/index.ts | 2 +- .../client-settings/subtitle-settings.ts | 2 +- frontend/src/store/index.ts | 2 +- frontend/src/store/playback-manager.ts | 6 ++--- frontend/src/store/player-element.ts | 4 +-- frontend/src/store/super/base-state.ts | 2 +- frontend/src/store/super/common-store.ts | 2 +- frontend/src/store/super/synced-store.ts | 2 +- frontend/src/store/task-manager.ts | 2 +- frontend/src/utils/external-config.ts | 2 +- frontend/src/utils/i18n.ts | 2 +- frontend/src/utils/items.ts | 2 +- .../helpers/codec-profiles.ts | 2 +- .../shared/src}/validation.ts | 1 + 57 files changed, 79 insertions(+), 76 deletions(-) rename {frontend/src/utils => packages/shared/src}/validation.ts (96%) diff --git a/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue b/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue index 6a08ff18c5b..e8bc2c6c65c 100644 --- a/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue +++ b/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue @@ -94,9 +94,9 @@ diff --git a/frontend/src/components/Layout/Backdrop.vue b/frontend/src/components/Layout/Backdrop.vue index 1bd05ad2b49..42afc062192 100644 --- a/frontend/src/components/Layout/Backdrop.vue +++ b/frontend/src/components/Layout/Backdrop.vue @@ -19,7 +19,7 @@ diff --git a/frontend/src/components/Buttons/Playback/PlayButton.vue b/frontend/src/components/Buttons/Playback/PlayButton.vue index a0f6630ef30..d2fd64e11c8 100644 --- a/frontend/src/components/Buttons/Playback/PlayButton.vue +++ b/frontend/src/components/Buttons/Playback/PlayButton.vue @@ -42,9 +42,9 @@ diff --git a/frontend/src/components/Buttons/Playback/RepeatButton.vue b/frontend/src/components/Buttons/Playback/RepeatButton.vue index 3d9d2ad461b..8069ab1bb74 100644 --- a/frontend/src/components/Buttons/Playback/RepeatButton.vue +++ b/frontend/src/components/Buttons/Playback/RepeatButton.vue @@ -14,7 +14,7 @@ import IMdiRepeat from 'virtual:icons/mdi/repeat'; import IMdiRepeatOnce from 'virtual:icons/mdi/repeat-once'; import { computed } from 'vue'; -import { RepeatMode, playbackManager } from '@/store/playback-manager'; +import { RepeatMode, playbackManager } from '#/store/playback-manager'; const repeatIcon = computed(() => playbackManager.repeatMode.value === RepeatMode.RepeatOne diff --git a/frontend/src/components/Buttons/Playback/ShuffleButton.vue b/frontend/src/components/Buttons/Playback/ShuffleButton.vue index 0873bb77df3..2a97bb3e7b4 100644 --- a/frontend/src/components/Buttons/Playback/ShuffleButton.vue +++ b/frontend/src/components/Buttons/Playback/ShuffleButton.vue @@ -11,5 +11,5 @@ diff --git a/frontend/src/components/Buttons/QueueButton.vue b/frontend/src/components/Buttons/QueueButton.vue index c70ef99e89f..aac5e2010f3 100644 --- a/frontend/src/components/Buttons/QueueButton.vue +++ b/frontend/src/components/Buttons/QueueButton.vue @@ -75,8 +75,8 @@ import IMdiPlaylistMusic from 'virtual:icons/mdi/playlist-music'; import IMdiShuffle from 'virtual:icons/mdi/shuffle'; import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; -import { getTotalEndsAtTime } from '@/utils/time'; -import { InitMode, playbackManager } from '@/store/playback-manager'; +import { getTotalEndsAtTime } from '#/utils/time'; +import { InitMode, playbackManager } from '#/store/playback-manager'; const { size = 40, closeOnClick = false } = defineProps<{ size?: number; diff --git a/frontend/src/components/Buttons/ScrollToTopButton.vue b/frontend/src/components/Buttons/ScrollToTopButton.vue index cb8d06d59d4..33fc58acfac 100644 --- a/frontend/src/components/Buttons/ScrollToTopButton.vue +++ b/frontend/src/components/Buttons/ScrollToTopButton.vue @@ -17,7 +17,7 @@ diff --git a/frontend/src/components/Item/ItemMenu.vue b/frontend/src/components/Item/ItemMenu.vue index 23b66a382df..86d54e769e5 100644 --- a/frontend/src/components/Item/ItemMenu.vue +++ b/frontend/src/components/Item/ItemMenu.vue @@ -401,7 +401,7 @@ function getQueueOptions(): MenuOption[] { } if ( - playbackManager.nextItem.value.Id !== item.Id + playbackManager.nextItem.value?.Id !== item.Id && playbackManager.currentItem.value?.Id !== item.Id ) { queueOptions.push(playNextAction); @@ -427,7 +427,7 @@ function getPlaybackOptions(): MenuOption[] { if (playbackManager.currentItem.value) { if ( - playbackManager.nextItem.value.Id !== item.Id + playbackManager.nextItem.value?.Id !== item.Id && playbackManager.currentItem.value.Id !== item.Id && !queue ) { @@ -450,7 +450,7 @@ function getPlaybackOptions(): MenuOption[] { function getCopyOptions(): MenuOption[] { const copyActions: MenuOption[] = []; - if (remote.auth.currentUser.value.Policy?.EnableContentDownloading) { + if (remote.auth.currentUser.value?.Policy?.EnableContentDownloading) { copyActions.push(copyDownloadURLAction); } @@ -471,7 +471,7 @@ function getLibraryOptions(): MenuOption[] { libraryOptions.push(refreshAction); } - if (remote.auth.currentUser.value.Policy?.IsAdministrator) { + if (remote.auth.currentUser.value?.Policy?.IsAdministrator) { libraryOptions.push(editMetadataAction); if (canIdentify(item)) { @@ -480,8 +480,8 @@ function getLibraryOptions(): MenuOption[] { } if ( - remote.auth.currentUser.value.Policy?.EnableContentDeletion - || remote.auth.currentUser.value.Policy?.EnableContentDeletionFromFolders + remote.auth.currentUser.value?.Policy?.EnableContentDeletion + || remote.auth.currentUser.value?.Policy?.EnableContentDeletionFromFolders ) { libraryOptions.push(deleteItemAction); } diff --git a/frontend/src/components/Layout/AppBar/Buttons/TaskManagerButton.vue b/frontend/src/components/Layout/AppBar/Buttons/TaskManagerButton.vue index 73a0bbe1297..52c429f4c68 100644 --- a/frontend/src/components/Layout/AppBar/Buttons/TaskManagerButton.vue +++ b/frontend/src/components/Layout/AppBar/Buttons/TaskManagerButton.vue @@ -3,10 +3,10 @@ v-if="showButton" :color="buttonColor"> - - diff --git a/packages/ui-toolkit/src/components/JOverlay.vue b/packages/ui-toolkit/src/components/JOverlay.vue new file mode 100644 index 00000000000..2031079463d --- /dev/null +++ b/packages/ui-toolkit/src/components/JOverlay.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/ui-toolkit/src/components/JProgressCircular.vue b/packages/ui-toolkit/src/components/JProgressCircular.vue new file mode 100644 index 00000000000..10802e606f6 --- /dev/null +++ b/packages/ui-toolkit/src/components/JProgressCircular.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/frontend/src/components/lib/JSafeHtml.vue b/packages/ui-toolkit/src/components/JSafeHtml.vue similarity index 85% rename from frontend/src/components/lib/JSafeHtml.vue rename to packages/ui-toolkit/src/components/JSafeHtml.vue index 4ab6cb312d9..ed65c4d749c 100644 --- a/frontend/src/components/lib/JSafeHtml.vue +++ b/packages/ui-toolkit/src/components/JSafeHtml.vue @@ -1,6 +1,6 @@ diff --git a/packages/ui-toolkit/src/components/JTransition.vue b/packages/ui-toolkit/src/components/JTransition.vue new file mode 100644 index 00000000000..763f250022c --- /dev/null +++ b/packages/ui-toolkit/src/components/JTransition.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/frontend/src/components/lib/JVirtual/JVirtual.vue b/packages/ui-toolkit/src/components/JVirtual/index.vue similarity index 72% rename from frontend/src/components/lib/JVirtual/JVirtual.vue rename to packages/ui-toolkit/src/components/JVirtual/index.vue index 30f995765a1..f0b13a9fffd 100644 --- a/frontend/src/components/lib/JVirtual/JVirtual.vue +++ b/packages/ui-toolkit/src/components/JVirtual/index.vue @@ -2,12 +2,11 @@ @@ -15,6 +14,7 @@ - Jellyfin Logo diff --git a/frontend/package.json b/frontend/package.json index 69442e1ee83..bb9d72de842 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,10 @@ "imports": { "#/*": "./src/*" }, + "exports": { + ".": "./src/main.ts", + "./vite-config": "./vite.config.ts" + }, "scripts": { "analyze:bundle": "vite build --mode analyze:bundle", "analyze:cycles": "vite build --mode analyze:cycles", diff --git a/frontend/scripts/paths.ts b/frontend/scripts/paths.ts index f79dbc21379..d85629d9452 100644 --- a/frontend/scripts/paths.ts +++ b/frontend/scripts/paths.ts @@ -1,3 +1,3 @@ import { resolve } from 'node:path'; -export const localeFilesFolder = resolve('locales/**'); +export const localeFilesFolder = resolve(import.meta.dirname, '../locales/**'); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 52becb91e56..39a79bb53b7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,4 +1,4 @@ -import { join } from 'node:path'; +import { resolve } from 'node:path'; import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; import Virtual from '@rollup/plugin-virtual'; import VueDevTools from 'vite-plugin-vue-devtools'; @@ -16,7 +16,7 @@ import { defineConfig } from 'vite'; * TODO: Replace with @jellyfin-vue/vite-plugins after https://github.com/vitejs/vite/issues/5370 * is fixed */ -import { BundleAnalysis, BundleChunking, BundleSizeReport } from '../packages/vite-plugins/src'; +import { JBundle, JMonorepo } from '../packages/vite-plugins/src'; import { JellyfinVueUIToolkit } from '../packages/ui-toolkit/src/resolver'; import virtualModules from './scripts/virtual-modules'; import { localeFilesFolder } from './scripts/paths'; @@ -24,16 +24,23 @@ import { localeFilesFolder } from './scripts/paths'; export default defineConfig({ appType: 'spa', base: './', - cacheDir: '../node_modules/.cache/vite', plugins: [ - BundleAnalysis(), - BundleChunking(), - BundleSizeReport(), + ...JBundle, + JMonorepo(import.meta.dirname, { + splashscreen: { + 'fetch-priority': 'high' + } + }), Virtual(virtualModules), VueRouter({ - dts: './types/global/routes.d.ts', + dts: resolve(import.meta.dirname, 'types/global/routes.d.ts'), importMode: 'sync', - routeBlockLang: 'yaml' + routeBlockLang: 'yaml', + routesFolder: [ + { + src: resolve(import.meta.dirname, 'src/pages') + } + ] }), Vue({ template: { @@ -44,7 +51,8 @@ export default defineConfig({ }), // This plugin allows to autoimport Vue components Components({ - dts: './types/global/components.d.ts', + dirs: [resolve(import.meta.dirname, 'src/components')], + dts: resolve(import.meta.dirname, 'types/global/components.d.ts'), /** * The icons resolver finds icons components from 'unplugin-icons' using this convenction: * {prefix}-{collection}-{icon} e.g. @@ -78,18 +86,15 @@ export default defineConfig({ * See main.ts for an explanation of this target */ target: 'esnext', - /** - * Disable chunk size warnings - */ cssCodeSplit: true, cssMinify: 'lightningcss', modulePreload: false, reportCompressedSize: false, rollupOptions: { input: { - splashscreen: join(import.meta.dirname, 'src/splashscreen.ts'), - main: join(import.meta.dirname, 'src/main.ts'), - index: join(import.meta.dirname, 'index.html') + splashscreen: resolve(import.meta.dirname, 'src/splashscreen.ts'), + main: resolve(import.meta.dirname, 'src/main.ts'), + index: resolve(import.meta.dirname, 'index.html') }, output: { validate: true diff --git a/package-lock.json b/package-lock.json index 36cf6c625f3..fabe4dc79c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2982,6 +2982,10 @@ "resolved": "packaging/tauri", "link": true }, + "node_modules/@jellyfin-vue/tauri-runtime": { + "resolved": "packages/tauri-runtime", + "link": true + }, "node_modules/@jellyfin-vue/ui-toolkit": { "resolved": "packages/ui-toolkit", "link": true @@ -13107,6 +13111,16 @@ "@jellyfin-vue/configs": "*" } }, + "packages/tauri-runtime": { + "name": "@jellyfin-vue/tauri-runtime", + "dependencies": { + "@jellyfin-vue/frontend": "*" + }, + "devDependencies": { + "@jellyfin-vue/configs": "*", + "vite": "6.0.7" + } + }, "packages/ui-toolkit": { "name": "@jellyfin-vue/ui-toolkit", "dependencies": { @@ -13125,6 +13139,7 @@ "name": "@jellyfin-vue/vite-plugins", "devDependencies": { "@jellyfin-vue/configs": "*", + "find-up-simple": "1.0.0", "pretty-bytes": "6.1.1", "sonda": "0.7.1", "vite": "6.0.7" @@ -13133,10 +13148,11 @@ "packaging/tauri": { "name": "@jellyfin-vue/tauri-packaging", "dependencies": { - "@jellyfin-vue/frontend": "*" + "@jellyfin-vue/tauri-runtime": "*" }, "devDependencies": { - "@tauri-apps/cli": "2.2.2" + "@tauri-apps/cli": "2.2.2", + "vite": "6.0.7" } } } diff --git a/packages/configs/src/lint/rules/base.ts b/packages/configs/src/lint/rules/base.ts index 7ffd4ce2469..3cd7acea0a8 100644 --- a/packages/configs/src/lint/rules/base.ts +++ b/packages/configs/src/lint/rules/base.ts @@ -1,4 +1,4 @@ -import { join, basename } from 'node:path'; +import { basename, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; import type { Linter } from 'eslint'; import { findUpSync } from 'find-up-simple'; @@ -35,7 +35,7 @@ export function getBaseConfig(packageName: string, forceCache = !CI_environment, const newArgs = process.argv.slice(1); if (forceCache && !(newArgs.includes('--cache') && newArgs.includes('--cache-location'))) { - const cacheLocation = join(findUpSync('node_modules', { type: 'directory' }) ?? '', '.cache/eslint', packageName.replace('/', '_')); + const cacheLocation = resolve(findUpSync('node_modules', { type: 'directory' }) ?? '', '.cache/eslint', packageName.replace('/', '_')); newArgs.push('--cache', '--cache-location', cacheLocation); console.log(`[@jellyfin-vue/configs/lint] (${packageName}) Force enabling caching for this run`); diff --git a/packages/tauri-runtime/entrypoint.ts b/packages/tauri-runtime/entrypoint.ts new file mode 100644 index 00000000000..d5fb95427a4 --- /dev/null +++ b/packages/tauri-runtime/entrypoint.ts @@ -0,0 +1,9 @@ +import './src/main.ts'; +/** + * The Tauri-specific code must be loaded before the frontend code to ensure that + * all the polyfills for the runtime have been loaded + * before the frontend code is executed. + * + * THIS IMPORT ALWAYS NEEDS TO BE THE LAST IMPORT IN THIS FILE + */ +import '@jellyfin-vue/frontend'; diff --git a/packages/tauri-runtime/eslint.config.ts b/packages/tauri-runtime/eslint.config.ts new file mode 100644 index 00000000000..96866c6f329 --- /dev/null +++ b/packages/tauri-runtime/eslint.config.ts @@ -0,0 +1,9 @@ +import type { Linter } from 'eslint'; +import { getBaseConfig, getNodeFiles, getTSVueConfig, tsFiles } from '@jellyfin-vue/configs/lint'; +import pkg from './package.json' with { type: 'json' }; + +export default [ + ...getBaseConfig(pkg.name), + ...getTSVueConfig(false, import.meta.dirname), + ...getNodeFiles(tsFiles) +] satisfies Linter.Config[]; diff --git a/packages/tauri-runtime/package.json b/packages/tauri-runtime/package.json new file mode 100644 index 00000000000..58f7efafc11 --- /dev/null +++ b/packages/tauri-runtime/package.json @@ -0,0 +1,26 @@ +{ + "name": "@jellyfin-vue/tauri-runtime", + "type": "module", + "description": "The frontend including tauri-specific runtime code", + "scripts": { + "analyze:bundle": "vite build --mode analyze:bundle", + "analyze:cycles": "vite build --mode analyze:cycles", + "lint": "eslint . --flag unstable_ts_config", + "lint:fix": "eslint . --fix --flag unstable_ts_config", + "lint:inspect": "eslint-config-inspector", + "build": "vite build", + "check": "npm run lint && npm run check:types", + "check:types": "vue-tsc", + "start": "vite", + "serve": "vite preview", + "prod": "npm run build && npm run serve", + "clean": "git clean -fxd" + }, + "devDependencies": { + "@jellyfin-vue/configs": "*", + "vite": "6.0.7" + }, + "dependencies": { + "@jellyfin-vue/frontend": "*" + } +} diff --git a/packages/tauri-runtime/src/main.ts b/packages/tauri-runtime/src/main.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/tauri-runtime/tsconfig.json b/packages/tauri-runtime/tsconfig.json new file mode 100644 index 00000000000..0692e8899d2 --- /dev/null +++ b/packages/tauri-runtime/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@jellyfin-vue/configs/typescript", + "compilerOptions": { + "baseUrl": "." + }, + "exclude": [ + "dist", + "node_modules", + "coverage" + ], + "include": [ + "**/*.ts" + ] +} diff --git a/packages/tauri-runtime/vite.config.ts b/packages/tauri-runtime/vite.config.ts new file mode 100644 index 00000000000..7f1b5b08307 --- /dev/null +++ b/packages/tauri-runtime/vite.config.ts @@ -0,0 +1,33 @@ +import { resolve } from 'node:path'; +import { defineConfig, mergeConfig } from 'vite'; +// @ts-expect-error - This error will be fixed once the Vite team adds monorepo support for config files +import BaseConfig from '../../frontend/vite.config.ts'; + +const host = process.env.TAURI_DEV_HOST; + +export default defineConfig( + mergeConfig(BaseConfig, { + // prevent vite from obscuring rust errors + clearScreen: false, + build: { + outDir: resolve(import.meta.dirname, 'dist'), + rollupOptions: { + input: { + main: resolve(import.meta.dirname, 'entrypoint.ts') + } + }, + // don't minify for debug builds + minify: process.env.TAURI_ENV_DEBUG ? false : 'esbuild', + // produce sourcemaps for debug builds + sourcemap: !!process.env.TAURI_ENV_DEBUG + }, + server: { + // Tauri expects a fixed port, fail if that port is not available + strictPort: true, + // if the host Tauri is expecting is set, use it + ...(host ? { host } : {}) + }, + // Env variables starting with the item of `envPrefix` will be exposed in tauri's source code through `import.meta.env`. + envPrefix: ['VITE_', 'TAURI_ENV_*'] + }) +); diff --git a/packages/vite-plugins/package.json b/packages/vite-plugins/package.json index 6cf4e396327..f4b29703e5d 100644 --- a/packages/vite-plugins/package.json +++ b/packages/vite-plugins/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@jellyfin-vue/configs": "*", + "find-up-simple": "1.0.0", "pretty-bytes": "6.1.1", "sonda": "0.7.1", "vite": "6.0.7" diff --git a/packages/vite-plugins/src/bundle.ts b/packages/vite-plugins/src/bundle.ts new file mode 100644 index 00000000000..b8623bc663f --- /dev/null +++ b/packages/vite-plugins/src/bundle.ts @@ -0,0 +1,177 @@ +import { basename, resolve } from 'node:path'; +import { globSync } from 'node:fs'; +import { lstat, rename, rm } from 'node:fs/promises'; +import type { LiteralUnion } from 'type-fest'; +import type { RollupLog } from 'rollup'; +import prettyBytes from 'pretty-bytes'; +import Sonda from 'sonda/rollup'; +import { normalizePath, preview, type Plugin } from 'vite'; + +/** + * TODO: Track https://github.com/vitejs/vite/pull/19005 so we can pull Vite's default config instead + * of hardcoding it here. + */ +const defaultConfig = { build: { outDir: 'dist' } }; + +/** + * This plugin extracts the logic for the analyze commands, so the main Vite config is cleaner. + */ +export function JBundleAnalysis(): Plugin { + let mode: LiteralUnion<'analyze:bundle' | 'analyze:cycles', string>; + const report_filename = () => resolve(defaultConfig.build.outDir, 'bundle-report.html'); + const warnings: RollupLog[] = []; + + return { + name: 'Jellyfin_Vue:bundle_analysis', + enforce: 'pre', + config: (_, env) => { + mode = env.mode; + + if (env.mode === 'analyze:bundle') { + return { + build: { + sourcemap: true, + rollupOptions: { + plugins: [ + Sonda({ + open: false, + filename: report_filename(), + detailed: true, + sources: true, + gzip: false, + brotli: false + }) + ] + } + } + }; + } else if (env.mode === 'analyze:cycles') { + return { + build: { + rollupOptions: { + onwarn: (warning) => { + if (warning.code === 'CIRCULAR_DEPENDENCY') { + warnings.push(warning); + } + } + } + } + }; + } + }, + closeBundle: async () => { + if (mode === 'analyze:cycles') { + if (warnings.length > 0) { + for (const warning of warnings) { + console.warn(warning); + } + + throw new Error('There are circular dependencies'); + } + } else if (mode === 'analyze:bundle') { + await rename(report_filename(), resolve(defaultConfig.build.outDir, 'index.html')); + + for (const file of globSync(resolve(defaultConfig.build.outDir, '**/*'))) { + if (!file.endsWith('index.html')) { + await rm(file, { force: true, recursive: true }); + } + } + + const server = await preview(); + + console.log(); + server.printUrls(); + } + } + }; +} + +/** + * Creates the Rollup's chunking strategy of the application (for code-splitting) + */ +export function JBundleChunking(): Plugin { + return { + name: 'Jellyfin_Vue:bundle_chunking', + enforce: 'pre', + config: () => ({ + build: { + rollupOptions: { + output: { + /** + * This is the first thing that should be debugged when there are issues + * withe the bundle. Check these issues: + * - https://github.com/vitejs/vite/issues/5142 + * - https://github.com/evanw/esbuild/issues/399 + * - https://github.com/rollup/rollup/issues/3888 + */ + manualChunks(id) { + const match = /node_modules\/([^/]+)/.exec(id)?.[1]; + + if (id.includes('virtual:locales') || ((id.includes('vuetify') || id.includes('date-fns')) && id.includes('locale'))) { + return 'localization/meta'; + } + + if (id.includes('@intlify/unplugin-vue-i18n/messages') + ) { + return 'localization/messages'; + } + + /** + * Split each vendor in its own chunk + */ + if (match) { + return `vendor/${match.replace('@', '')}`; + } + } + } + } + } + }) + }; +} + +/** + * Reports the total siz and also per file type + */ +export function JBundleSizeReport(): Plugin { + const files = new Map(); + const sizes = new Map(); + let outDir: string; + let totalSize = 0; + const convert = (bytes: number) => prettyBytes(bytes, { minimumFractionDigits: 2 }); + + return { + name: 'Jellyfin_Vue:bundle_size_report', + enforce: 'pre', + // Only run on normal production builds, not analyze:bundle or analyze:cycles + apply: (_, env) => env.mode === 'production', + closeBundle: async () => { + for (const file of globSync(`${outDir}/**/**`)) { + const stat = await lstat(file); + + if (stat.isFile()) { + const extension = basename(file).split('.').at(-1); + const filenum = files.get(extension!) ?? 0; + const size = sizes.get(extension!) ?? 0; + + files.set(extension!, filenum + 1); + sizes.set(extension!, size + stat.size); + totalSize += stat.size; + } + } + + for (const [key, val] of sizes) { + const num = files.get(key)!; + + console.info( + `There are ${num} ${key} ${num > 1 ? 'files' : 'file'} (${convert(val)})` + ); + } + + console.info(`Total size of the bundle: ${convert(totalSize)}`); + }, + configResolved: (config) => { + outDir = normalizePath(config.build.outDir); + } + }; +} diff --git a/packages/vite-plugins/src/index.ts b/packages/vite-plugins/src/index.ts index 0f4499f90a0..a474fb46147 100644 --- a/packages/vite-plugins/src/index.ts +++ b/packages/vite-plugins/src/index.ts @@ -1,177 +1,4 @@ -import { basename, join } from 'node:path'; -import { globSync } from 'node:fs'; -import { lstat, rename, rm } from 'node:fs/promises'; -import type { LiteralUnion } from 'type-fest'; -import type { RollupLog } from 'rollup'; -import prettyBytes from 'pretty-bytes'; -import Sonda from 'sonda/rollup'; -import { normalizePath, preview, type Plugin } from 'vite'; +import { JBundleAnalysis, JBundleChunking, JBundleSizeReport } from './bundle.ts'; -/** - * TODO: Track https://github.com/vitejs/vite/pull/19005 so we can pull Vite's default config instead - * of hardcoding it here. - */ -const defaultConfig = { build: { outDir: 'dist' } }; - -/** - * This plugin extracts the logic for the analyze commands, so the main Vite config is cleaner. - */ -export function BundleAnalysis(): Plugin { - let mode: LiteralUnion<'analyze:bundle' | 'analyze:cycles', string>; - const report_filename = () => join(defaultConfig.build.outDir, 'bundle-report.html'); - const warnings: RollupLog[] = []; - - return { - name: 'Jellyfin_Vue:bundle_analysis', - enforce: 'pre', - config: (_, env) => { - mode = env.mode; - - if (env.mode === 'analyze:bundle') { - return { - build: { - sourcemap: true, - rollupOptions: { - plugins: [ - Sonda({ - open: false, - filename: report_filename(), - detailed: true, - sources: true, - gzip: false, - brotli: false - }) - ] - } - } - }; - } else if (env.mode === 'analyze:cycles') { - return { - build: { - rollupOptions: { - onwarn: (warning) => { - if (warning.code === 'CIRCULAR_DEPENDENCY') { - warnings.push(warning); - } - } - } - } - }; - } - }, - closeBundle: async () => { - if (mode === 'analyze:cycles') { - if (warnings.length > 0) { - for (const warning of warnings) { - console.warn(warning); - } - - throw new Error('There are circular dependencies'); - } - } else if (mode === 'analyze:bundle') { - await rename(report_filename(), join(defaultConfig.build.outDir, 'index.html')); - - for (const file of globSync(join(defaultConfig.build.outDir, '**/*'))) { - if (!file.endsWith('index.html')) { - await rm(file, { force: true, recursive: true }); - } - } - - const server = await preview(); - - console.log(); - server.printUrls(); - } - } - }; -} - -/** - * Creates the Rollup's chunking strategy of the application (for code-splitting) - */ -export function BundleChunking(): Plugin { - return { - name: 'Jellyfin_Vue:bundle_chunking', - enforce: 'pre', - config: () => ({ - build: { - rollupOptions: { - output: { - /** - * This is the first thing that should be debugged when there are issues - * withe the bundle. Check these issues: - * - https://github.com/vitejs/vite/issues/5142 - * - https://github.com/evanw/esbuild/issues/399 - * - https://github.com/rollup/rollup/issues/3888 - */ - manualChunks(id) { - const match = /node_modules\/([^/]+)/.exec(id)?.[1]; - - if (id.includes('virtual:locales') || ((id.includes('vuetify') || id.includes('date-fns')) && id.includes('locale'))) { - return 'localization/meta'; - } - - if (id.includes('@intlify/unplugin-vue-i18n/messages') - ) { - return 'localization/messages'; - } - - /** - * Split each vendor in its own chunk - */ - if (match) { - return `vendor/${match.replace('@', '')}`; - } - } - } - } - } - }) - }; -} - -/** - * Reports the total siz and also per file type - */ -export function BundleSizeReport(): Plugin { - const files = new Map(); - const sizes = new Map(); - let outDir: string; - let totalSize = 0; - const convert = (bytes: number) => prettyBytes(bytes, { minimumFractionDigits: 2 }); - - return { - name: 'Jellyfin_Vue:bundle_size_report', - enforce: 'pre', - // Only run on normal production builds, not analyze:bundle or analyze:cycles - apply: (_, env) => env.mode === 'production', - closeBundle: async () => { - for (const file of globSync(`${outDir}/**/**`)) { - const stat = await lstat(file); - - if (stat.isFile()) { - const extension = basename(file).split('.').at(-1); - const filenum = files.get(extension!) ?? 0; - const size = sizes.get(extension!) ?? 0; - - files.set(extension!, filenum + 1); - sizes.set(extension!, size + stat.size); - totalSize += stat.size; - } - } - - for (const [key, val] of sizes) { - const num = files.get(key)!; - - console.info( - `There are ${num} ${key} ${num > 1 ? 'files' : 'file'} (${convert(val)})` - ); - } - - console.info(`Total size of the bundle: ${convert(totalSize)}`); - }, - configResolved: (config) => { - outDir = normalizePath(config.build.outDir); - } - }; -} +export const JBundle = [JBundleAnalysis(), JBundleChunking(), JBundleSizeReport()]; +export { JMonorepo } from './transform.ts'; diff --git a/packages/vite-plugins/src/transform.ts b/packages/vite-plugins/src/transform.ts new file mode 100644 index 00000000000..06a75e3dad2 --- /dev/null +++ b/packages/vite-plugins/src/transform.ts @@ -0,0 +1,49 @@ +import { extname, join } from 'node:path'; +import type { Plugin } from 'vite'; +import type { InputOptions } from 'rollup'; +import { findUpSync } from 'find-up-simple'; + +/** + * This plugin allows the Vite Config to be used as a monorepo with multiple Vite projects + * In normal setups, Vite takes the application inputs from the ` diff --git a/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue b/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue index 1d350d9d327..e721254cf4d 100644 --- a/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue +++ b/frontend/src/components/Buttons/Playback/PlaybackSettingsButton.vue @@ -2,9 +2,7 @@ - - - + diff --git a/frontend/src/components/Buttons/Playback/PreviousTrackButton.vue b/frontend/src/components/Buttons/Playback/PreviousTrackButton.vue index a75fe326007..dc32af5db8a 100644 --- a/frontend/src/components/Buttons/Playback/PreviousTrackButton.vue +++ b/frontend/src/components/Buttons/Playback/PreviousTrackButton.vue @@ -2,15 +2,14 @@ - - - + @click.passive="() => playbackManager.setPreviousItem()" + @dblclick.passive="() => playbackManager.setPreviousItem(true)"> + diff --git a/frontend/src/components/Buttons/Playback/RepeatButton.vue b/frontend/src/components/Buttons/Playback/RepeatButton.vue index 8069ab1bb74..d3b4609f3b5 100644 --- a/frontend/src/components/Buttons/Playback/RepeatButton.vue +++ b/frontend/src/components/Buttons/Playback/RepeatButton.vue @@ -3,22 +3,13 @@ v-bind="$attrs" icon :color="playbackManager.isRepeating.value ? 'primary' : undefined" - @click="playbackManager.toggleRepeatMode"> - + + :class="playbackManager.repeatMode.value === RepeatMode.RepeatOne ? 'i-mdi:repeat-once' : 'i-mdi:repeat'" /> diff --git a/frontend/src/components/Buttons/Playback/ShuffleButton.vue b/frontend/src/components/Buttons/Playback/ShuffleButton.vue index 2a97bb3e7b4..b3ed61dac8e 100644 --- a/frontend/src/components/Buttons/Playback/ShuffleButton.vue +++ b/frontend/src/components/Buttons/Playback/ShuffleButton.vue @@ -4,9 +4,9 @@ icon :color="playbackManager.isShuffling.value ? 'primary' : undefined" @click="playbackManager.toggleShuffle"> - - - + diff --git a/frontend/src/components/Buttons/QueueButton.vue b/frontend/src/components/Buttons/QueueButton.vue index aac5e2010f3..fb7b1aefa50 100644 --- a/frontend/src/components/Buttons/QueueButton.vue +++ b/frontend/src/components/Buttons/QueueButton.vue @@ -2,9 +2,7 @@ - - - + @@ -22,9 +20,9 @@ - + :class="modeIcon" /> diff --git a/frontend/src/components/Buttons/ScrollToTopButton.vue b/frontend/src/components/Buttons/ScrollToTopButton.vue index 33fc58acfac..82b71004784 100644 --- a/frontend/src/components/Buttons/ScrollToTopButton.vue +++ b/frontend/src/components/Buttons/ScrollToTopButton.vue @@ -8,10 +8,8 @@ location="bottom right" position="fixed" variant="elevated" - @click="scrollToTop"> - - - + @click.passive="scrollToTop"> + diff --git a/frontend/src/components/Buttons/SortButton.vue b/frontend/src/components/Buttons/SortButton.vue index d763aa2dd5b..2cfbe2b17fb 100644 --- a/frontend/src/components/Buttons/SortButton.vue +++ b/frontend/src/components/Buttons/SortButton.vue @@ -3,23 +3,24 @@ icon :disabled="disabled" @click="emit('change', model[0], !ascending)"> - - - - - - + {{ !$vuetify.display.smAndDown ? sortingLabel : undefined }} - - - - - - + + + + - - - - + @@ -22,15 +23,16 @@ + "> + + @@ -38,7 +40,6 @@ diff --git a/frontend/src/components/Item/MediaInfo.vue b/frontend/src/components/Item/MediaInfo.vue index eb30aa575f1..97054840f78 100644 --- a/frontend/src/components/Item/MediaInfo.vue +++ b/frontend/src/components/Item/MediaInfo.vue @@ -11,11 +11,8 @@ {{ item.ProductionYear }} {{ item.OfficialRating }} - - - + {{ item.CommunityRating.toFixed(1) }} @@ -57,8 +54,4 @@ span:first-of-type { span:last-of-type { margin-right: 0; } - -.rating-icon { - opacity: 0.6; -} diff --git a/frontend/src/components/Item/MediaStreamSelector.vue b/frontend/src/components/Item/MediaStreamSelector.vue index 7240ae3b1dd..40b4aec2497 100644 --- a/frontend/src/components/Item/MediaStreamSelector.vue +++ b/frontend/src/components/Item/MediaStreamSelector.vue @@ -16,8 +16,13 @@ + :subtitle="item.raw.subtitle"> + + @@ -26,11 +31,6 @@ import { ref, computed, watch } from 'vue'; import type { MediaStream } from '@jellyfin/sdk/lib/generated-client'; import { useI18n } from 'vue-i18n'; -import IMdiSurroundSound20 from 'virtual:icons/mdi/surround-sound-2-0'; -import IMdiSurroundSound31 from 'virtual:icons/mdi/surround-sound-3-1'; -import IMdiSurroundSound51 from 'virtual:icons/mdi/surround-sound-5-1'; -import IMdiSurroundSound71 from 'virtual:icons/mdi/surround-sound-7-1'; -import IMdiSurroundSound from 'virtual:icons/mdi/surround-sound'; import { watchImmediate } from '@vueuse/core'; import { isNil } from '@jellyfin-vue/shared/validation'; import { getLocaleName } from '#/utils/i18n'; @@ -50,22 +50,22 @@ const { t, locale } = useI18n(); * Audio layout to get related icon * @returns Icon name */ -function getSurroundIcon(layout: string): typeof IMdiSurroundSound { +function getSurroundIcon(layout: string) { switch (layout) { case '2.0': { - return IMdiSurroundSound20; + return 'i-mdi:surround-sound-2-0'; } case '3.1': { - return IMdiSurroundSound31; + return 'i-mdi:surround-sound-3-1'; } case '5.1': { - return IMdiSurroundSound51; + return 'i-mdi:surround-sound-5-1'; } case '7.1': { - return IMdiSurroundSound71; + return 'i-mdi:surround-sound-7-1'; } default: { - return IMdiSurroundSound; + return 'i-mdi:surround-sound'; } } } @@ -76,7 +76,7 @@ function getSurroundIcon(layout: string): typeof IMdiSurroundSound { */ function getTrackIcon( track: MediaStream -): typeof IMdiSurroundSound | undefined { +) { if (type === 'Audio' && track.ChannelLayout) { return getSurroundIcon(track.ChannelLayout); } diff --git a/frontend/src/components/Item/Metadata/ImageEditor.vue b/frontend/src/components/Item/Metadata/ImageEditor.vue index a655542ca99..a908b452fc2 100644 --- a/frontend/src/components/Item/Metadata/ImageEditor.vue +++ b/frontend/src/components/Item/Metadata/ImageEditor.vue @@ -29,17 +29,13 @@ - - - + - - - + @@ -75,17 +71,13 @@ - - - + - - - + diff --git a/frontend/src/components/Item/Metadata/ImageSearch.vue b/frontend/src/components/Item/Metadata/ImageSearch.vue index 67d7d58971f..88f39c4b8d4 100644 --- a/frontend/src/components/Item/Metadata/ImageSearch.vue +++ b/frontend/src/components/Item/Metadata/ImageSearch.vue @@ -100,9 +100,7 @@ icon :disabled="loading" @click="onDownload(item)"> - - - + diff --git a/frontend/src/components/Item/Metadata/MetadataEditor.vue b/frontend/src/components/Item/Metadata/MetadataEditor.vue index 3b0f8bed618..357f028bdef 100644 --- a/frontend/src/components/Item/Metadata/MetadataEditor.vue +++ b/frontend/src/components/Item/Metadata/MetadataEditor.vue @@ -140,9 +140,7 @@ @click="onPersonAdd"> @@ -164,19 +162,15 @@ ) "> diff --git a/frontend/src/components/Item/Metadata/PersonEditor.vue b/frontend/src/components/Item/Metadata/PersonEditor.vue index 3994c711996..0054909e6f8 100644 --- a/frontend/src/components/Item/Metadata/PersonEditor.vue +++ b/frontend/src/components/Item/Metadata/PersonEditor.vue @@ -18,10 +18,8 @@ " :alt="$t('person')"> diff --git a/frontend/src/components/Item/WatchedIndicator.vue b/frontend/src/components/Item/WatchedIndicator.vue index 33afcbc0a01..049bf909350 100644 --- a/frontend/src/components/Item/WatchedIndicator.vue +++ b/frontend/src/components/Item/WatchedIndicator.vue @@ -3,9 +3,7 @@ color="success" size="small" variant="elevated"> - - - + diff --git a/frontend/src/components/Layout/AppBar/AppBar.vue b/frontend/src/components/Layout/AppBar/AppBar.vue index cfffd3ba3b8..c7c024e2e4a 100644 --- a/frontend/src/components/Layout/AppBar/AppBar.vue +++ b/frontend/src/components/Layout/AppBar/AppBar.vue @@ -10,9 +10,7 @@ @click="navigationDrawer = !navigationDrawer" /> @@ -22,9 +20,7 @@ v-hide="remote.socket.isConnected.value && isConnectedToServer" :color="isConnectedToServer ? 'yellow' : 'red'"> - - diff --git a/frontend/src/components/Layout/Images/UserImage.vue b/frontend/src/components/Layout/Images/UserImage.vue index e917c28c72c..6f855d1e28e 100644 --- a/frontend/src/components/Layout/Images/UserImage.vue +++ b/frontend/src/components/Layout/Images/UserImage.vue @@ -11,9 +11,8 @@ - - - + diff --git a/frontend/src/components/Layout/Navigation/CommitLink.vue b/frontend/src/components/Layout/Navigation/CommitLink.vue index 3e5d087646a..161de09bbc0 100644 --- a/frontend/src/components/Layout/Navigation/CommitLink.vue +++ b/frontend/src/components/Layout/Navigation/CommitLink.vue @@ -2,14 +2,18 @@ + rel="noopener noreferrer"> + + diff --git a/frontend/src/components/Playback/DraggableQueue.vue b/frontend/src/components/Playback/DraggableQueue.vue index 9d71c31632c..e13f3fe47e3 100644 --- a/frontend/src/components/Playback/DraggableQueue.vue +++ b/frontend/src/components/Playback/DraggableQueue.vue @@ -7,19 +7,20 @@