mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-11-23 22:19:46 +00:00
feat(ui): switch to Radix and Inter, cleanups (#2250)
* Inter is going to be one of the brand Jellyfin fonts, as discussed in Matrix's UI/UX channels. Check this: https://matrix.to/#/!xrSDQsdjElWFYUAMoG:matrix.org/$_ZCxjEgHmaYdFo6aiCfqdXSnhEvg8UAksk0NG5PUhZg?via=bonifacelabs.ca&via=t2bot.io&via=matrix.org (Previous messages are also relevant) * Add radix-vue to use their components as base. There is only one modification I would like to have to their components (the ability to pass arbitrary props to Primitive) but it's something that's likely to be accepted upstream * Minor cleanup in Carousel styles * Use the client font as Jassub's font * Install UnoCSS and it's resets (not enabled yet due to Vuetify inconsistencies) Signed-off-by: Fernando Fernández <ferferga@hotmail.com>
This commit is contained in:
parent
75bca13018
commit
a379e25b01
@ -18,8 +18,8 @@
|
|||||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "5.0.8",
|
|
||||||
"@jellyfin/sdk": "0.8.2",
|
"@jellyfin/sdk": "0.8.2",
|
||||||
|
"@unocss/reset": "0.58.5",
|
||||||
"@vueuse/components": "10.9.0",
|
"@vueuse/components": "10.9.0",
|
||||||
"@vueuse/core": "10.9.0",
|
"@vueuse/core": "10.9.0",
|
||||||
"audiomotion-analyzer": "4.4.0",
|
"audiomotion-analyzer": "4.4.0",
|
||||||
@ -31,9 +31,11 @@
|
|||||||
"dompurify": "3.0.9",
|
"dompurify": "3.0.9",
|
||||||
"fast-equals": "5.0.1",
|
"fast-equals": "5.0.1",
|
||||||
"hls.js": "1.5.7",
|
"hls.js": "1.5.7",
|
||||||
|
"inter-ui": "4.0.2",
|
||||||
"jassub": "1.7.15",
|
"jassub": "1.7.15",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"marked": "12.0.0",
|
"marked": "12.0.0",
|
||||||
|
"radix-vue": "1.4.9",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.2",
|
||||||
"swiper": "11.0.7",
|
"swiper": "11.0.7",
|
||||||
"uuid": "9.0.1",
|
"uuid": "9.0.1",
|
||||||
@ -80,6 +82,7 @@
|
|||||||
"sass": "1.71.1",
|
"sass": "1.71.1",
|
||||||
"typescript": "5.3.3",
|
"typescript": "5.3.3",
|
||||||
"typescript-eslint-parser-for-extra-files": "0.6.0",
|
"typescript-eslint-parser-for-extra-files": "0.6.0",
|
||||||
|
"unocss": "0.58.5",
|
||||||
"unplugin-icons": "0.18.5",
|
"unplugin-icons": "0.18.5",
|
||||||
"unplugin-vue-components": "0.26.0",
|
"unplugin-vue-components": "0.26.0",
|
||||||
"unplugin-vue-router": "0.8.4",
|
"unplugin-vue-router": "0.8.4",
|
||||||
|
@ -1,16 +1,3 @@
|
|||||||
.default-icon {
|
|
||||||
display: flex;
|
|
||||||
align-content: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swiperContainer {
|
.swiperContainer {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
* {
|
* {
|
||||||
-webkit-font-smoothing: antialiased;
|
font-family: "InterVariable", system-ui !important;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
cursor: var(--j-client-cursor);
|
cursor: var(--j-client-cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@
|
|||||||
class="elevation-2">
|
class="elevation-2">
|
||||||
<div
|
<div
|
||||||
class="absolute-cover card-content d-flex justify-center align-center">
|
class="absolute-cover card-content d-flex justify-center align-center">
|
||||||
<JSlot class="card-image">
|
<RSlot class="card-image">
|
||||||
<slot
|
<slot
|
||||||
name="image" />
|
name="image" />
|
||||||
</JSlot>
|
</RSlot>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute-cover card-overlay d-flex justify-center align-center"
|
class="absolute-cover card-overlay d-flex justify-center align-center"
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
<div
|
<div
|
||||||
:class="useResponsiveClasses('slide-backdrop')"
|
:class="useResponsiveClasses('slide-backdrop')"
|
||||||
data-swiper-parallax="-100">
|
data-swiper-parallax="-100">
|
||||||
<div class="default-icon" />
|
|
||||||
<BlurhashImage
|
<BlurhashImage
|
||||||
:key="`${item.Id}-image`"
|
:key="`${item.Id}-image`"
|
||||||
:item="getRelatedItem(item)"
|
:item="getRelatedItem(item)"
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<JSlot
|
<RSlot
|
||||||
@mouseenter="isHovering = true"
|
@mouseenter="isHovering = true"
|
||||||
@mouseleave="isHovering = false">
|
@mouseleave="isHovering = false">
|
||||||
<slot
|
<slot
|
||||||
:is-hovering="isHovering" />
|
:is-hovering="isHovering" />
|
||||||
</JSlot>
|
</RSlot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access */
|
|
||||||
import type { Arrayable } from '@vueuse/core';
|
|
||||||
import { cloneVNode, mergeProps, Fragment, type VNode } from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render multiple child nodes from a slot, including nested fragments.
|
|
||||||
*/
|
|
||||||
function renderSlotFragments(children?: VNode[]): VNode[] {
|
|
||||||
if (!children) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return children.flatMap((child) => {
|
|
||||||
if (child.type === Fragment) {
|
|
||||||
return renderSlotFragments(child.children as VNode[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [child];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
inheritAttrs: false,
|
|
||||||
setup(_, { attrs, slots }) {
|
|
||||||
return (): Arrayable<VNode> | undefined => {
|
|
||||||
|
|
||||||
if (!slots.default) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childrens = renderSlotFragments(slots.default());
|
|
||||||
|
|
||||||
const [firstChildren, ...otherChildren] = childrens;
|
|
||||||
|
|
||||||
if (Object.keys(attrs).length > 0) {
|
|
||||||
// Remove props ref from being inferred
|
|
||||||
delete firstChildren.props?.ref;
|
|
||||||
|
|
||||||
const mergedProps = mergeProps(attrs, firstChildren.props ?? {});
|
|
||||||
|
|
||||||
// Remove class to prevent duplicated
|
|
||||||
if (attrs.class && firstChildren.props?.class) {
|
|
||||||
delete firstChildren.props.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloned = cloneVNode(firstChildren, mergedProps);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Explicitly override props starting with `on`.
|
|
||||||
* It seems cloneVNode from Vue doesn't like overriding `onXXX` props. So
|
|
||||||
* we have to do it manually.
|
|
||||||
*/
|
|
||||||
|
|
||||||
for (const prop in mergedProps) {
|
|
||||||
if (prop.startsWith('on')) {
|
|
||||||
cloned.props ||= {};
|
|
||||||
cloned.props[prop] = mergedProps[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return childrens.length === 1 ? cloned : [cloned, ...otherChildren];
|
|
||||||
}
|
|
||||||
|
|
||||||
return childrens;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access */
|
|
||||||
</script>
|
|
@ -16,8 +16,16 @@ import { vuetify } from '@/plugins/vuetify';
|
|||||||
/**
|
/**
|
||||||
* - GLOBAL STYLES -
|
* - GLOBAL STYLES -
|
||||||
*/
|
*/
|
||||||
|
import 'inter-ui/inter-variable.css';
|
||||||
|
/**
|
||||||
|
* TODO: Re-enable once Vuetify is gone
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* import 'uno.css';
|
||||||
|
* import 'virtual:unocss-devtools';
|
||||||
|
*/
|
||||||
|
import '@unocss/reset/tailwind.css';
|
||||||
import '@/assets/styles/global.scss';
|
import '@/assets/styles/global.scss';
|
||||||
import '@fontsource/roboto';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - VUE PLUGINS, STORE AND DIRECTIVE -
|
* - VUE PLUGINS, STORE AND DIRECTIVE -
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
* an agnostic way, regardless of where the media is being played (remotely or locally)
|
* an agnostic way, regardless of where the media is being played (remotely or locally)
|
||||||
*/
|
*/
|
||||||
import JASSUB from 'jassub';
|
import JASSUB from 'jassub';
|
||||||
import jassubDefaultFont from 'jassub/dist/default.woff2?url';
|
|
||||||
import jassubWorker from 'jassub/dist/jassub-worker.js?url';
|
import jassubWorker from 'jassub/dist/jassub-worker.js?url';
|
||||||
import jassubWasmUrl from 'jassub/dist/jassub-worker.wasm?url';
|
import jassubWasmUrl from 'jassub/dist/jassub-worker.wasm?url';
|
||||||
import { nextTick, reactive, watch } from 'vue';
|
import { nextTick, reactive, watch } from 'vue';
|
||||||
@ -98,10 +97,12 @@ class PlayerElementStore {
|
|||||||
fonts: attachedFonts,
|
fonts: attachedFonts,
|
||||||
workerUrl: jassubWorker,
|
workerUrl: jassubWorker,
|
||||||
wasmUrl: jassubWasmUrl,
|
wasmUrl: jassubWasmUrl,
|
||||||
availableFonts: { 'liberation sans': jassubDefaultFont },
|
fallbackFont: 'InterVariable',
|
||||||
// Both parameters needed for subs to work on iOS
|
// Both parameters needed for subs to work on iOS
|
||||||
prescaleFactor: 0.8,
|
prescaleFactor: 0.8,
|
||||||
onDemandRender: false
|
onDemandRender: false,
|
||||||
|
// OffscreenCanvas doesn't work perfectly on Workers: https://github.com/ThaUnknown/jassub/issues/33
|
||||||
|
offscreenRender: false
|
||||||
});
|
});
|
||||||
} else if (jassub) {
|
} else if (jassub) {
|
||||||
if (isArray(attachedFonts)) {
|
if (isArray(attachedFonts)) {
|
||||||
|
2
frontend/types/global/components.d.ts
vendored
2
frontend/types/global/components.d.ts
vendored
@ -98,7 +98,6 @@ declare module 'vue' {
|
|||||||
ItemsCarouselTitle: typeof import('./../../src/components/Layout/Carousel/Item/ItemsCarouselTitle.vue')['default']
|
ItemsCarouselTitle: typeof import('./../../src/components/Layout/Carousel/Item/ItemsCarouselTitle.vue')['default']
|
||||||
JHover: typeof import('./../../src/components/lib/JHover.vue')['default']
|
JHover: typeof import('./../../src/components/lib/JHover.vue')['default']
|
||||||
JImg: typeof import('./../../src/components/lib/JImg.vue')['default']
|
JImg: typeof import('./../../src/components/lib/JImg.vue')['default']
|
||||||
JSlot: typeof import('./../../src/components/lib/JSlot.vue')['default']
|
|
||||||
LikeButton: typeof import('./../../src/components/Buttons/LikeButton.vue')['default']
|
LikeButton: typeof import('./../../src/components/Buttons/LikeButton.vue')['default']
|
||||||
LoadingIndicator: typeof import('./../../src/components/System/LoadingIndicator.vue')['default']
|
LoadingIndicator: typeof import('./../../src/components/System/LoadingIndicator.vue')['default']
|
||||||
LocaleSwitcher: typeof import('./../../src/components/System/LocaleSwitcher.vue')['default']
|
LocaleSwitcher: typeof import('./../../src/components/System/LocaleSwitcher.vue')['default']
|
||||||
@ -129,6 +128,7 @@ declare module 'vue' {
|
|||||||
RepeatButton: typeof import('./../../src/components/Buttons/Playback/RepeatButton.vue')['default']
|
RepeatButton: typeof import('./../../src/components/Buttons/Playback/RepeatButton.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
RSlot: typeof import('radix-vue')['Slot']
|
||||||
ScrollToTopButton: typeof import('./../../src/components/Buttons/ScrollToTopButton.vue')['default']
|
ScrollToTopButton: typeof import('./../../src/components/Buttons/ScrollToTopButton.vue')['default']
|
||||||
SearchField: typeof import('./../../src/components/Layout/AppBar/SearchField.vue')['default']
|
SearchField: typeof import('./../../src/components/Layout/AppBar/SearchField.vue')['default']
|
||||||
SeasonTabs: typeof import('./../../src/components/Item/SeasonTabs.vue')['default']
|
SeasonTabs: typeof import('./../../src/components/Item/SeasonTabs.vue')['default']
|
||||||
|
@ -12,6 +12,9 @@ import {
|
|||||||
VueUseDirectiveResolver
|
VueUseDirectiveResolver
|
||||||
} from 'unplugin-vue-components/resolvers';
|
} from 'unplugin-vue-components/resolvers';
|
||||||
import Components from 'unplugin-vue-components/vite';
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import RadixVueResolver from 'radix-vue/resolver';
|
||||||
|
import UnoCSS from 'unocss/vite';
|
||||||
|
import { presetUno } from 'unocss';
|
||||||
import VueRouter from 'unplugin-vue-router/vite';
|
import VueRouter from 'unplugin-vue-router/vite';
|
||||||
import { defineConfig, type UserConfig } from 'vite';
|
import { defineConfig, type UserConfig } from 'vite';
|
||||||
import { entrypoints, localeFilesFolder, srcRoot } from './scripts/paths';
|
import { entrypoints, localeFilesFolder, srcRoot } from './scripts/paths';
|
||||||
@ -41,7 +44,10 @@ export default defineConfig(({ mode }): UserConfig => {
|
|||||||
IconsResolver(),
|
IconsResolver(),
|
||||||
VueUseComponentsResolver(),
|
VueUseComponentsResolver(),
|
||||||
Vuetify3Resolver(),
|
Vuetify3Resolver(),
|
||||||
VueUseDirectiveResolver()
|
VueUseDirectiveResolver(),
|
||||||
|
RadixVueResolver({
|
||||||
|
prefix: 'R'
|
||||||
|
})
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
/**
|
/**
|
||||||
@ -57,6 +63,11 @@ export default defineConfig(({ mode }): UserConfig => {
|
|||||||
fullInstall: false,
|
fullInstall: false,
|
||||||
forceStringify: true,
|
forceStringify: true,
|
||||||
include: localeFilesFolder
|
include: localeFilesFolder
|
||||||
|
}),
|
||||||
|
UnoCSS({
|
||||||
|
presets: [
|
||||||
|
presetUno()
|
||||||
|
]
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
|
1178
package-lock.json
generated
1178
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user