mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-21 21:16:09 +02:00
fix visual bug
This commit is contained in:
+1
-1
@@ -82,7 +82,7 @@
|
||||
"test:settings-search": "node --experimental-strip-types --test src/components/search/settingsSearchRoutes.test.mts",
|
||||
"test:now-playing-layout": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/player/nowPlayingLayout.test.mts src/components/player/nowPlayingPreferences.test.mts src/components/player/nowPlayingDismiss.test.mts src/playback/playbackTargetPresentation.test.mts",
|
||||
"test:memory-lifecycle": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/delayedPresence.test.mts scripts/android-memory-profile.test.mjs",
|
||||
"test:ui-navigation": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/stores/playerPresence.test.mts src/navigation/tabsAnchor.test.mts src/navigation/libraryDetailBack.test.mts src/navigation/tabTransition.test.mts",
|
||||
"test:ui-navigation": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/stores/playerPresence.test.mts src/navigation/tabsAnchor.test.mts src/navigation/libraryDetailBack.test.mts src/navigation/homeLibraryNavigation.test.mts src/navigation/tabTransition.test.mts",
|
||||
"test:haptics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lib/haptics.test.mts",
|
||||
"test:app-dialog": "node --experimental-strip-types --test src/components/dialogs/dialogQueue.test.mts",
|
||||
"test:home-greeting": "node --experimental-strip-types --test src/home/homeGreeting.test.mts",
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
HOME_GREETING_ROTATION_MS,
|
||||
type HomeGreetingTextMode,
|
||||
} from '@/home/homeGreeting';
|
||||
import { useHomeLibraryNavigation } from '@/navigation/useHomeLibraryNavigation';
|
||||
import type { Album, Artist, DbTrack } from '@/types/library';
|
||||
|
||||
const RECENT_ALBUM_LIMIT = 8;
|
||||
@@ -514,6 +515,7 @@ function EmptyHomeCard({
|
||||
export default function HomeScreen() {
|
||||
const styles = useStyles();
|
||||
const router = useRouter();
|
||||
const openLibrary = useHomeLibraryNavigation();
|
||||
const totalTrackCount = useLibraryStore((s) => s.totalTrackCount);
|
||||
const albums = useLibraryStore((s) => s.homeAlbums);
|
||||
const artists = useLibraryStore((s) => s.homeArtists);
|
||||
@@ -589,17 +591,11 @@ export default function HomeScreen() {
|
||||
const canExpandRecentTracks = recentlyPlayedTracks.length > RECENT_TRACK_LIMIT;
|
||||
|
||||
const openAlbum = (album: Album) => {
|
||||
router.push({
|
||||
pathname: '/library/album/[key]',
|
||||
params: { key: album.identity_key },
|
||||
});
|
||||
openLibrary({ kind: 'album', key: album.identity_key });
|
||||
};
|
||||
|
||||
const openArtist = (artist: Artist) => {
|
||||
router.push({
|
||||
pathname: '/library/artist/[name]',
|
||||
params: { name: artist.artist },
|
||||
});
|
||||
openLibrary({ kind: 'artist', name: artist.artist });
|
||||
};
|
||||
|
||||
const playRecentlyPlayed = (list: DbTrack[], index = 0) => {
|
||||
@@ -735,7 +731,7 @@ export default function HomeScreen() {
|
||||
trackCount={favoriteTracks.length}
|
||||
coverHash={favoriteTracks[0]?.artwork_hash ?? null}
|
||||
pinned
|
||||
onPress={() => router.push('/library/playlist/favorites')}
|
||||
onPress={() => openLibrary({ kind: 'playlist', id: 'favorites' })}
|
||||
/>
|
||||
) : null}
|
||||
{homePlaylists.map((playlist) => (
|
||||
@@ -745,7 +741,7 @@ export default function HomeScreen() {
|
||||
trackCount={playlist.track_count}
|
||||
missingCount={playlist.missing_track_count}
|
||||
coverHash={playlist.auto_cover_hash}
|
||||
onPress={() => router.push(`/library/playlist/${playlist.id}`)}
|
||||
onPress={() => openLibrary({ kind: 'playlist', id: playlist.id })}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import {
|
||||
hasHomeLibraryHandoff,
|
||||
withoutHomeLibraryHandoff,
|
||||
} from '@/navigation/homeLibraryNavigation';
|
||||
import { useColors } from '@/theme/themed';
|
||||
|
||||
/**
|
||||
@@ -19,10 +23,23 @@ export default function LibraryLayout() {
|
||||
const colors = useColors();
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
screenOptions={({ route }) => ({
|
||||
headerShown: false,
|
||||
contentStyle: { backgroundColor: colors.bgPrimary },
|
||||
}}
|
||||
// The tab cross-fade is the only transition needed for a Home handoff.
|
||||
// Suppressing the nested push prevents the replaced detail from being
|
||||
// visible underneath the incoming screen.
|
||||
animation: hasHomeLibraryHandoff(route.params) ? 'none' : undefined,
|
||||
})}
|
||||
screenListeners={({ route, navigation }) => ({
|
||||
transitionEnd: (event) => {
|
||||
if (event.data.closing || !hasHomeLibraryHandoff(route.params)) return;
|
||||
// Re-enable ordinary Library push/pop animations after this arrival.
|
||||
navigation.replaceParams(
|
||||
withoutHomeLibraryHandoff(route.params as Record<string, unknown> | undefined)
|
||||
);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
HOME_LIBRARY_HANDOFF_PARAM,
|
||||
buildHomeLibraryResetAction,
|
||||
hasHomeLibraryHandoff,
|
||||
homeLibraryDetailRoute,
|
||||
homeLibraryTargetHref,
|
||||
withoutHomeLibraryHandoff,
|
||||
type NavigationStateLike,
|
||||
} from './homeLibraryNavigation.ts';
|
||||
import { libraryParentLabel, parentRoute } from './libraryDetailBack.ts';
|
||||
|
||||
function tabsState(libraryState?: NavigationStateLike): NavigationStateLike {
|
||||
return {
|
||||
stale: false,
|
||||
type: 'tab',
|
||||
key: 'tabs-1',
|
||||
index: 0,
|
||||
routeNames: ['index', 'library', 'eq', 'settings'],
|
||||
history: [{ type: 'route', key: 'index-1' }],
|
||||
preloadedRouteKeys: ['library-1', 'eq-1'],
|
||||
routes: [
|
||||
{ key: 'index-1', name: 'index', state: { key: 'home-stack', routes: [] } },
|
||||
{
|
||||
key: 'library-1',
|
||||
name: 'library',
|
||||
params: { screen: 'album/[key]', params: { key: 'old' } },
|
||||
state: libraryState,
|
||||
},
|
||||
{ key: 'eq-1', name: 'eq', params: { retained: true } },
|
||||
{ key: 'settings-1', name: 'settings' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test('atomically selects Library and replaces stale detail history with the requested album', () => {
|
||||
const root = { key: 'library-index-1', name: 'index', params: { retained: true } };
|
||||
const oldAlbum = { key: 'album-old', name: 'album/[key]', params: { key: 'old' } };
|
||||
const oldArtist = { key: 'artist-old', name: 'artist/[name]', params: { name: 'Old' } };
|
||||
const state = tabsState({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'library-stack',
|
||||
index: 2,
|
||||
routeNames: ['index', 'album/[key]', 'artist/[name]', 'playlist/[id]'],
|
||||
preloadedRoutes: [],
|
||||
routes: [root, oldAlbum, oldArtist],
|
||||
});
|
||||
|
||||
const action = buildHomeLibraryResetAction(state, { kind: 'album', key: 'new/key' });
|
||||
assert.ok(action);
|
||||
assert.equal(action.type, 'RESET');
|
||||
assert.equal(action.target, 'tabs-1');
|
||||
assert.equal(action.payload.index, 1);
|
||||
assert.deepEqual(action.payload.history, [
|
||||
{ type: 'route', key: 'index-1' },
|
||||
{ type: 'route', key: 'library-1' },
|
||||
]);
|
||||
assert.deepEqual(action.payload.preloadedRouteKeys, ['eq-1']);
|
||||
|
||||
const library = action.payload.routes[1];
|
||||
assert.equal(library.params, undefined);
|
||||
assert.equal(library.state?.stale, false);
|
||||
assert.equal(library.state?.type, 'stack');
|
||||
assert.equal(library.state?.key, 'library-stack');
|
||||
assert.equal(library.state?.index, 1);
|
||||
assert.equal(library.state?.routes[0], root);
|
||||
assert.match(String(library.state?.routes[1].key), /^home-library-handoff-\d+-detail$/);
|
||||
assert.equal(library.state?.routes[1].name, 'album/[key]');
|
||||
assert.deepEqual(library.state?.routes[1].params, {
|
||||
key: 'new/key',
|
||||
[HOME_LIBRARY_HANDOFF_PARAM]: true,
|
||||
});
|
||||
assert.deepEqual(library.state?.preloadedRoutes, []);
|
||||
assert.equal(libraryParentLabel(parentRoute(library.state)), 'Library');
|
||||
});
|
||||
|
||||
test('supports artist, regular playlist, and Favorites destinations', () => {
|
||||
assert.deepEqual(homeLibraryDetailRoute({ kind: 'artist', name: 'AC/DC' }), {
|
||||
name: 'artist/[name]',
|
||||
params: { name: 'AC/DC', [HOME_LIBRARY_HANDOFF_PARAM]: true },
|
||||
});
|
||||
assert.deepEqual(homeLibraryDetailRoute({ kind: 'playlist', id: 42 }), {
|
||||
name: 'playlist/[id]',
|
||||
params: { id: '42', [HOME_LIBRARY_HANDOFF_PARAM]: true },
|
||||
});
|
||||
assert.deepEqual(homeLibraryDetailRoute({ kind: 'playlist', id: 'favorites' }), {
|
||||
name: 'playlist/[id]',
|
||||
params: { id: 'favorites', [HOME_LIBRARY_HANDOFF_PARAM]: true },
|
||||
});
|
||||
assert.equal(homeLibraryTargetHref({ kind: 'album', key: 'a/b' }), '/library/album/a%2Fb');
|
||||
assert.equal(homeLibraryTargetHref({ kind: 'artist', name: 'AC/DC' }), '/library/artist/AC%2FDC');
|
||||
assert.equal(
|
||||
homeLibraryTargetHref({ kind: 'playlist', id: 'favorites' }),
|
||||
'/library/playlist/favorites'
|
||||
);
|
||||
});
|
||||
|
||||
test('builds a fully initialized anchored Library stack before that tab has mounted', () => {
|
||||
const state = tabsState();
|
||||
const action = buildHomeLibraryResetAction(state, { kind: 'playlist', id: 7 });
|
||||
assert.ok(action);
|
||||
const libraryState = action.payload.routes[1].state;
|
||||
assert.equal(libraryState?.stale, false);
|
||||
assert.equal(libraryState?.type, 'stack');
|
||||
assert.equal(libraryState?.index, 1);
|
||||
assert.deepEqual(libraryState?.routeNames, ['index', 'playlist/[id]']);
|
||||
assert.match(String(libraryState?.key), /^home-library-handoff-\d+-stack$/);
|
||||
assert.match(String(libraryState?.routes[0].key), /^home-library-handoff-\d+-root$/);
|
||||
assert.match(String(libraryState?.routes[1].key), /^home-library-handoff-\d+-detail$/);
|
||||
assert.deepEqual(libraryState?.routes[1].params, {
|
||||
id: '7',
|
||||
[HOME_LIBRARY_HANDOFF_PARAM]: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves every unrelated tab and leaves the input state immutable', () => {
|
||||
const state = tabsState({
|
||||
key: 'library-stack',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'library-index', name: 'index' },
|
||||
{ key: 'old-playlist', name: 'playlist/[id]', params: { id: '9' } },
|
||||
],
|
||||
});
|
||||
const before = structuredClone(state);
|
||||
const action = buildHomeLibraryResetAction(state, { kind: 'artist', name: 'Björk' });
|
||||
assert.ok(action);
|
||||
assert.deepEqual(state, before);
|
||||
assert.equal(action.payload.routes[0], state.routes[0]);
|
||||
assert.equal(action.payload.routes[2], state.routes[2]);
|
||||
assert.equal(action.payload.routes[3], state.routes[3]);
|
||||
});
|
||||
|
||||
test('fails safely for missing navigator identity or Library route', () => {
|
||||
assert.equal(buildHomeLibraryResetAction(undefined, { kind: 'album', key: 'a' }), null);
|
||||
assert.equal(
|
||||
buildHomeLibraryResetAction({ key: '', routes: [] }, { kind: 'album', key: 'a' }),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
buildHomeLibraryResetAction(
|
||||
{ key: 'tabs', index: 0, routes: [{ name: 'index' }] },
|
||||
{ kind: 'album', key: 'a' }
|
||||
),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
buildHomeLibraryResetAction(
|
||||
{ key: 'tabs', index: 0, routes: [{ key: 'home', name: 'index' }, { name: 'library' }] },
|
||||
{ kind: 'album', key: 'a' }
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('the Home handoff transition marker is one-shot data', () => {
|
||||
const params = { key: 'album', [HOME_LIBRARY_HANDOFF_PARAM]: true };
|
||||
assert.equal(hasHomeLibraryHandoff(params), true);
|
||||
assert.deepEqual(withoutHomeLibraryHandoff(params), { key: 'album' });
|
||||
assert.equal(hasHomeLibraryHandoff({ [HOME_LIBRARY_HANDOFF_PARAM]: false }), false);
|
||||
assert.deepEqual(withoutHomeLibraryHandoff({ key: 'album' }), { key: 'album' });
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* State surgery for Home -> Library detail handoffs.
|
||||
*
|
||||
* Expo Router normally focuses the retained Library tab before its nested
|
||||
* navigation is applied. If that tab was left on another detail, the stale
|
||||
* screen participates in the tab transition. Replacing the tab state and the
|
||||
* Library child state in one RESET action removes that intermediate state.
|
||||
*
|
||||
* Kept free of framework imports so the state transformation is unit-testable.
|
||||
*/
|
||||
|
||||
export const HOME_LIBRARY_HANDOFF_PARAM = '__astra_home_library_handoff';
|
||||
|
||||
export type HomeLibraryTarget =
|
||||
| { kind: 'album'; key: string }
|
||||
| { kind: 'artist'; name: string }
|
||||
| { kind: 'playlist'; id: string | number };
|
||||
|
||||
export interface NavigationRouteLike {
|
||||
key?: string;
|
||||
name: string;
|
||||
params?: Record<string, unknown>;
|
||||
state?: NavigationStateLike;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface NavigationStateLike {
|
||||
key?: string;
|
||||
index?: number;
|
||||
routes: NavigationRouteLike[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface HomeLibraryResetAction {
|
||||
type: 'RESET';
|
||||
target: string;
|
||||
payload: NavigationStateLike;
|
||||
}
|
||||
|
||||
const LIBRARY_TAB_ROUTE = 'library';
|
||||
const LIBRARY_ROOT_ROUTE = 'index';
|
||||
let handoffSequence = 0;
|
||||
|
||||
function nextHandoffKeyPrefix(): string {
|
||||
handoffSequence += 1;
|
||||
return `home-library-handoff-${handoffSequence}`;
|
||||
}
|
||||
|
||||
export function homeLibraryDetailRoute(target: HomeLibraryTarget): NavigationRouteLike {
|
||||
const handoff = { [HOME_LIBRARY_HANDOFF_PARAM]: true };
|
||||
|
||||
switch (target.kind) {
|
||||
case 'album':
|
||||
return {
|
||||
name: 'album/[key]',
|
||||
params: { key: target.key, ...handoff },
|
||||
};
|
||||
case 'artist':
|
||||
return {
|
||||
name: 'artist/[name]',
|
||||
params: { name: target.name, ...handoff },
|
||||
};
|
||||
case 'playlist':
|
||||
return {
|
||||
name: 'playlist/[id]',
|
||||
params: { id: String(target.id), ...handoff },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function homeLibraryTargetHref(target: HomeLibraryTarget): string {
|
||||
switch (target.kind) {
|
||||
case 'album':
|
||||
return `/library/album/${encodeURIComponent(target.key)}`;
|
||||
case 'artist':
|
||||
return `/library/artist/${encodeURIComponent(target.name)}`;
|
||||
case 'playlist':
|
||||
return `/library/playlist/${encodeURIComponent(String(target.id))}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a single action that both selects Library and replaces its nested
|
||||
* history with [Library root, requested detail].
|
||||
*
|
||||
* The child state is fully initialized before the Library tab receives focus.
|
||||
* Supplying a stale/partial state here lets the nested navigator briefly render
|
||||
* its initial route while it rehydrates, which is the Library-list flash this
|
||||
* helper exists to prevent.
|
||||
*/
|
||||
export function buildHomeLibraryResetAction(
|
||||
tabsState: NavigationStateLike | undefined,
|
||||
target: HomeLibraryTarget
|
||||
): HomeLibraryResetAction | null {
|
||||
if (
|
||||
!tabsState ||
|
||||
typeof tabsState.key !== 'string' ||
|
||||
tabsState.key.length === 0 ||
|
||||
!Array.isArray(tabsState.routes)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const libraryIndex = tabsState.routes.findIndex((route) => route.name === LIBRARY_TAB_ROUTE);
|
||||
if (libraryIndex < 0) return null;
|
||||
|
||||
const libraryRoute = tabsState.routes[libraryIndex];
|
||||
const initialTabRoute = tabsState.routes[0];
|
||||
if (
|
||||
typeof libraryRoute.key !== 'string' ||
|
||||
typeof initialTabRoute?.key !== 'string'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const existingLibraryState =
|
||||
libraryRoute.state && Array.isArray(libraryRoute.state.routes)
|
||||
? libraryRoute.state
|
||||
: undefined;
|
||||
const existingRoot = existingLibraryState?.routes.find(
|
||||
(route) => route.name === LIBRARY_ROOT_ROUTE
|
||||
);
|
||||
const keyPrefix = nextHandoffKeyPrefix();
|
||||
const rootRoute =
|
||||
existingRoot && typeof existingRoot.key === 'string'
|
||||
? existingRoot
|
||||
: {
|
||||
...(existingRoot ?? {}),
|
||||
key: `${keyPrefix}-root`,
|
||||
name: LIBRARY_ROOT_ROUTE,
|
||||
};
|
||||
const detailRoute = {
|
||||
...homeLibraryDetailRoute(target),
|
||||
key: `${keyPrefix}-detail`,
|
||||
};
|
||||
const existingRouteNames = existingLibraryState?.routeNames;
|
||||
const routeNames =
|
||||
Array.isArray(existingRouteNames) &&
|
||||
existingRouteNames.every((name): name is string => typeof name === 'string')
|
||||
? existingRouteNames
|
||||
: [LIBRARY_ROOT_ROUTE, detailRoute.name];
|
||||
|
||||
const nextLibraryState: NavigationStateLike = {
|
||||
...(existingLibraryState ?? {}),
|
||||
stale: false,
|
||||
type:
|
||||
typeof existingLibraryState?.type === 'string'
|
||||
? existingLibraryState.type
|
||||
: 'stack',
|
||||
key:
|
||||
typeof existingLibraryState?.key === 'string'
|
||||
? existingLibraryState.key
|
||||
: `${keyPrefix}-stack`,
|
||||
index: 1,
|
||||
routeNames,
|
||||
preloadedRoutes: [],
|
||||
routes: [rootRoute, detailRoute],
|
||||
};
|
||||
|
||||
const nextLibraryRoute: NavigationRouteLike = {
|
||||
...libraryRoute,
|
||||
// Clear nested-navigation command params left by earlier href navigation;
|
||||
// the explicit child state above is now the sole source of truth.
|
||||
params: undefined,
|
||||
state: nextLibraryState,
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'RESET',
|
||||
target: tabsState.key,
|
||||
payload: {
|
||||
...tabsState,
|
||||
index: libraryIndex,
|
||||
// Tabs use firstRoute back behaviour, so RESET must update the history
|
||||
// invariant that an ordinary navigate-to-Library action would establish.
|
||||
history:
|
||||
libraryIndex === 0
|
||||
? [{ type: 'route', key: libraryRoute.key }]
|
||||
: [
|
||||
{ type: 'route', key: initialTabRoute.key },
|
||||
{ type: 'route', key: libraryRoute.key },
|
||||
],
|
||||
...(Array.isArray(tabsState.preloadedRouteKeys)
|
||||
? {
|
||||
preloadedRouteKeys: tabsState.preloadedRouteKeys.filter(
|
||||
(key) => key !== libraryRoute.key
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
routes: tabsState.routes.map((route, index) =>
|
||||
index === libraryIndex ? nextLibraryRoute : route
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function hasHomeLibraryHandoff(params: unknown): boolean {
|
||||
return (
|
||||
typeof params === 'object' &&
|
||||
params !== null &&
|
||||
HOME_LIBRARY_HANDOFF_PARAM in params &&
|
||||
(params as Record<string, unknown>)[HOME_LIBRARY_HANDOFF_PARAM] === true
|
||||
);
|
||||
}
|
||||
|
||||
export function withoutHomeLibraryHandoff(
|
||||
params: Record<string, unknown> | undefined
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!params || !hasHomeLibraryHandoff(params)) return params;
|
||||
const next = { ...params };
|
||||
delete next[HOME_LIBRARY_HANDOFF_PARAM];
|
||||
return Object.keys(next).length > 0 ? next : undefined;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigation, useRouter } from 'expo-router';
|
||||
import {
|
||||
buildHomeLibraryResetAction,
|
||||
homeLibraryTargetHref,
|
||||
type HomeLibraryTarget,
|
||||
type NavigationStateLike,
|
||||
} from '@/navigation/homeLibraryNavigation';
|
||||
|
||||
/**
|
||||
* Opens a Library detail as a fresh navigation context from Home.
|
||||
*
|
||||
* A malformed/unready tab state falls back to ordinary href navigation so a
|
||||
* state-shape mismatch never turns a content card into a dead control.
|
||||
*/
|
||||
export function useHomeLibraryNavigation(): (target: HomeLibraryTarget) => void {
|
||||
const navigation = useNavigation();
|
||||
const router = useRouter();
|
||||
|
||||
return useCallback(
|
||||
(target: HomeLibraryTarget) => {
|
||||
const action = buildHomeLibraryResetAction(
|
||||
navigation.getState() as NavigationStateLike | undefined,
|
||||
target
|
||||
);
|
||||
if (!action) {
|
||||
router.push(homeLibraryTargetHref(target) as never, { withAnchor: true });
|
||||
return;
|
||||
}
|
||||
navigation.dispatch(action as never);
|
||||
},
|
||||
[navigation, router]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user