Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: content fetching when navigating #2441

Merged
merged 2 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
:mode="defaultTransitionMode ?? route.meta.layout.transition.mode">
<Suspense suspensible>
<JView
:key="route.path"
:key="route.name"
:comp="Component" />
</Suspense>
</JTransition>
Expand Down Expand Up @@ -72,7 +72,7 @@
return ServerLayout as VueComponent;
}
default: {
return DefaultLayout;

Check failure on line 75 in frontend/src/App.vue

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

Unsafe return of a value of type error
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Layout/AppBar/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
<SearchField />
<VSpacer />
<AppBarButtonLayout
v-if="!remote.socket.isConnected.value || !isConnectedToServer"
:color="!remote.socket.isConnected.value ? 'yellow' : 'red'">
v-hide="remote.socket.isConnected.value && isConnectedToServer"
:color="isConnectedToServer ? 'yellow' : 'red'">
<template #icon>
<VIcon>
<IMdiNetworkOffOutline />
Expand Down
53 changes: 32 additions & 21 deletions frontend/src/composables/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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, getCurrentScope, isRef, shallowRef, toValue, unref, watch, type ComputedRef, type Ref } from 'vue';
import { computed, effectScope, getCurrentScope, isRef, shallowRef, toValue, unref, watch, type ComputedRef, type Ref } from 'vue';
import { until } from '@vueuse/core';
import { useLoading } from '@/composables/use-loading';
import { useSnackbar } from '@/composables/use-snackbar';
Expand All @@ -11,6 +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';

/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
type OmittedKeys = 'fields' | 'userId' | 'enableImages' | 'enableTotalRecordCount' | 'enableImageTypes';
Expand Down Expand Up @@ -191,6 +192,7 @@ function _sharedInternalLogic<T extends Record<K, (...args: any[]) => any>, K ex
const stringArgs = computed(() => {
return JSON.stringify(argsRef.value);
});

/**
* TODO: Check why previous returns unknown by default without the type annotation
*/
Expand Down Expand Up @@ -275,29 +277,38 @@ function _sharedInternalLogic<T extends Record<K, (...args: any[]) => any>, K ex
argsRef.value = normalizeArgs();

if (getCurrentScope() !== undefined) {
if (args.length) {
watch(args, async (_newVal, oldVal) => {
const normalizedArgs = normalizeArgs();

/**
* Does a deep comparison to avoid useless double requests
*/
if (!normalizedArgs.every((a, index) => deepEqual(a, toValue(oldVal[index])))) {
argsRef.value = normalizedArgs;
await runNormally();
}
});
}
const handleArgsChange = async (_: typeof args, old: typeof args | undefined): Promise<void> => {
const normalizedArgs = normalizeArgs();

/**
* Does a deep comparison to avoid useless double requests
*/
if (old && !normalizedArgs.every((a, index) => deepEqual(a, toValue(old[index])))) {
argsRef.value = normalizedArgs;
await runNormally();
}
};
const scope = effectScope();

watch(isConnectedToServer, runWithRetry);
scope.run(() => {
if (args.length) {
watch(args, handleArgsChange);
}

if (isRef(api)) {
watch(api, runNormally);
}
watch(isConnectedToServer, runWithRetry);

if (isRef(methodName)) {
watch(methodName, runNormally);
}
if (isRef(api)) {
watch(api, runNormally);
}

if (isRef(methodName)) {
watch(methodName, runNormally);
}
});

watch(() => router.currentRoute.value.name, () => scope.stop(),
{ once: true, flush: 'sync' }
);
}

