mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-11-23 05:59:55 +00:00
perf: pause patching while navigating
This also fixes some data changes that happened hile navigating like: * When using search and clicking on an item, the "No results available" message would appear * When navigating to a liibrary, the transparency effects of the navdrawer or appbar would match those of the entering route. Signed-off-by: Fernando Fernández <ferferga@hotmail.com>
This commit is contained in:
parent
67d03099c0
commit
12d6bc5376
@ -3,30 +3,9 @@
|
||||
<VApp>
|
||||
<JApp>
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<JTransition
|
||||
:name="route.meta.layout.transition.enter ?? defaultTransition"
|
||||
:mode="defaultTransitionMode ?? route.meta.layout.transition.mode">
|
||||
<Suspense @resolve="apploaded = true">
|
||||
<JView
|
||||
:key="route.meta.layout.name ?? 'default'"
|
||||
:comp="getLayoutComponent(route.meta.layout.name)">
|
||||
<JTransition
|
||||
:name="route.meta.layout.transition.enter ?? defaultTransition"
|
||||
:mode="defaultTransitionMode ?? route.meta.layout.transition.mode">
|
||||
<Suspense suspensible>
|
||||
<JView
|
||||
:key="route.name"
|
||||
:comp="Component" />
|
||||
</Suspense>
|
||||
</JTransition>
|
||||
</JView>
|
||||
<template
|
||||
v-if="!apploaded"
|
||||
#fallback>
|
||||
<JSplashscreen />
|
||||
</template>
|
||||
</Suspense>
|
||||
</JTransition>
|
||||
:comp="Component"
|
||||
:route="route" />
|
||||
</RouterView>
|
||||
</JApp>
|
||||
<Snackbar />
|
||||
@ -36,15 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, type Component as VueComponent, onMounted } from 'vue';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
import DefaultLayout from '@/layouts/default.vue';
|
||||
import FullPageLayout from '@/layouts/fullpage.vue';
|
||||
import ServerLayout from '@/layouts/server.vue';
|
||||
|
||||
const apploaded = shallowRef(false);
|
||||
const defaultTransition = 'slide-x-reverse';
|
||||
const defaultTransitionMode = 'out-in';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
/**
|
||||
* When app is mounted, the classes and styles we initialized in the pre-Vue splashscreen in body
|
||||
@ -56,21 +27,4 @@ onMounted(() => {
|
||||
document.body.removeAttribute('class');
|
||||
document.body.removeAttribute('style');
|
||||
});
|
||||
|
||||
/**
|
||||
* Return the appropiate layout component according to the route's meta.layout property
|
||||
*/
|
||||
function getLayoutComponent(layout: RouteMeta['layout']['name']): VueComponent {
|
||||
switch (layout) {
|
||||
case 'fullpage': {
|
||||
return FullPageLayout as VueComponent;
|
||||
}
|
||||
case 'server': {
|
||||
return ServerLayout as VueComponent;
|
||||
}
|
||||
default: {
|
||||
return DefaultLayout;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -60,14 +60,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, type Ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { windowScroll, isConnectedToServer, prefersNoTransparency } from '@/store';
|
||||
import { windowScroll, isConnectedToServer, transparencyEffects } from '@/store';
|
||||
import { clientSettings } from '@/store/client-settings';
|
||||
import { remote } from '@/plugins/remote';
|
||||
import { JView_isRouting } from '@/store/keys';
|
||||
|
||||
const route = useRoute();
|
||||
const { y } = windowScroll;
|
||||
const transparentAppBar = computed(() => !prefersNoTransparency.value && route.meta.layout.transparent && y.value < 10);
|
||||
const isRouting = inject(JView_isRouting);
|
||||
const transparentAppBar = computed(previous => isRouting?.value ? previous : transparencyEffects.value && y.value < 10);
|
||||
|
||||
/**
|
||||
* Cycle between the different color schemas
|
||||
|
@ -40,10 +40,10 @@
|
||||
import IMdiHome from 'virtual:icons/mdi/home';
|
||||
import { computed, inject, type Ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { RouteNamedMap } from 'vue-router/auto-routes';
|
||||
import type { getLibraryIcon } from '@/utils/items';
|
||||
import { prefersNoTransparency } from '@/store';
|
||||
import { transparencyEffects } from '@/store';
|
||||
import { JView_isRouting } from '@/store/keys';
|
||||
|
||||
export interface DrawerItem {
|
||||
icon: ReturnType<typeof getLibraryIcon>;
|
||||
@ -56,11 +56,11 @@ const { order, drawerItems } = defineProps<{
|
||||
drawerItems: DrawerItem[];
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const drawer = inject<Ref<boolean>>('NavigationDrawer');
|
||||
const transparentLayout = computed(() => !prefersNoTransparency.value && route.meta.layout.transparent);
|
||||
const isRouting = inject(JView_isRouting);
|
||||
const transparentLayout = computed(previous => isRouting?.value ? previous : transparencyEffects.value);
|
||||
|
||||
const items = [
|
||||
{
|
||||
|
@ -4,9 +4,18 @@
|
||||
class="j-transition"
|
||||
v-bind="$attrs"
|
||||
:name="forcedDisable || disabled ? undefined : `j-transition-${name}`"
|
||||
@before-leave="leaving = true"
|
||||
@after-leave="onNoLeave"
|
||||
@leave-cancelled="onNoLeave">
|
||||
@before-leave="() => {
|
||||
leaving = true;
|
||||
$attrs.onBeforeLeave?.();
|
||||
}"
|
||||
@after-leave="() => {
|
||||
onNoLeave();
|
||||
$attrs.onAfterLeave?.();
|
||||
}"
|
||||
@leave-cancelled="() => {
|
||||
onNoLeave();
|
||||
$attrs.onLeaveCancelled?.();
|
||||
}">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
@ -26,6 +35,10 @@ interface Props {
|
||||
* If the transition should be disabled
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Don't stop patching the DOM while transitioning
|
||||
*/
|
||||
skipPausing?: boolean;
|
||||
}
|
||||
|
||||
export type JTransitionProps = TransitionProps & Props;
|
||||
@ -33,14 +46,16 @@ const forcedDisable = computed(() => prefersNoMotion.value || isSlow.value);
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { name = 'fade', group, disabled } = defineProps<Props>();
|
||||
const { name = 'fade', group, disabled, skipPausing } = defineProps<Props>();
|
||||
const leaving = shallowRef(false);
|
||||
const onNoLeave = () => leaving.value = false;
|
||||
|
||||
usePausableEffect(leaving);
|
||||
if (!skipPausing) {
|
||||
usePausableEffect(leaving);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Set scoped and remove .j-transition* prefix after: https://github.com/vuejs/core/issues/5148 -->
|
||||
<!-- TODO: Set scoped and remove .j-transition* prefix after: https://github.com/vuejs/core/issues/5148#issuecomment-2041118368 -->
|
||||
|
||||
<style>
|
||||
.j-transition {
|
||||
|
@ -1,20 +1,103 @@
|
||||
<template>
|
||||
<JTransition
|
||||
:name="route.meta.layout.transition.enter ?? defaultTransition"
|
||||
:mode="defaultTransitionMode ?? route.meta.layout.transition.mode"
|
||||
skip-pausing>
|
||||
<Suspense
|
||||
:suspensible="!root"
|
||||
@pending="resolved = false"
|
||||
@resolve=" resolved = true">
|
||||
<div
|
||||
:key="root ? route.meta.layout.name ?? 'default' : route.name"
|
||||
class="j-transition uno-h-full">
|
||||
<component
|
||||
:is="comp">
|
||||
<slot />
|
||||
</component>
|
||||
<Component
|
||||
:is="root ? getLayoutComponent(route.meta.layout.name) : comp">
|
||||
<JView
|
||||
v-if="root"
|
||||
v-bind="$props" />
|
||||
<slot v-else />
|
||||
</Component>
|
||||
</div>
|
||||
<template
|
||||
v-if="!apploaded && root"
|
||||
#fallback>
|
||||
<JSplashscreen />
|
||||
</template>
|
||||
</Suspense>
|
||||
</JTransition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* TODO: Remove j-transition classes from this file once https://github.com/vuejs/core/issues/5148 is fixed
|
||||
*/
|
||||
import type { Component } from 'vue';
|
||||
<!-- TODO: Remove j-transition classes from this file once https://github.com/vuejs/core/issues/5148#issuecomment-2041118368 is fixed -->
|
||||
|
||||
const { comp } = defineProps<{
|
||||
comp: Component;
|
||||
}>();
|
||||
<script lang="ts">
|
||||
import { onErrorCaptured, shallowRef, type Component, watch, computed, provide, useId, ref, type WatchOptions, inject } from 'vue';
|
||||
import type { RouteLocationNormalizedGeneric, RouteMeta } from 'vue-router';
|
||||
import DefaultLayout from '@/layouts/default.vue';
|
||||
import FullPageLayout from '@/layouts/fullpage.vue';
|
||||
import ServerLayout from '@/layouts/server.vue';
|
||||
import { usePausableEffect } from '@/composables/use-pausable-effect';
|
||||
import { JView_isRouting } from '@/store/keys';
|
||||
import { router } from '@/plugins/router';
|
||||
import { isNil } from '@/utils/validation';
|
||||
|
||||
/**
|
||||
* Return the appropiate layout component according to the route's meta.layout property
|
||||
*/
|
||||
function getLayoutComponent(layout: RouteMeta['layout']['name']): Component {
|
||||
switch (layout) {
|
||||
case 'fullpage': {
|
||||
return FullPageLayout as Component;
|
||||
}
|
||||
case 'server': {
|
||||
return ServerLayout as Component;
|
||||
}
|
||||
default: {
|
||||
return DefaultLayout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTransition = 'slide-x-reverse';
|
||||
const defaultTransitionMode = 'out-in';
|
||||
const watchOps = { flush: 'sync' } satisfies WatchOptions;
|
||||
const apploaded = shallowRef(false);
|
||||
const isRouting = shallowRef(false);
|
||||
const _resolveStatus = ref<Record<string, boolean>>({});
|
||||
const allResolved = computed(() => Object.values(_resolveStatus.value).every(Boolean));
|
||||
const mustBePaused = computed(() => !allResolved.value || isRouting.value);
|
||||
|
||||
watch(() => router.currentRoute.value.name, () => isRouting.value = true, watchOps);
|
||||
watch(allResolved, () => isRouting.value = false, watchOps);
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { comp, route } = defineProps<{
|
||||
comp: Component;
|
||||
route: RouteLocationNormalizedGeneric;
|
||||
}>();
|
||||
|
||||
const id = useId();
|
||||
const root = isNil(inject(JView_isRouting));
|
||||
|
||||
const resolved = computed({
|
||||
get() {
|
||||
return _resolveStatus.value[id] ?? false;
|
||||
},
|
||||
set(newVal) {
|
||||
_resolveStatus.value[id] = newVal;
|
||||
|
||||
if (root && newVal) {
|
||||
apploaded.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (root) {
|
||||
provide(JView_isRouting, mustBePaused);
|
||||
usePausableEffect(mustBePaused);
|
||||
onErrorCaptured(() => {
|
||||
resolved.value = true;
|
||||
isRouting.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
@ -2,8 +2,8 @@ import type { Api } from '@jellyfin/sdk';
|
||||
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { deepEqual } from 'fast-equals';
|
||||
import { computed, effectScope, getCurrentScope, isRef, shallowRef, toValue, unref, watch, type ComputedRef, type Ref } from 'vue';
|
||||
import { until } from '@vueuse/core';
|
||||
import { computed, effectScope, getCurrentScope, inject, isRef, shallowRef, toValue, unref, watch, type ComputedRef, type Ref } from 'vue';
|
||||
import { until, whenever } from '@vueuse/core';
|
||||
import { useLoading } from '@/composables/use-loading';
|
||||
import { useSnackbar } from '@/composables/use-snackbar';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
@ -11,7 +11,7 @@ import { remote } from '@/plugins/remote';
|
||||
import { isConnectedToServer } from '@/store';
|
||||
import { apiStore } from '@/store/api';
|
||||
import { isArray, isNil } from '@/utils/validation';
|
||||
import { router } from '@/plugins/router';
|
||||
import { JView_isRouting } from '@/store/keys';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
type OmittedKeys = 'fields' | 'userId' | 'enableImages' | 'enableTotalRecordCount' | 'enableImageTypes';
|
||||
@ -306,10 +306,18 @@ function _sharedInternalLogic<T extends Record<K, (...args: any[]) => any>, K ex
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => router.currentRoute.value.name, () => scope.stop(),
|
||||
/**
|
||||
* If we're routing, the effects of this composable are no longer useful, so we stop them
|
||||
* to avoid accidental data fetching (e.g due to route param changes)
|
||||
*/
|
||||
const isRouting = inject(JView_isRouting);
|
||||
|
||||
if (!isNil(isRouting)) {
|
||||
whenever(isRouting, () => scope.stop(),
|
||||
{ once: true, flush: 'sync' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there's available data before component mount, we return the cached data rightaway (see below how
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
import { computedAsync, useMediaControls, useMediaQuery, useNetwork, useNow, useScroll } from '@vueuse/core';
|
||||
import { shallowRef } from 'vue';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import { remote } from '@/plugins/remote';
|
||||
import { isNil } from '@/utils/validation';
|
||||
import { router } from '@/plugins/router';
|
||||
|
||||
/**
|
||||
* This file contains global variables (specially VueUse refs) that are used multiple times across the client.
|
||||
@ -85,6 +86,11 @@ export const hasHDRDisplay = useMediaQuery('(video-dynamic-range:high)');
|
||||
*/
|
||||
export const isSlow = useMediaQuery('(update:slow)');
|
||||
|
||||
/**
|
||||
* Whether the layout must use transparency effects
|
||||
*/
|
||||
export const transparencyEffects = computed(() => !prefersNoTransparency.value && router.currentRoute.value.meta.layout.transparent);
|
||||
|
||||
/**
|
||||
* Reactively tracks if the user is connected to the server
|
||||
*/
|
||||
|
7
frontend/src/store/keys.ts
Normal file
7
frontend/src/store/keys.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* This file contains all the symbols used with provide/inject API:
|
||||
* https://vuejs.org/guide/components/provide-inject.html#working-with-symbol-keys
|
||||
*/
|
||||
import type { ComputedRef, InjectionKey } from 'vue';
|
||||
|
||||
export const JView_isRouting = Symbol('JView:isRouting') as InjectionKey<ComputedRef<boolean>>;
|
Loading…
Reference in New Issue
Block a user