From f892e311bb75f514aa1f01be229ed90a108325eb Mon Sep 17 00:00:00 2001 From: PikachuEXE Date: Wed, 4 Dec 2024 04:21:05 +0800 Subject: [PATCH] Make history page remember last query string & search limit (#5192) * $ Simplify boolean assignment, rename session storage key * * Make history page remember last query string & search limit only when going back * ! Fix restoring filtered history having unnecessary delay * * Make subscribed channels page remember last query string only when going back * * Make user playlists page remember last query string only when going back * * Make playlist page remember last query string only when going back * * Make channel page remember last query string only when going back * * Save more options * ! Fix strange outline on nav buttons * * Put `currentTab` value into proper place params instead of query * ! Fix search tab showing "0 results" before search done * - Remove useless file after merging dev * $ Change code style * * Put event listener back to mounted --- .../ChannelDetails/ChannelDetails.vue | 7 +- src/renderer/views/Channel/Channel.js | 86 +++++++++++++++--- src/renderer/views/Channel/Channel.vue | 7 +- src/renderer/views/History/History.js | 89 ++++++++++++++++--- src/renderer/views/History/History.vue | 5 +- src/renderer/views/Playlist/Playlist.js | 31 ++++++- src/renderer/views/Playlist/Playlist.vue | 2 +- .../SubscribedChannels/SubscribedChannels.vue | 62 +++++++++++-- .../views/UserPlaylists/UserPlaylists.js | 79 ++++++++++++++-- .../views/UserPlaylists/UserPlaylists.vue | 5 +- 10 files changed, 322 insertions(+), 51 deletions(-) diff --git a/src/renderer/components/ChannelDetails/ChannelDetails.vue b/src/renderer/components/ChannelDetails/ChannelDetails.vue index 28888464c8d3a..49d6fef6c887e 100644 --- a/src/renderer/components/ChannelDetails/ChannelDetails.vue +++ b/src/renderer/components/ChannelDetails/ChannelDetails.vue @@ -223,6 +223,7 @@ v-if="showSearchBar" ref="searchBar" :placeholder="$t('Channel.Search Channel')" + :value="query" :show-clear-text-button="true" class="channelSearch" :maxlength="255" @@ -291,7 +292,11 @@ const props = defineProps({ currentTab: { type: String, default: 'videos' - } + }, + query: { + type: String, + default: '' + }, }) const emit = defineEmits(['change-tab', 'search', 'subscribed']) diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js index 4f5582da29721..1b8ab33374866 100644 --- a/src/renderer/views/Channel/Channel.js +++ b/src/renderer/views/Channel/Channel.js @@ -45,6 +45,7 @@ import { parseLocalPlaylistVideo, parseChannelHomeTab } from '../../helpers/api/local' +import { isNavigationFailure, NavigationFailureType } from 'vue-router' export default defineComponent({ name: 'Channel', @@ -62,8 +63,10 @@ export default defineComponent({ }, data: function () { return { + skipRouteChangeWatcherOnce: false, isLoading: true, isElementListLoading: false, + isSearchTabLoading: false, currentTab: 'videos', id: '', /** @type {import('youtubei.js').YT.Channel|null} */ @@ -306,10 +309,22 @@ export default defineComponent({ return values }, + + isCurrentTabLoading() { + if (this.currentTab === 'search') { + return this.isSearchTabLoading + } + + return this.isElementListLoading + }, }, watch: { $route() { // react to route changes... + if (this.skipRouteChangeWatcherOnce) { + this.skipRouteChangeWatcherOnce = false + return + } this.isLoading = true if (this.$route.query.url) { @@ -366,8 +381,9 @@ export default defineComponent({ // Re-enable auto refresh on sort value change AFTER update done if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') { - this.getChannelInfoInvidious() - this.autoRefreshOnSortByChangeEnabled = true + this.getChannelInfoInvidious().finally(() => { + this.autoRefreshOnSortByChangeEnabled = true + }) } else { this.getChannelLocal().finally(() => { this.autoRefreshOnSortByChangeEnabled = true @@ -444,9 +460,9 @@ export default defineComponent({ } } }, - mounted: function () { + mounted: async function () { if (this.$route.query.url) { - this.resolveChannelUrl(this.$route.query.url, this.$route.params.currentTab) + await this.resolveChannelUrl(this.$route.query.url, this.$route.params.currentTab) return } @@ -462,13 +478,19 @@ export default defineComponent({ // Enable auto refresh on sort value change AFTER initial update done if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') { - this.getChannelInfoInvidious() - this.autoRefreshOnSortByChangeEnabled = true + await this.getChannelInfoInvidious().finally(() => { + this.autoRefreshOnSortByChangeEnabled = true + }) } else { - this.getChannelLocal().finally(() => { + await this.getChannelLocal().finally(() => { this.autoRefreshOnSortByChangeEnabled = true }) } + + const oldQuery = this.$route.query.searchQueryText ?? '' + if (oldQuery !== null && oldQuery !== '') { + this.newSearch(oldQuery) + } }, methods: { resolveChannelUrl: async function (url, tab = undefined) { @@ -1027,7 +1049,7 @@ export default defineComponent({ this.channelInstance = null const expectedId = this.id - invidiousGetChannelInfo(this.id).then((response) => { + return invidiousGetChannelInfo(this.id).then((response) => { if (expectedId !== this.id) { return } @@ -1889,13 +1911,14 @@ export default defineComponent({ const newTabNode = document.getElementById(`${tab}Tab`) this.currentTab = tab newTabNode?.focus() - this.showOutlines() + // Prevents outline shown in strange places + if (newTabNode != null) { this.showOutlines() } }, newSearch: function (query) { this.lastSearchQuery = query this.searchContinuationData = null - this.isElementListLoading = true + this.isSearchTabLoading = true this.searchPage = 1 this.searchResults = [] this.changeTab('search') @@ -1908,6 +1931,10 @@ export default defineComponent({ break } }, + newSearchWithStatePersist(query) { + this.saveStateInRouter(query) + this.newSearch(query) + }, searchChannelLocal: async function () { const isNewSearch = this.searchContinuationData === null @@ -1946,7 +1973,7 @@ export default defineComponent({ } this.searchContinuationData = result.has_continuation ? result : null - this.isElementListLoading = false + this.isSearchTabLoading = false } catch (err) { console.error(err) const errorMessage = this.$t('Local API Error (Click to copy)') @@ -1982,7 +2009,7 @@ export default defineComponent({ } else { this.searchResults = this.searchResults.concat(response) } - this.isElementListLoading = false + this.isSearchTabLoading = false this.searchPage++ }).catch((err) => { console.error(err) @@ -2026,6 +2053,41 @@ export default defineComponent({ }) }, + async saveStateInRouter(query) { + this.skipRouteChangeWatcherOnce = true + if (query === '') { + try { + await this.$router.replace({ path: `/channel/${this.id}` }) + } catch (failure) { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + } + return + } + + try { + await this.$router.replace({ + path: `/channel/${this.id}`, + params: { + currentTab: 'search', + }, + query: { + searchQueryText: query, + }, + }) + } catch (failure) { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + } + this.skipRouteChangeWatcherOnce = false + }, + getIconForSortPreference: (s) => getIconForSortPreference(s), ...mapActions([ diff --git a/src/renderer/views/Channel/Channel.vue b/src/renderer/views/Channel/Channel.vue index 7eb96b579d4bc..d8507169525dc 100644 --- a/src/renderer/views/Channel/Channel.vue +++ b/src/renderer/views/Channel/Channel.vue @@ -17,9 +17,10 @@ :is-subscribed="isSubscribed" :visible-tabs="tabInfoValues" :current-tab="currentTab" + :query="lastSearchQuery" class="card channelDetails" @change-tab="changeTab" - @search="newSearch" + @search="newSearchWithStatePersist" @subscribed="handleSubscription" />

{{ $t("Channel.Your search results have returned 0 results") }} diff --git a/src/renderer/views/History/History.js b/src/renderer/views/History/History.js index 118bbc0b62525..eddf1ba8023a6 100644 --- a/src/renderer/views/History/History.js +++ b/src/renderer/views/History/History.js @@ -1,4 +1,5 @@ import { defineComponent } from 'vue' +import { isNavigationFailure, NavigationFailureType } from 'vue-router' import debounce from 'lodash.debounce' import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtSelect from '../../components/ft-select/ft-select.vue' @@ -76,42 +77,68 @@ export default defineComponent({ } }, watch: { - query() { - this.searchDataLimit = 100 - this.filterHistoryAsync() - }, fullData() { this.filterHistory() }, doCaseSensitiveSearch() { this.filterHistory() - } + this.saveStateInRouter() + }, }, - mounted: function () { + created: function () { document.addEventListener('keydown', this.keyboardShortcutHandler) - const limit = sessionStorage.getItem('historyLimit') - if (limit !== null) { - this.dataLimit = limit + const oldDataLimit = sessionStorage.getItem('History/dataLimit') + if (oldDataLimit !== null) { + this.dataLimit = oldDataLimit } - this.activeData = this.fullData - - this.showLoadMoreButton = this.activeData.length < this.historyCacheSorted.length - this.filterHistoryDebounce = debounce(this.filterHistory, 500) + + const oldQuery = this.$route.query.searchQueryText ?? '' + if (oldQuery !== null && oldQuery !== '') { + // `handleQueryChange` must be called after `filterHistoryDebounce` assigned + this.handleQueryChange( + oldQuery, + { + limit: this.$route.query.searchDataLimit, + doCaseSensitiveSearch: this.$route.query.doCaseSensitiveSearch === 'true', + filterNow: true, + }, + ) + } else { + // Only display unfiltered data when no query used last time + this.filterHistory() + } }, beforeDestroy: function () { document.removeEventListener('keydown', this.keyboardShortcutHandler) }, methods: { + handleQueryChange(query, { limit = null, doCaseSensitiveSearch = null, filterNow = false } = {}) { + this.query = query + + const newLimit = limit ?? 100 + this.searchDataLimit = newLimit + const newDoCaseSensitiveSearch = doCaseSensitiveSearch ?? this.doCaseSensitiveSearch + this.doCaseSensitiveSearch = newDoCaseSensitiveSearch + + this.saveStateInRouter({ + query: query, + searchDataLimit: newLimit, + doCaseSensitiveSearch: newDoCaseSensitiveSearch, + }) + + filterNow ? this.filterHistory() : this.filterHistoryAsync() + }, + increaseLimit: function () { if (this.query !== '') { this.searchDataLimit += 100 this.filterHistory() } else { this.dataLimit += 100 - sessionStorage.setItem('historyLimit', this.dataLimit) + sessionStorage.setItem('History/dataLimit', this.dataLimit) } }, filterHistoryAsync: function() { @@ -135,6 +162,40 @@ export default defineComponent({ this.activeData = filteredQuery.length < this.searchDataLimit ? filteredQuery : filteredQuery.slice(0, this.searchDataLimit) this.showLoadMoreButton = this.activeData.length > this.searchDataLimit }, + + async saveStateInRouter({ query = this.query, searchDataLimit = this.searchDataLimit, doCaseSensitiveSearch = this.doCaseSensitiveSearch } = {}) { + if (query === '') { + try { + await this.$router.replace({ name: 'history' }) + } catch (failure) { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + } + return + } + + const routerQuery = { + searchQueryText: query, + searchDataLimit: searchDataLimit, + } + if (doCaseSensitiveSearch) { routerQuery.doCaseSensitiveSearch = 'true' } + try { + await this.$router.replace({ + name: 'history', + query: routerQuery, + }) + } catch (failure) { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + } + }, + keyboardShortcutHandler: function (event) { ctrlFHandler(event, this.$refs.searchBar) }, diff --git a/src/renderer/views/History/History.vue b/src/renderer/views/History/History.vue index 503834ce98aa8..d1f1064bbd65e 100644 --- a/src/renderer/views/History/History.vue +++ b/src/renderer/views/History/History.vue @@ -15,8 +15,9 @@ :placeholder="$t('History.Search bar placeholder')" :show-clear-text-button="true" :show-action-button="false" - @input="(input) => query = input" - @clear="query = ''" + :value="query" + @input="(input) => handleQueryChange(input)" + @clear="() => handleQueryChange('')" />

getIconForSortPreference(s), ...mapActions([ diff --git a/src/renderer/views/Playlist/Playlist.vue b/src/renderer/views/Playlist/Playlist.vue index c7e6efb8b30af..576f501900d02 100644 --- a/src/renderer/views/Playlist/Playlist.vue +++ b/src/renderer/views/Playlist/Playlist.vue @@ -37,7 +37,7 @@ class="playlistInfo" @enter-edit-mode="playlistInEditMode = true" @exit-edit-mode="playlistInEditMode = false" - @search-video-query-change="(v) => videoSearchQuery = v" + @search-video-query-change="(v) => handleVideoSearchQueryChange(v)" @prompt-open="promptOpen = true" @prompt-close="promptOpen = false" /> diff --git a/src/renderer/views/SubscribedChannels/SubscribedChannels.vue b/src/renderer/views/SubscribedChannels/SubscribedChannels.vue index d3c1adf6ab627..742eb469e86ab 100644 --- a/src/renderer/views/SubscribedChannels/SubscribedChannels.vue +++ b/src/renderer/views/SubscribedChannels/SubscribedChannels.vue @@ -6,12 +6,13 @@ v-show="subscribedChannels.length > 0" ref="searchBarChannels" :placeholder="$t('Channels.Search bar placeholder')" + :value="query" :show-clear-text-button="true" :show-action-button="false" :spellcheck="false" :maxlength="255" - @input="handleInput" - @clear="query = ''" + @input="(input) => handleQueryChange(input)" + @clear="() => handleQueryChange('')" /> import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue' +import { isNavigationFailure, NavigationFailureType } from 'vue-router' +import { useRoute, useRouter } from 'vue-router/composables' import FtCard from '../../components/ft-card/ft-card.vue' import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtInput from '../../components/ft-input/ft-input.vue' @@ -84,7 +87,10 @@ import { getLocalChannel, parseLocalChannelHeader } from '../../helpers/api/loca import { ctrlFHandler } from '../../helpers/utils' import { useI18n } from '../../composables/use-i18n-polyfill.js' import store from '../../store/index' +import debounce from 'lodash.debounce' +const route = useRoute() +const router = useRouter() const { locale } = useI18n() const re = { @@ -147,11 +153,6 @@ function getSubscription() { }) } -function handleInput(input) { - query.value = input - filterChannels() -} - function filterChannels() { if (query.value === '') { filteredChannels.value = [] @@ -165,6 +166,8 @@ function filterChannels() { }) } +const filterChannelsDebounce = debounce(filterChannels, 500) + function thumbnailURL(originalURL) { if (originalURL == null) { return null } let newURL = originalURL @@ -216,6 +219,38 @@ function updateThumbnail(channel) { } } +function handleQueryChange(val, filterNow = false) { + query.value = val + + saveStateInRouter(val) + + filterNow ? filterChannels() : filterChannelsDebounce() +} + +async function saveStateInRouter(query) { + if (query.value === '') { + await router.replace({ name: 'subscribedChannels' }).catch(failure => { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + }) + return + } + + await router.replace({ + name: 'subscribedChannels', + query: { searchQueryText: query }, + }).catch(failure => { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + }) +} + function keyboardShortcutHandler(event) { ctrlFHandler(event, searchBarChannels.value) } @@ -230,9 +265,20 @@ watch(activeSubscriptionList, () => { filterChannels() }) +// region created + +getSubscription() + +const oldQuery = route.query.searchQueryText ?? '' +if (oldQuery !== null && oldQuery !== '') { + // `handleQueryChange` must be called after `filterHistoryDebounce` assigned + handleQueryChange(oldQuery, true) +} + +// endregion created + onMounted(() => { document.addEventListener('keydown', keyboardShortcutHandler) - getSubscription() }) onBeforeUnmount(() => { diff --git a/src/renderer/views/UserPlaylists/UserPlaylists.js b/src/renderer/views/UserPlaylists/UserPlaylists.js index 053d0b50421b2..25deadd856017 100644 --- a/src/renderer/views/UserPlaylists/UserPlaylists.js +++ b/src/renderer/views/UserPlaylists/UserPlaylists.js @@ -13,6 +13,7 @@ import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue' import FtToggleSwitch from '../../components/ft-toggle-switch/ft-toggle-switch.vue' import FtAutoLoadNextPageWrapper from '../../components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.vue' import { ctrlFHandler, getIconForSortPreference } from '../../helpers/utils' +import { isNavigationFailure, NavigationFailureType } from 'vue-router' const SORT_BY_VALUES = { NameAscending: 'name_ascending', @@ -174,6 +175,7 @@ export default defineComponent({ doSearchPlaylistsWithMatchingVideos() { this.searchDataLimit = 100 this.filterPlaylistAsync() + this.saveStateInRouter() }, fullData() { this.activeData = this.fullData @@ -183,9 +185,9 @@ export default defineComponent({ sessionStorage.setItem('UserPlaylists/sortBy', this.sortBy) }, }, - mounted: function () { + created: function () { document.addEventListener('keydown', this.keyboardShortcutHandler) - const limit = sessionStorage.getItem('favoritesLimit') + const limit = sessionStorage.getItem('UserPlaylists/dataLimit') if (limit !== null) { this.dataLimit = limit } @@ -195,23 +197,53 @@ export default defineComponent({ this.sortBy = sortBy } - this.activeData = this.fullData - - this.showLoadMoreButton = this.activeData.length < this.allPlaylists.length - this.filterPlaylistDebounce = debounce(this.filterPlaylist, 500) + + const oldQuery = this.$route.query.searchQueryText ?? '' + if (oldQuery !== null && oldQuery !== '') { + // `handleQueryChange` must be called after `filterHistoryDebounce` assigned + this.handleQueryChange( + oldQuery, + { + limit: this.$route.query.searchDataLimit, + doSearchPlaylistsWithMatchingVideos: this.$route.query.doSearchPlaylistsWithMatchingVideos === 'true', + filterNow: true, + }, + ) + } else { + // Only display unfiltered data when no query used last time + this.filterPlaylist() + } }, beforeDestroy: function () { document.removeEventListener('keydown', this.keyboardShortcutHandler) }, methods: { + handleQueryChange(query, { limit = null, doSearchPlaylistsWithMatchingVideos = null, filterNow = false } = {}) { + this.query = query + + const newLimit = limit ?? 100 + this.searchDataLimit = newLimit + const newDoSearchPlaylistsWithMatchingVideos = doSearchPlaylistsWithMatchingVideos ?? this.doSearchPlaylistsWithMatchingVideos + this.doSearchPlaylistsWithMatchingVideos = newDoSearchPlaylistsWithMatchingVideos + + this.saveStateInRouter({ + query: query, + searchDataLimit: newLimit, + doSearchPlaylistsWithMatchingVideos: newDoSearchPlaylistsWithMatchingVideos, + }) + + filterNow ? this.filterPlaylist() : this.filterPlaylistAsync() + }, + increaseLimit: function () { if (this.query !== '') { this.searchDataLimit += 100 + this.saveStateInRouter() this.filterPlaylist() } else { this.dataLimit += 100 - sessionStorage.setItem('favoritesLimit', this.dataLimit) + sessionStorage.setItem('UserPlaylists/dataLimit', this.dataLimit) } }, filterPlaylistAsync: function() { @@ -247,6 +279,39 @@ export default defineComponent({ }) }, + async saveStateInRouter({ query = this.query, searchDataLimit = this.searchDataLimit, doSearchPlaylistsWithMatchingVideos = this.doSearchPlaylistsWithMatchingVideos } = {}) { + if (this.query === '') { + try { + await this.$router.replace({ name: 'userPlaylists' }) + } catch (failure) { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + } + return + } + + const routerQuery = { + searchQueryText: query, + searchDataLimit: searchDataLimit, + } + if (doSearchPlaylistsWithMatchingVideos) { routerQuery.doSearchPlaylistsWithMatchingVideos = 'true' } + try { + await this.$router.replace({ + name: 'userPlaylists', + query: routerQuery, + }) + } catch (failure) { + if (isNavigationFailure(failure, NavigationFailureType.duplicated)) { + return + } + + throw failure + } + }, + keyboardShortcutHandler: function (event) { ctrlFHandler(event, this.$refs.searchBar) }, diff --git a/src/renderer/views/UserPlaylists/UserPlaylists.vue b/src/renderer/views/UserPlaylists/UserPlaylists.vue index 5f455651d12c5..b5930b207375f 100644 --- a/src/renderer/views/UserPlaylists/UserPlaylists.vue +++ b/src/renderer/views/UserPlaylists/UserPlaylists.vue @@ -26,11 +26,12 @@