mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-11-23 05:59:55 +00:00
feat(FontSelector): refactor font selector logic and move to component
This commit is contained in:
parent
84dcbe2260
commit
ac424696e8
@ -104,6 +104,7 @@
|
||||
"dlnaSettingsDescription": "Configure DLNA settings and profile",
|
||||
"editMetadata": "Edit metadata",
|
||||
"editPerson": "Edit person",
|
||||
"enablePermission": "Enable Permission",
|
||||
"enableUPNP": "Enable UPnP",
|
||||
"endsAt": "Ends at {time}",
|
||||
"eps": "EPs",
|
||||
@ -145,7 +146,8 @@
|
||||
"lastActive": "Last active",
|
||||
"lastActivityDate": "Last seen {value}",
|
||||
"latestLibrary": "Latest {libraryName}",
|
||||
"lazyLoading": "Showing {value} items. Loading more…",
|
||||
"lazyLoading": "Showing {value} items. Loading more...",
|
||||
"learnMore": "Learn More",
|
||||
"libraries": "Libraries",
|
||||
"librariesSettingsDescription": "Manage libraries and their metadata",
|
||||
"libraryAccess": "Library access",
|
||||
@ -155,6 +157,7 @@
|
||||
"liked": "Liked",
|
||||
"liveTv": "Live TV & DVR",
|
||||
"liveTvSettingsDescription": "Manage TV tuners, guide data providers and DVR settings",
|
||||
"localFontsPermissionWarning": "Access to the local fonts permission is required to select a font.",
|
||||
"login": "Login",
|
||||
"loginAs": "Login as {name}",
|
||||
"logo": "Logo",
|
||||
@ -279,6 +282,7 @@
|
||||
"pushToBottom": "Move to the end",
|
||||
"pushToTop": "Move to the beginning",
|
||||
"quality": "Quality",
|
||||
"queryLocalFontsNotSupportedWarning": "Local fonts are currently not supported by your browser.",
|
||||
"queue": "Queue",
|
||||
"queueItems": "{items} tracks",
|
||||
"rating": "Rating",
|
||||
|
74
frontend/src/components/Selectors/FontSelector.vue
Normal file
74
frontend/src/components/Selectors/FontSelector.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<VAlert
|
||||
v-if="!isQueryLocalFontsSupported"
|
||||
class="uno-mb-5"
|
||||
color="warning"
|
||||
icon="$warning">
|
||||
{{ $t('queryLocalFontsNotSupportedWarning') }}
|
||||
<br>
|
||||
|
||||
<a
|
||||
class="uno-font-bold"
|
||||
href="https://caniuse.com/mdn-api_window_querylocalfonts"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
{{ $t('learnMore') }}
|
||||
</a>
|
||||
</VAlert>
|
||||
|
||||
<VAlert
|
||||
v-if="isQueryLocalFontsSupported && !fontAccess"
|
||||
class="uno-mb-5"
|
||||
color="warning"
|
||||
icon="$warning">
|
||||
{{ $t('localFontsPermissionWarning') }}
|
||||
<br>
|
||||
<a
|
||||
class="uno-font-bold"
|
||||
href="https://support.google.com/chrome/answer/114662?hl=en&co=GENIE.Platform=Desktop"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
{{ $t('enablePermission') }}
|
||||
</a>
|
||||
</VAlert>
|
||||
|
||||
<VSelect
|
||||
v-model="model"
|
||||
:label="label"
|
||||
:items="fontList"
|
||||
:disabled="!isQueryLocalFontsSupported || !fontAccess" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePermission, useSupported } from '@vueuse/core';
|
||||
import { ref, computed, watchEffect } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps<{
|
||||
label?: string;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const model = defineModel<string | undefined>();
|
||||
const fontList = ref<string[]>([]);
|
||||
|
||||
const fontPermission = usePermission('local-fonts');
|
||||
const fontAccess = computed(() => fontPermission.value == 'granted');
|
||||
const isQueryLocalFontsSupported = useSupported(() => 'queryLocalFonts' in window);
|
||||
|
||||
watchEffect(async () => {
|
||||
if (isQueryLocalFontsSupported.value && fontAccess.value) {
|
||||
const localFonts = await window.queryLocalFonts();
|
||||
const uniqueFonts: string[] = [];
|
||||
|
||||
for (const font of localFonts) {
|
||||
if (!uniqueFonts.includes(font.family)) {
|
||||
uniqueFonts.push(font.family);
|
||||
}
|
||||
}
|
||||
|
||||
fontList.value = uniqueFonts;
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,80 +1,52 @@
|
||||
<template>
|
||||
<SettingsPage page-title="subtitles">
|
||||
<SettingsPage>
|
||||
<template #title>
|
||||
{{ t('subtitles') }}
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<VCol
|
||||
md="6"
|
||||
class="pt-0 pb-4">
|
||||
<VSelect
|
||||
<FontSelector
|
||||
v-model="clientSettings.subtitleAppearance.fontFamily"
|
||||
:label="$t('subtitleFont')"
|
||||
:items="availableSubtitleFonts"/>
|
||||
:label="$t('subtitleFont')" />
|
||||
|
||||
<VSlider
|
||||
v-model="clientSettings.subtitleAppearance.fontSize"
|
||||
:label="$t('fontSize')"
|
||||
:min="1"
|
||||
:max="4.5"
|
||||
:step="0.1"/>
|
||||
:step="0.1" />
|
||||
|
||||
<VSlider
|
||||
v-model="clientSettings.subtitleAppearance.positionFromBottom"
|
||||
:label="$t('positionFromBottom')"
|
||||
:min="0"
|
||||
:max="30"
|
||||
:step="1"/>
|
||||
:step="1" />
|
||||
|
||||
<VCheckbox
|
||||
v-model="clientSettings.subtitleAppearance.backdrop"
|
||||
:label="$t('backdrop')"/>
|
||||
:label="$t('backdrop')" />
|
||||
|
||||
<VCheckbox
|
||||
v-model="clientSettings.subtitleAppearance.stroke"
|
||||
:label="$t('stroke')"/>
|
||||
:label="$t('stroke')" />
|
||||
|
||||
<SubtitleTrack :previewText="$t('subtitlePreviewText')"/>
|
||||
<SubtitleTrack :preview-text="$t('subtitlePreviewText')" />
|
||||
</VCol>
|
||||
</template>
|
||||
</SettingsPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router/auto';
|
||||
import { clientSettings } from '@/store/client-settings';
|
||||
import { SUBTITLE_FONT_FAMILIES } from '@/utils/subtitles';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
route.meta.title = t('subtitles');
|
||||
|
||||
const availableSubtitleFonts = ref<string[]>([]);
|
||||
|
||||
// Load subtitle fonts available to the client
|
||||
const loadAvailableFonts = async () => {
|
||||
const checkFontAvailability = async (font: string) => {
|
||||
// Default font
|
||||
if (font == SUBTITLE_FONT_FAMILIES[0]) {
|
||||
return font;
|
||||
}
|
||||
|
||||
try {
|
||||
const fontFace = new FontFace(font, `local(${font})`);
|
||||
const loaded = await fontFace.load();
|
||||
|
||||
if (loaded.status !== 'error') {
|
||||
return font;
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const fontChecks = SUBTITLE_FONT_FAMILIES.map(checkFontAvailability);
|
||||
const fonts = await Promise.all(fontChecks);
|
||||
const validFonts = fonts.filter(font => font !== undefined);
|
||||
|
||||
availableSubtitleFonts.value = validFonts;
|
||||
};
|
||||
|
||||
await loadAvailableFonts();
|
||||
</script>
|
||||
|
@ -8,19 +8,17 @@ import { remote } from '@/plugins/remote';
|
||||
import { vuetify } from '@/plugins/vuetify';
|
||||
import { sealed } from '@/utils/validation';
|
||||
import { SyncedStore } from '@/store/super/synced-store';
|
||||
import { FALLBACK_SUBTITLE_FONT, SUBTITLE_FONT_FAMILIES } from '@/utils/subtitles';
|
||||
|
||||
/**
|
||||
* == INTERFACES AND TYPES ==
|
||||
* Casted typings for the CustomPrefs property of DisplayPreferencesDto
|
||||
*/
|
||||
|
||||
export type subtitleFontFamily = typeof SUBTITLE_FONT_FAMILIES[number];
|
||||
export interface ClientSettingsState {
|
||||
darkMode: 'auto' | boolean;
|
||||
locale: string;
|
||||
subtitleAppearance: {
|
||||
fontFamily: subtitleFontFamily;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
positionFromBottom: number;
|
||||
backdrop: boolean;
|
||||
@ -92,7 +90,7 @@ class ClientSettingsStore extends SyncedStore<ClientSettingsState> {
|
||||
darkMode: 'auto',
|
||||
locale: 'auto',
|
||||
subtitleAppearance: {
|
||||
fontFamily: SUBTITLE_FONT_FAMILIES[0],
|
||||
fontFamily: 'FigTree Variable',
|
||||
fontSize: 1.5,
|
||||
positionFromBottom: 10,
|
||||
backdrop: true,
|
||||
|
@ -17,22 +17,6 @@ export interface ParsedSubtitleTrack {
|
||||
|
||||
type TagMap = Record<string, string>;
|
||||
|
||||
export const SUBTITLE_FONT_FAMILIES = [
|
||||
'Figtree Variable',
|
||||
'Trebuchet MS',
|
||||
'Verdana',
|
||||
'Sans Serif MS',
|
||||
'Arial',
|
||||
'Courier New',
|
||||
'Times New Roman',
|
||||
'Old English Text MT',
|
||||
'Century Gothic',
|
||||
'Helvetica',
|
||||
'Garamond'
|
||||
] as const;
|
||||
|
||||
export const FALLBACK_SUBTITLE_FONT = 'sans-serif, system-ui';
|
||||
|
||||
/**
|
||||
* Parse time string used in subtitle files to seconds
|
||||
*/
|
||||
|
2
frontend/types/global/components.d.ts
vendored
2
frontend/types/global/components.d.ts
vendored
@ -27,6 +27,7 @@ declare module 'vue' {
|
||||
DateInput: typeof import('./../../src/components/Item/Metadata/DateInput.vue')['default']
|
||||
DraggableQueue: typeof import('./../../src/components/Playback/DraggableQueue.vue')['default']
|
||||
FilterButton: typeof import('./../../src/components/Buttons/FilterButton.vue')['default']
|
||||
FontSelector: typeof import('./../../src/components/Selectors/FontSelector.vue')['default']
|
||||
GenericDialog: typeof import('./../../src/components/Dialogs/GenericDialog.vue')['default']
|
||||
GenericItemCard: typeof import('./../../src/components/Item/Card/GenericItemCard.vue')['default']
|
||||
IDashiconsAlbum: typeof import('~icons/dashicons/album')['default']
|
||||
@ -156,6 +157,7 @@ declare module 'vue' {
|
||||
UserButton: typeof import('./../../src/components/Layout/AppBar/Buttons/UserButton.vue')['default']
|
||||
UserCard: typeof import('./../../src/components/Users/UserCard.vue')['default']
|
||||
UserImage: typeof import('./../../src/components/Layout/Images/UserImage.vue')['default']
|
||||
VAlert: typeof import('vuetify/components')['VAlert']
|
||||
VApp: typeof import('vuetify/components')['VApp']
|
||||
VAppBar: typeof import('vuetify/components')['VAppBar']
|
||||
VAppBarNavIcon: typeof import('vuetify/components')['VAppBarNavIcon']
|
||||
|
Loading…
Reference in New Issue
Block a user