Fix search exit flow and history behavior
This commit is contained in:
+111
-15
@@ -35,8 +35,7 @@
|
|||||||
:mpc-be-connected="mpcBeConnected"
|
:mpc-be-connected="mpcBeConnected"
|
||||||
:nav-row="1"
|
:nav-row="1"
|
||||||
:position="headerPosition"
|
:position="headerPosition"
|
||||||
@search="searchQuery = $event"
|
@search="updateSearchQuery"
|
||||||
@clear-search="searchQuery = ''"
|
|
||||||
@go-back="goBack"
|
@go-back="goBack"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -199,6 +198,33 @@ const { getFocusState, restoreFocusState, focusAt, focusElement } = useKeyboardN
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
function normalizeSearchQuery(value: unknown): string {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return normalizeSearchQuery(value[0]);
|
||||||
|
}
|
||||||
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRouteSearchQuery() {
|
||||||
|
return normalizeSearchQuery(route.query.q);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowsePath() {
|
||||||
|
return route.meta.view === 'series' ? '/series' : '/movies';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowseViewFromPath(path: string): 'movies' | 'series' {
|
||||||
|
return path === '/series' ? 'series' : 'movies';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHistoryPath(value: string): string {
|
||||||
|
const hashIndex = value.indexOf('#');
|
||||||
|
const fromHash = hashIndex >= 0 ? value.slice(hashIndex + 1) : value;
|
||||||
|
const pathWithQuery = fromHash.startsWith('/') ? fromHash : `/${fromHash}`;
|
||||||
|
const queryIndex = pathWithQuery.indexOf('?');
|
||||||
|
return queryIndex >= 0 ? pathWithQuery.slice(0, queryIndex) : pathWithQuery;
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket-driven media index
|
// WebSocket-driven media index
|
||||||
const { mediaIndex, loading, error, connected: wsConnected, tasks } = useMediaWebSocket();
|
const { mediaIndex, loading, error, connected: wsConnected, tasks } = useMediaWebSocket();
|
||||||
|
|
||||||
@@ -209,6 +235,8 @@ const searchResults = ref<MediaItem[]>([]);
|
|||||||
const isSearching = ref(false);
|
const isSearching = ref(false);
|
||||||
const mpcBeConnected = ref(false);
|
const mpcBeConnected = ref(false);
|
||||||
const resumePositions = ref<Record<string, number>>({});
|
const resumePositions = ref<Record<string, number>>({});
|
||||||
|
const searchQuery = ref(getRouteSearchQuery());
|
||||||
|
const searchReturnPath = ref<string | null>(null);
|
||||||
const MPC_BE_OPENING_GRACE_MS = 4000;
|
const MPC_BE_OPENING_GRACE_MS = 4000;
|
||||||
const mpcBeOpeningUntil = ref(0);
|
const mpcBeOpeningUntil = ref(0);
|
||||||
let mpcBePollTimer: number | null = null;
|
let mpcBePollTimer: number | null = null;
|
||||||
@@ -340,6 +368,66 @@ function restoreFocusForPage(page: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreBrowseFocus(path: string) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
restoreFocusForPage(getBrowseViewFromPath(path));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}) {
|
||||||
|
const currentQuery = getRouteSearchQuery();
|
||||||
|
const targetPath = options.targetPath ?? getBrowsePath();
|
||||||
|
|
||||||
|
searchQuery.value = '';
|
||||||
|
|
||||||
|
if (!currentQuery && route.path === targetPath) {
|
||||||
|
searchReturnPath.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backPath = typeof window.history.state?.back === 'string'
|
||||||
|
? normalizeHistoryPath(window.history.state.back)
|
||||||
|
: '';
|
||||||
|
const canRestoreWithBack = options.preferBack !== false
|
||||||
|
&& !!currentQuery
|
||||||
|
&& searchReturnPath.value === targetPath
|
||||||
|
&& backPath === targetPath;
|
||||||
|
|
||||||
|
searchReturnPath.value = null;
|
||||||
|
|
||||||
|
if (canRestoreWithBack) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
void router.replace({ path: targetPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreBrowseFocus(targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSearchQuery(nextValue: string) {
|
||||||
|
const nextQuery = normalizeSearchQuery(nextValue);
|
||||||
|
const currentQuery = getRouteSearchQuery();
|
||||||
|
const browsePath = getBrowsePath();
|
||||||
|
|
||||||
|
if (!nextQuery) {
|
||||||
|
clearSearch({ preferBack: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchQuery.value = nextQuery;
|
||||||
|
|
||||||
|
if (!currentQuery) {
|
||||||
|
if (!route.params.id) {
|
||||||
|
saveFocusForPage(currentView.value);
|
||||||
|
}
|
||||||
|
searchReturnPath.value = browsePath;
|
||||||
|
void router.push({ path: browsePath, query: { q: nextQuery } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void router.replace({ path: browsePath, query: { q: nextQuery } });
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Escape key for navigation hierarchy
|
// Handle Escape key for navigation hierarchy
|
||||||
function handleEscapeKey(event: KeyboardEvent) {
|
function handleEscapeKey(event: KeyboardEvent) {
|
||||||
if (event.key !== 'Escape') return;
|
if (event.key !== 'Escape') return;
|
||||||
@@ -368,6 +456,12 @@ function handleEscapeKey(event: KeyboardEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getRouteSearchQuery()) {
|
||||||
|
event.preventDefault();
|
||||||
|
clearSearch({ preferBack: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// From series list -> movies list
|
// From series list -> movies list
|
||||||
if (path === '/series') {
|
if (path === '/series') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -377,14 +471,6 @@ function handleEscapeKey(event: KeyboardEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From search -> go back
|
|
||||||
if (path.startsWith('/search')) {
|
|
||||||
event.preventDefault();
|
|
||||||
router.push('/movies');
|
|
||||||
restoreFocusForPage('movies');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// From movies list -> do nothing (stop here)
|
// From movies list -> do nothing (stop here)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,9 +489,6 @@ onUnmounted(() => {
|
|||||||
stopMpcBePolling();
|
stopMpcBePolling();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search query stored in ref (not URL-based)
|
|
||||||
const searchQuery = ref('');
|
|
||||||
|
|
||||||
// Handle back navigation (Escape key or Back button)
|
// Handle back navigation (Escape key or Back button)
|
||||||
function goBack() {
|
function goBack() {
|
||||||
const path = route.path;
|
const path = route.path;
|
||||||
@@ -507,8 +590,7 @@ function closeDetail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleActorSearch(actorName: string) {
|
function handleActorSearch(actorName: string) {
|
||||||
searchQuery.value = actorName;
|
updateSearchQuery(actorName);
|
||||||
router.push(`/${currentView.value}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert raw data to MediaItem format
|
// Convert raw data to MediaItem format
|
||||||
@@ -1043,6 +1125,20 @@ watch([mediaIndex, currentView, searchQuery, selectedItem], ([index, view, query
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
const nextQuery = getRouteSearchQuery();
|
||||||
|
if (nextQuery !== searchQuery.value) {
|
||||||
|
searchQuery.value = nextQuery;
|
||||||
|
}
|
||||||
|
if (!nextQuery) {
|
||||||
|
searchReturnPath.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
watch(searchQuery, (query) => {
|
watch(searchQuery, (query) => {
|
||||||
if (searchTimeout) {
|
if (searchTimeout) {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
search: [string];
|
search: [string];
|
||||||
clearSearch: [];
|
|
||||||
goBack: [];
|
goBack: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -126,14 +125,12 @@ const isSearchActive = computed(() => {
|
|||||||
// Switch views on focus (no Enter required) - only in browse mode
|
// Switch views on focus (no Enter required) - only in browse mode
|
||||||
function switchToMovies() {
|
function switchToMovies() {
|
||||||
if (!isDetailPage.value && props.currentView !== 'movies') {
|
if (!isDetailPage.value && props.currentView !== 'movies') {
|
||||||
emit('clearSearch');
|
|
||||||
router.push('/movies');
|
router.push('/movies');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToSeries() {
|
function switchToSeries() {
|
||||||
if (!isDetailPage.value && props.currentView !== 'series') {
|
if (!isDetailPage.value && props.currentView !== 'series') {
|
||||||
emit('clearSearch');
|
|
||||||
router.push('/series');
|
router.push('/series');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,11 +147,7 @@ function handleSearchFocus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sync local search to parent
|
// Sync local search to parent
|
||||||
watch(localSearch, (val, prevVal) => {
|
watch(localSearch, (val) => {
|
||||||
// When user starts typing from detail page, exit detail and show search results.
|
|
||||||
if (isDetailPage.value && !prevVal && !!val.trim()) {
|
|
||||||
goToCategory();
|
|
||||||
}
|
|
||||||
emit('search', val);
|
emit('search', val);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user