/**
Expand Down
16 changes: 6 additions & 10 deletions frontend/src/pages/item/[itemId].vue
Original file line number Diff line number Diff line change
Expand Up @@ -321,16 +321,12 @@ const { data: relatedItems } = await useBaseItem(getLibraryApi, 'getSimilarItems
itemId: route.params.itemId,
limit: 12
}));
const { data: currentSeries } = await useBaseItem(getUserLibraryApi, 'getItem')(
() => ({
itemId: item.value.SeriesId ?? ''
})
);
const { data: childItems } = await useBaseItem(getItemsApi, 'getItems')(
() => ({
parentId: item.value.Id
})
);
const { data: currentSeries } = await useBaseItem(getUserLibraryApi, 'getItem')(() => ({
itemId: item.value.SeriesId ?? ''
}));
const { data: childItems } = await useBaseItem(getItemsApi, 'getItems')(() => ({
parentId: item.value.Id
}));

const selectedSource = ref<MediaSourceInfo>();
const currentVideoTrack = ref<number>();
Expand Down
28 changes: 11 additions & 17 deletions frontend/src/pages/library/[itemId].vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
<VChip
size="small"
class="ma-2 hidden-sm-and-down">
<template v-if="!fullQueryIsCached">
<template v-if="loading && items.length === lazyLoadLimit && initialId === route.params.itemId">
{{ t('lazyLoading', { value: items.length }) }}
</template>
<VProgressCircular
v-else-if="loading"
indeterminate
width="2"
size="16" />
<template v-else>
{{ items?.length ?? 0 }}
{{ items.length ?? 0 }}
</template>
</VChip>
<VDivider
Expand Down Expand Up @@ -63,7 +68,7 @@

<script setup lang="ts">
import {
BaseItemKind, SortOrder, type BaseItemDto
BaseItemKind, SortOrder
} from '@jellyfin/sdk/lib/generated-client';
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api';
Expand All @@ -74,7 +79,6 @@
import { computed, onBeforeMount, ref, shallowRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { apiStore } from '@/store/api';
import { methodsAsObject, useBaseItem } from '@/composables/apis';
import type { Filters } from '@/components/Buttons/FilterButton.vue';
import { useItemPageTitle } from '@/composables/page-title';
Expand All @@ -83,6 +87,7 @@
const route = useRoute('/library/[itemId]');

const lazyLoadLimit = 50;
const initialId = route.params.itemId;
const COLLECTION_TYPES_MAPPINGS: Record<string, BaseItemKind> = {
tvshows: BaseItemKind.Series,
movies: BaseItemKind.Movie,
Expand All @@ -95,7 +100,6 @@
const sortBy = shallowRef<string>();
const sortAscending = shallowRef(true);
const queryLimit = shallowRef<number | undefined>(lazyLoadLimit);
const lazyLoadIds = shallowRef<BaseItemDto['Id'][]>([]);
const filters = ref<Filters>({
status: [],
features: [],
Expand Down Expand Up @@ -158,7 +162,6 @@
'Person',
'Genre',
'MusicGenre',
'MusicGenre',
'Studio'
].includes(viewType.value)
);
Expand Down Expand Up @@ -201,7 +204,7 @@
/**
* TODO: Improve the type situation of this statement
*/
const { loading, data: queryItems } = await useBaseItem(api, method)(() => ({
const { loading, data: items } = await useBaseItem(api, method)(() => ({
parentId: parentId.value,
personTypes: viewType.value === 'Person' ? ['Actor'] : undefined,
includeItemTypes: viewType.value ? [viewType.value] : undefined,
Expand All @@ -220,36 +223,27 @@
isHd: filters.value.types.includes('isHD') || undefined,
is4K: filters.value.types.includes('is4K') || undefined,
is3D: filters.value.types.includes('is3D') || undefined,
startIndex: queryLimit.value ? undefined : lazyLoadLimit,
limit: queryLimit.value
}));

/**
* The queryItems for the 2nd request will return the items from (lazyloadLimit, n],
* so checking if just the first matches is a good solution
*/
const fullQueryIsCached = computed(() => loading.value ? !queryLimit.value && queryItems.value[0].Id !== lazyLoadIds.value[0] : true);
const items = computed(() => fullQueryIsCached.value ? [...(apiStore.getItemsById(lazyLoadIds.value) as BaseItemDto[]), ...queryItems.value] : queryItems.value);

useItemPageTitle(library);

/**
* We fetch the 1st 100 items and, after mount, we fetch the rest.
* We fetch the 1st 50 items and, after mount, we fetch the rest.
*/
onBeforeMount(() => {
lazyLoadIds.value = queryItems.value.map(i => i.Id);
queryLimit.value = undefined;
});
</script>

<style scoped>
.empty-card-container {

Check warning on line 240 in frontend/src/pages/library/[itemId].vue

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

The selector `.empty-card-container` is unused
max-height: 90vh;
overflow: hidden;
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
}

.empty-message {

Check warning on line 246 in frontend/src/pages/library/[itemId].vue

View workflow job for this annotation

GitHub Actions / Quality checks 👌🧪 / Run lint 🕵️‍♂️

The selector `.empty-message` is unused
position: absolute;
top: 50%;
left: 50%;
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/plugins/remote/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* It also sets the header and base URL for our axios instance
*/
import type { Api } from '@jellyfin/sdk';
import { watchEffect } from 'vue';
import { watchSyncEffect } from 'vue';
import RemotePluginAuthInstance from '../auth';
import RemotePluginAxiosInstance from '../axios';
import SDK, { useOneTimeAPI } from './sdk-utils';
Expand All @@ -21,7 +21,7 @@ class RemotePluginSDK {
/**
* Configure app's axios instance to perform requests to the given Jellyfin server.
*/
watchEffect(() => {
watchSyncEffect(() => {
const server = auth.currentServer;
const accessToken = auth.currentUserToken;

Expand Down
57 changes: 26 additions & 31 deletions frontend/src/plugins/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { watch } from 'vue';
import { watchSyncEffect } from 'vue';
import {
createRouter,
createWebHashHistory,
Expand Down Expand Up @@ -64,35 +64,30 @@ router.back = (): ReturnType<typeof router.back> => {
/**
* Re-run the middleware pipeline when the user logs out or state is cleared
*/
watch(
[
(): typeof remote.auth.currentUser => remote.auth.currentUser,
(): typeof remote.auth.servers => remote.auth.servers
],
async () => {
if (!remote.auth.currentUser && remote.auth.servers.length <= 0) {
/**
* We run the redirect to /server/add as it's the first page in the login flow
*
* In case the whole localStorage is gone at runtime, if we're at the login
* page, redirecting to /server/login wouldn't work, as we're in that same page.
* /server/add doesn't depend on the state of localStorage, so it's always safe to
* redirect there and leave the middleware take care of the final destination
* (when servers are already available, for example)
*/
await router.replace('/server/add');
} else if (
!remote.auth.currentUser
&& remote.auth.servers.length
&& remote.auth.currentServer
) {
await (remote.auth.currentServer.StartupWizardCompleted ? router.replace('/server/login') : router.replace('/wizard'));
} else if (
!remote.auth.currentUser
&& remote.auth.servers.length
&& !remote.auth.currentServer
) {
await router.replace('/server/select');
}
watchSyncEffect(() => {
if (!remote.auth.currentUser && remote.auth.servers.length <= 0) {
/**
* We run the redirect to /server/add as it's the first page in the login flow
*
* In case the whole localStorage is gone at runtime, if we're at the login
* page, redirecting to /server/login wouldn't work, as we're in that same page.
* /server/add doesn't depend on the state of localStorage, so it's always safe to
* redirect there and leave the middleware take care of the final destination
* (when servers are already available, for example)
*/
void router.replace('/server/add');
} else if (
!remote.auth.currentUser
&& remote.auth.servers.length > 0
&& remote.auth.currentServer
) {
void (remote.auth.currentServer.StartupWizardCompleted ? router.replace('/server/login') : router.replace('/wizard'));
} else if (
!remote.auth.currentUser
&& remote.auth.servers.length > 0
&& !remote.auth.currentServer
) {
void router.replace('/server/select');
}
}
);
13 changes: 10 additions & 3 deletions frontend/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,16 @@ export const isSlow = useMediaQuery('(update:slow)');
*/
const network = useNetwork();
export const isConnectedToServer = computedAsync(async () => {
if (network.isSupported.value && network.isOnline.value) {
return true;
} else if (!isNil(remote.auth.currentServer) || !remote.socket.isConnected.value) {
/**
* These can't be merged inside the if statement as they must be picked up by watchEffect, and the OR operation
* stops evaluating in the first await tick as soon as the first truthy value is found.
*
* See https://vuejs.org/guide/essentials/watchers.html#watcheffect
*/
const socket = remote.socket.isConnected.value;
const networkAPI = network.isOnline.value;

if (!isNil(remote.auth.currentServer) || !socket || !networkAPI) {
try {
await remote.sdk.newUserApi(getSystemApi).getPingSystem();

Expand Down
Loading