diff --git a/src/constants.js b/src/constants.js index c21ace4448a8c..53d4612bdc65d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -26,10 +26,12 @@ const IpcChannels = { DB_HISTORY: 'db-history', DB_PROFILES: 'db-profiles', DB_PLAYLISTS: 'db-playlists', + DB_SEARCH_HISTORY: 'db-search-history', DB_SUBSCRIPTION_CACHE: 'db-subscription-cache', SYNC_SETTINGS: 'sync-settings', SYNC_HISTORY: 'sync-history', + SYNC_SEARCH_HISTORY: 'sync-search-history', SYNC_PROFILES: 'sync-profiles', SYNC_PLAYLISTS: 'sync-playlists', SYNC_SUBSCRIPTION_CACHE: 'sync-subscription-cache', @@ -191,6 +193,12 @@ const PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD = 500 // YouTube search character limit is 100 characters const SEARCH_CHAR_LIMIT = 100 +// max # of results we show for search suggestions +const SEARCH_RESULTS_DISPLAY_LIMIT = 14 + +// max # of search history results we show when mixed with YT search suggestions +const MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 4 + // Displayed on the about page and used in the main.js file to only allow bitcoin URLs with this wallet address to be opened const ABOUT_BITCOIN_ADDRESS = '1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS' @@ -203,5 +211,7 @@ export { MOBILE_WIDTH_THRESHOLD, PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD, SEARCH_CHAR_LIMIT, + SEARCH_RESULTS_DISPLAY_LIMIT, + MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT, ABOUT_BITCOIN_ADDRESS, } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index a0f620925b6a9..d25963376f641 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -226,6 +226,24 @@ class Playlists { } } +class SearchHistory { + static find() { + return db.searchHistory.findAsync({}).sort({ lastUpdatedAt: -1 }) + } + + static upsert(searchHistoryEntry) { + return db.searchHistory.updateAsync({ _id: searchHistoryEntry._id }, searchHistoryEntry, { upsert: true }) + } + + static delete(_id) { + return db.searchHistory.removeAsync({ _id: _id }) + } + + static deleteAll() { + return db.searchHistory.removeAsync({}, { multi: true }) + } +} + class SubscriptionCache { static find() { return db.subscriptionCache.findAsync({}) @@ -311,6 +329,7 @@ function compactAllDatastores() { db.history.compactDatafileAsync(), db.profiles.compactDatafileAsync(), db.playlists.compactDatafileAsync(), + db.searchHistory.compactDatafileAsync(), db.subscriptionCache.compactDatafileAsync(), ]) } @@ -320,6 +339,7 @@ export { History as history, Profiles as profiles, Playlists as playlists, + SearchHistory as searchHistory, SubscriptionCache as subscriptionCache, compactAllDatastores, diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index b18706ec8ee5d..7a178174f0db0 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -218,6 +218,36 @@ class Playlists { } } +class SearchHistory { + static find() { + return ipcRenderer.invoke( + IpcChannels.DB_SEARCH_HISTORY, + { action: DBActions.GENERAL.FIND } + ) + } + + static upsert(searchHistoryEntry) { + return ipcRenderer.invoke( + IpcChannels.DB_SEARCH_HISTORY, + { action: DBActions.GENERAL.UPSERT, data: searchHistoryEntry } + ) + } + + static delete(_id) { + return ipcRenderer.invoke( + IpcChannels.DB_SEARCH_HISTORY, + { action: DBActions.GENERAL.DELETE, data: _id } + ) + } + + static deleteAll() { + return ipcRenderer.invoke( + IpcChannels.DB_SEARCH_HISTORY, + { action: DBActions.GENERAL.DELETE_ALL } + ) + } +} + class SubscriptionCache { static find() { return ipcRenderer.invoke( @@ -296,5 +326,6 @@ export { History as history, Profiles as profiles, Playlists as playlists, + SearchHistory as searchHistory, SubscriptionCache as subscriptionCache, } diff --git a/src/datastores/handlers/index.js b/src/datastores/handlers/index.js index 6d9b8ab729d43..1409b1cd3a450 100644 --- a/src/datastores/handlers/index.js +++ b/src/datastores/handlers/index.js @@ -3,5 +3,6 @@ export { history as DBHistoryHandlers, profiles as DBProfileHandlers, playlists as DBPlaylistHandlers, + searchHistory as DBSearchHistoryHandlers, subscriptionCache as DBSubscriptionCacheHandlers, } from 'DB_HANDLERS_ELECTRON_RENDERER_OR_WEB' diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index dfddc6a0c31ba..68e7e9e75e8a5 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -24,4 +24,4 @@ class Settings { export { Settings as settings } // These classes don't require any changes from the base classes, so can be exported as-is. -export { history, profiles, playlists, subscriptionCache } from './base' +export { history, profiles, playlists, searchHistory, subscriptionCache } from './base' diff --git a/src/datastores/index.js b/src/datastores/index.js index 7a3da53356f00..37a27b12cff89 100644 --- a/src/datastores/index.js +++ b/src/datastores/index.js @@ -26,4 +26,5 @@ export const settings = new Datastore({ filename: dbPath('settings'), autoload: export const profiles = new Datastore({ filename: dbPath('profiles'), autoload: true }) export const playlists = new Datastore({ filename: dbPath('playlists'), autoload: true }) export const history = new Datastore({ filename: dbPath('history'), autoload: true }) +export const searchHistory = new Datastore({ filename: dbPath('search-history'), autoload: true }) export const subscriptionCache = new Datastore({ filename: dbPath('subscription-cache'), autoload: true }) diff --git a/src/main/index.js b/src/main/index.js index d361bf19dfa98..1a7544a6ea7b3 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1351,6 +1351,51 @@ function runApp() { // *********** // + // ************** // + // Search History + ipcMain.handle(IpcChannels.DB_SEARCH_HISTORY, async (event, { action, data }) => { + try { + switch (action) { + case DBActions.GENERAL.FIND: + return await baseHandlers.searchHistory.find() + + case DBActions.GENERAL.UPSERT: + await baseHandlers.searchHistory.upsert(data) + syncOtherWindows( + IpcChannels.SYNC_SEARCH_HISTORY, + event, + { event: SyncEvents.GENERAL.UPSERT, data } + ) + return null + + case DBActions.GENERAL.DELETE: + await baseHandlers.searchHistory.delete(data) + syncOtherWindows( + IpcChannels.SYNC_SEARCH_HISTORY, + event, + { event: SyncEvents.GENERAL.DELETE, data } + ) + return null + + case DBActions.GENERAL.DELETE_ALL: + await baseHandlers.searchHistory.deleteAll() + syncOtherWindows( + IpcChannels.SYNC_SEARCH_HISTORY, + event, + { event: SyncEvents.GENERAL.DELETE_ALL } + ) + return null + + default: + // eslint-disable-next-line no-throw-literal + throw 'invalid search history db action' + } + } catch (err) { + if (typeof err === 'string') throw err + else throw err.toString() + } + }) + // *********** // // Profiles ipcMain.handle(IpcChannels.DB_SUBSCRIPTION_CACHE, async (event, { action, data }) => { diff --git a/src/renderer/App.js b/src/renderer/App.js index e7ebffa07ca9b..c56a7394f2625 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -194,6 +194,7 @@ export default defineComponent({ this.grabHistory() this.grabAllPlaylists() this.grabAllSubscriptions() + this.grabSearchHistoryEntries() if (process.env.IS_ELECTRON) { ipcRenderer = require('electron').ipcRenderer @@ -579,6 +580,7 @@ export default defineComponent({ 'grabHistory', 'grabAllPlaylists', 'grabAllSubscriptions', + 'grabSearchHistoryEntries', 'getYoutubeUrlInfo', 'getExternalPlayerCmdArgumentsData', 'fetchInvidiousInstances', diff --git a/src/renderer/components/ft-input/ft-input.css b/src/renderer/components/ft-input/ft-input.css index 89b7450e588f6..9ad55ce7f9c1f 100644 --- a/src/renderer/components/ft-input/ft-input.css +++ b/src/renderer/components/ft-input/ft-input.css @@ -31,12 +31,15 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp padding-inline-start: 46px; } -.ft-input-component:focus-within .clearInputTextButton { +.ft-input-component:focus-within .clearInputTextButton, +.ft-input-component.showOptions .clearInputTextButton { opacity: 0.5; } -.clearTextButtonVisible .clearInputTextButton.visible, -.ft-input-component:focus-within .clearInputTextButton.visible { + +.ft-input-component.inputDataPresent .clearInputTextButton.visible, +.clearTextButtonVisible:not(.showOptions) .clearInputTextButton.visible, +.ft-input-component:focus-within:not(.showOptions) .clearInputTextButton.visible { cursor: pointer; opacity: 1; } @@ -200,10 +203,30 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp } .list li { - display: block; + display: flex; + justify-content: space-between; padding-block: 0; - padding-inline: 15px; line-height: 2rem; + padding-inline: 15px; +} + +.searchResultIcon { + opacity: 0.6; + padding-inline-end: 10px; + inline-size: 16px; + block-size: 16px; +} + +.removeButton { + text-decoration: none; + float: var(--float-right-ltr-rtl-value); + font-size: 13px; +} + +.removeButton:hover, +.removeButtonSelected { + text-decoration: underline; + font-weight: bold; } .hover { diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 7a3aa07150fe7..1c0d5a132d463 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -63,12 +63,24 @@ export default defineComponent({ type: Array, default: () => { return [] } }, + dataListProperties: { + type: Array, + default: () => { return [] } + }, + searchResultIconNames: { + type: Array, + default: null + }, + showDataWhenEmpty: { + type: Boolean, + default: false + }, tooltip: { type: String, default: '' } }, - emits: ['clear', 'click', 'input'], + emits: ['clear', 'click', 'input', 'remove'], data: function () { let actionIcon = ['fas', 'search'] if (this.forceActionButtonIconName !== null) { @@ -88,10 +100,16 @@ export default defineComponent({ // As the text input box should be empty clearTextButtonExisting: false, clearTextButtonVisible: false, + removeButtonSelectedIndex: -1, + removalMade: false, actionButtonIconName: actionIcon } }, computed: { + showOptions: function () { + return (this.inputData !== '' || this.showDataWhenEmpty) && this.visibleDataList.length > 0 && this.searchState.showOptions + }, + barColor: function () { return this.$store.getters.getBarColor }, @@ -101,8 +119,9 @@ export default defineComponent({ }, inputDataPresent: function () { - return this.inputData.length > 0 + return this.inputDataDisplayed.length > 0 }, + inputDataDisplayed() { if (!this.isSearch) { return this.inputData } @@ -116,7 +135,6 @@ export default defineComponent({ searchStateKeyboardSelectedOptionValue() { if (this.searchState.keyboardSelectedOptionIndex === -1) { return null } - return this.visibleDataList[this.searchState.keyboardSelectedOptionIndex] }, }, @@ -152,6 +170,7 @@ export default defineComponent({ this.searchState.showOptions = false this.searchState.selectedOption = -1 this.searchState.keyboardSelectedOptionIndex = -1 + this.removeButtonSelectedIndex = -1 this.$emit('input', this.inputData) this.$emit('click', this.inputData, { event: e }) }, @@ -173,6 +192,7 @@ export default defineComponent({ this.inputData = '' this.handleActionIconChange() this.updateVisibleDataList() + this.searchState.isPointerInList = false this.$refs.input.value = '' @@ -233,56 +253,83 @@ export default defineComponent({ }, handleOptionClick: function (index) { + if (this.removeButtonSelectedIndex !== -1) { + this.handleRemoveClick(index) + return + } this.searchState.showOptions = false this.inputData = this.visibleDataList[index] this.$emit('input', this.inputData) this.handleClick() }, + handleRemoveClick: function (index) { + if (!this.dataListProperties[index]?.isRemoveable) { return } + + // keep input in focus even when the to-be-removed "Remove" button was clicked + this.$refs.input.focus() + this.removalMade = true + this.$emit('remove', this.visibleDataList[index]) + }, + /** * @param {KeyboardEvent} event */ handleKeyDown: function (event) { + // Update Input box value if enter key was pressed and option selected if (event.key === 'Enter') { - // Update Input box value if enter key was pressed and option selected - if (this.searchState.selectedOption !== -1) { + if (this.removeButtonSelectedIndex !== -1) { + this.handleRemoveClick(this.removeButtonSelectedIndex) + } else if (this.searchState.selectedOption !== -1) { this.searchState.showOptions = false event.preventDefault() this.inputData = this.visibleDataList[this.searchState.selectedOption] + this.handleOptionClick(this.searchState.selectedOption) + } else { + this.handleClick(event) } - this.handleClick(event) - // Early return + return } if (this.visibleDataList.length === 0) { return } this.searchState.showOptions = true - const isArrow = event.key === 'ArrowDown' || event.key === 'ArrowUp' - if (!isArrow) { + + // "select" the Remove button through right arrow navigation, and unselect it with the left arrow + if (event.key === 'ArrowRight') { + this.removeButtonSelectedIndex = this.searchState.selectedOption + } else if (event.key === 'ArrowLeft') { + this.removeButtonSelectedIndex = -1 + } else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + event.preventDefault() + const newIndex = this.searchState.selectedOption + (event.key === 'ArrowDown' ? 1 : -1) + this.updateSelectedOptionIndex(newIndex) + } else { const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue // Keyboard selected & is char if (!isNullOrEmpty(selectedOptionValue) && isKeyboardEventKeyPrintableChar(event.key)) { // Update input based on KB selected suggestion value instead of current input value event.preventDefault() this.handleInput(`${selectedOptionValue}${event.key}`) - return } - return } + }, + + // Updates the selected dropdown option index and handles the under/over-flow behavior + updateSelectedOptionIndex: function (index) { + this.searchState.selectedOption = index + + // unset selection of "Remove" button + this.removeButtonSelectedIndex = -1 - event.preventDefault() - if (event.key === 'ArrowDown') { - this.searchState.selectedOption++ - } else if (event.key === 'ArrowUp') { - this.searchState.selectedOption-- - } // Allow deselecting suggestion if (this.searchState.selectedOption < -1) { this.searchState.selectedOption = this.visibleDataList.length - 1 } else if (this.searchState.selectedOption > this.visibleDataList.length - 1) { this.searchState.selectedOption = -1 } + // Update displayed value this.searchState.keyboardSelectedOptionIndex = this.searchState.selectedOption }, @@ -291,16 +338,22 @@ export default defineComponent({ if (!this.searchState.isPointerInList) { this.searchState.showOptions = false } }, - handleFocus: function(e) { + handleFocus: function () { this.searchState.showOptions = true }, updateVisibleDataList: function () { - if (this.dataList.length === 0) { return } // Reset selected option before it's updated - this.searchState.selectedOption = -1 - this.searchState.keyboardSelectedOptionIndex = -1 - if (this.inputData === '') { + // Block resetting if it was just the "Remove" button that was pressed + if (!this.removalMade || this.searchState.selectedOption >= this.dataList.length) { + this.searchState.selectedOption = -1 + this.searchState.keyboardSelectedOptionIndex = -1 + this.removeButtonSelectedIndex = -1 + } + + this.removalMade = false + + if (this.inputData.trim() === '') { this.visibleDataList = this.dataList return } diff --git a/src/renderer/components/ft-input/ft-input.vue b/src/renderer/components/ft-input/ft-input.vue index ea5f5c80d56a0..0177c818e1309 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -7,7 +7,9 @@ forceTextColor: forceTextColor, showActionButton: showActionButton, showClearTextButton: showClearTextButton, - clearTextButtonVisible: inputDataPresent, + clearTextButtonVisible: inputDataPresent || showOptions, + inputDataPresent: inputDataPresent, + showOptions: showOptions, disabled: disabled }" > @@ -29,7 +31,7 @@ :icon="['fas', 'times-circle']" class="clearInputTextButton" :class="{ - visible: inputDataPresent + visible: inputDataPresent || showOptions }" tabindex="0" role="button" @@ -68,21 +70,39 @@
diff --git a/src/renderer/components/privacy-settings/privacy-settings.js b/src/renderer/components/privacy-settings/privacy-settings.js index f9855341ef80d..7a52c7b709fa1 100644 --- a/src/renderer/components/privacy-settings/privacy-settings.js +++ b/src/renderer/components/privacy-settings/privacy-settings.js @@ -30,6 +30,9 @@ export default defineComponent({ } }, computed: { + rememberSearchHistory: function () { + return this.$store.getters.getRememberSearchHistory + }, rememberHistory: function () { return this.$store.getters.getRememberHistory }, @@ -60,7 +63,8 @@ export default defineComponent({ if (option !== 'delete') { return } this.clearSessionSearchHistory() - showToast(this.$t('Settings.Privacy Settings.Search cache has been cleared')) + this.removeAllSearchHistoryEntries() + showToast(this.$t('Settings.Privacy Settings.Search history and cache have been cleared')) }, handleRememberHistory: function (value) { @@ -118,9 +122,11 @@ export default defineComponent({ ...mapActions([ 'updateRememberHistory', 'removeAllHistory', + 'updateRememberSearchHistory', 'updateSaveWatchedProgress', 'updateSaveVideoHistoryWithLastViewedPlaylist', 'clearSessionSearchHistory', + 'removeAllSearchHistoryEntries', 'updateProfile', 'removeProfile', 'updateActiveProfile', diff --git a/src/renderer/components/privacy-settings/privacy-settings.vue b/src/renderer/components/privacy-settings/privacy-settings.vue index 3fe025d4e7df1..497616d95b8ea 100644 --- a/src/renderer/components/privacy-settings/privacy-settings.vue +++ b/src/renderer/components/privacy-settings/privacy-settings.vue @@ -11,6 +11,14 @@ @change="handleRememberHistory" />
+
+ +
{ + const isSearchHistoryEntry = i < searchHistoryEntriesCount + return isSearchHistoryEntry + ? { isRemoveable: true, iconName: 'clock-rotate-left' } + : { isRemoveable: false, iconName: 'magnifying-glass' } + }) + }, }, watch: { $route: function () { @@ -288,12 +337,15 @@ export default defineComponent({ }, getSearchSuggestionsDebounce: function (query) { + const trimmedQuery = query.trim() + if (trimmedQuery === this.lastSuggestionQuery) { + return + } + + this.lastSuggestionQuery = trimmedQuery + if (this.enableSearchSuggestions) { - const trimmedQuery = query.trim() - if (trimmedQuery !== this.lastSuggestionQuery) { - this.lastSuggestionQuery = trimmedQuery - this.debounceSearchResults(trimmedQuery) - } + this.debounceSearchResults(trimmedQuery) } }, @@ -409,10 +461,19 @@ export default defineComponent({ this.navigationHistoryDropdownActiveEntry.label = value } }, + removeSearchHistoryEntryInDbAndCache(query) { + this.removeSearchHistoryEntry(query) + this.removeFromSessionSearchHistory(query) + }, ...mapActions([ 'getYoutubeUrlInfo', + 'removeSearchHistoryEntry', 'showSearchFilters' + ]), + + ...mapMutations([ + 'removeFromSessionSearchHistory' ]) } }) diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue index d156e02968541..c9309df3c7a1e 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -92,11 +92,15 @@ :placeholder="$t('Search / Go to URL')" class="searchInput" :is-search="true" - :data-list="searchSuggestionsDataList" + :data-list="activeDataList" + :data-list-properties="activeDataListProperties" :spellcheck="false" :show-clear-text-button="true" + :show-data-when-empty="true" @input="getSearchSuggestionsDebounce" @click="goToSearch" + @clear="() => lastSuggestionQuery = ''" + @remove="removeSearchHistoryEntryInDbAndCache" /> { + return state.searchHistoryEntries + }, + + getLatestSearchHistoryNames: (state) => { + return state.searchHistoryEntries.slice(0, SEARCH_RESULTS_DISPLAY_LIMIT).map((entry) => entry._id) + }, + + getLatestMatchingSearchHistoryNames: (state) => (id) => { + const matches = state.searchHistoryEntries.filter((entry) => entry._id.startsWith(id)) + + // prioritize more concise matches + return matches.map((entry) => entry._id) + .sort((a, b) => a.length - b.length) + }, + + getSearchHistoryEntryWithId: (state) => (id) => { + return state.searchHistoryEntries.find(p => p._id === id) + }, +} +const actions = { + async grabSearchHistoryEntries({ commit }) { + try { + const results = await DBSearchHistoryHandlers.find() + commit('setSearchHistoryEntries', results) + } catch (errMessage) { + console.error(errMessage) + } + }, + + async updateSearchHistoryEntry({ commit }, searchHistoryEntry) { + try { + await DBSearchHistoryHandlers.upsert(searchHistoryEntry) + commit('upsertSearchHistoryEntryToList', searchHistoryEntry) + } catch (errMessage) { + console.error(errMessage) + } + }, + + async removeSearchHistoryEntry({ commit }, _id) { + try { + await DBSearchHistoryHandlers.delete(_id) + commit('removeSearchHistoryEntryFromList', _id) + } catch (errMessage) { + console.error(errMessage) + } + }, + + async removeAllSearchHistoryEntries({ commit }) { + try { + await DBSearchHistoryHandlers.deleteAll() + commit('setSearchHistoryEntries', []) + } catch (errMessage) { + console.error(errMessage) + } + }, +} + +const mutations = { + setSearchHistoryEntries(state, searchHistoryEntries) { + state.searchHistoryEntries = searchHistoryEntries + }, + + upsertSearchHistoryEntryToList(state, updatedSearchHistoryEntry) { + state.searchHistoryEntries = state.searchHistoryEntries.filter((p) => { + return p._id !== updatedSearchHistoryEntry._id + }) + + state.searchHistoryEntries.unshift(updatedSearchHistoryEntry) + }, + + removeSearchHistoryEntryFromList(state, _id) { + state.searchHistoryEntries = state.searchHistoryEntries.filter((searchHistoryEntry) => searchHistoryEntry._id !== _id) + } +} + +export default { + state, + getters, + actions, + mutations +} diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js index e733ccffbe0f1..a50f4c3937b1c 100644 --- a/src/renderer/store/modules/settings.js +++ b/src/renderer/store/modules/settings.js @@ -245,6 +245,7 @@ const state = { proxyVideos: !process.env.SUPPORTS_LOCAL_API, region: 'US', rememberHistory: true, + rememberSearchHistory: true, saveWatchedProgress: true, saveVideoHistoryWithLastViewedPlaylist: true, showFamilyFriendlyOnly: false, @@ -513,6 +514,25 @@ const customActions = { } }) + ipcRenderer.on(IpcChannels.SYNC_SEARCH_HISTORY, (_, { event, data }) => { + switch (event) { + case SyncEvents.GENERAL.UPSERT: + commit('upsertSearchHistoryEntryToList', data) + break + + case SyncEvents.GENERAL.DELETE: + commit('removeSearchHistoryEntryFromList', data) + break + + case SyncEvents.GENERAL.DELETE_ALL: + commit('setSearchHistoryEntries', []) + break + + default: + console.error('search history: invalid sync event received') + } + }) + ipcRenderer.on(IpcChannels.SYNC_PROFILES, (_, { event, data }) => { switch (event) { case SyncEvents.GENERAL.CREATE: diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js index 7c3a3822051c5..2022f5e44e4eb 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -843,6 +843,10 @@ const mutations = { vueSet(state.deArrowCache, payload.videoId, payload) }, + removeFromSessionSearchHistory (state, query) { + state.sessionSearchHistory = state.sessionSearchHistory.filter((search) => search.query !== query) + }, + addToSessionSearchHistory (state, payload) { const sameSearch = state.sessionSearchHistory.findIndex((search) => { return search.query === payload.query && searchFiltersMatch(payload.searchSettings, search.searchSettings) diff --git a/src/renderer/views/Search/Search.js b/src/renderer/views/Search/Search.js index 1ba41d14a3332..d7bd2f74a896d 100644 --- a/src/renderer/views/Search/Search.js +++ b/src/renderer/views/Search/Search.js @@ -50,11 +50,13 @@ export default defineComponent({ showFamilyFriendlyOnly: function() { return this.$store.getters.getShowFamilyFriendlyOnly }, + + rememberSearchHistory: function () { + return this.$store.getters.getRememberSearchHistory + }, }, watch: { $route () { - // react to route changes... - const query = this.$route.params.query let features = this.$route.query.features // if page gets refreshed and there's only one feature then it will be a string @@ -108,6 +110,15 @@ export default defineComponent({ this.checkSearchCache(payload) }, methods: { + updateSearchHistoryEntry: function () { + const persistentSearchHistoryPayload = { + _id: this.query, + lastUpdatedAt: Date.now() + } + + this.$store.dispatch('updateSearchHistoryEntry', persistentSearchHistoryPayload) + }, + checkSearchCache: function (payload) { if (payload.query.length > SEARCH_CHAR_LIMIT) { console.warn(`Search character limit is: ${SEARCH_CHAR_LIMIT}`) @@ -136,6 +147,10 @@ export default defineComponent({ break } } + + if (this.rememberSearchHistory) { + this.updateSearchHistoryEntry() + } }, performSearchLocal: async function (payload) { diff --git a/static/locales/en-GB.yaml b/static/locales/en-GB.yaml index 9b745e4f844b2..8a13ad548677c 100644 --- a/static/locales/en-GB.yaml +++ b/static/locales/en-GB.yaml @@ -455,7 +455,8 @@ Settings: Name: None Privacy Settings: Privacy Settings: 'Privacy' - Remember History: 'Remember History' + Remember History: 'Remember Watch History' + Remember Search History: 'Remember Search History' Save Watched Progress: 'Save Watched Progress' Clear Search Cache: 'Clear Search Cache' Are you sure you want to clear out your search cache?: 'Are you sure you want diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index b3aaa45826c91..d5a7a15409753 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -63,6 +63,7 @@ Global: Search / Go to URL: Search / Go to URL Search Bar: Clear Input: Clear Input + Remove: Remove Search character limit: Search query is over the {searchCharacterLimit} character limit Search Listing: Label: @@ -481,13 +482,14 @@ Settings: Name: None Privacy Settings: Privacy Settings: Privacy - Remember History: Remember History + Remember History: Remember Watch History + Remember Search History: Remember Search History Save Watched Progress: Save Watched Progress Save Watched Videos With Last Viewed Playlist: Save Watched Videos With Last Viewed Playlist - Clear Search Cache: Clear Search Cache - Are you sure you want to clear out your search cache?: Are you sure you want to - clear out your search cache? - Search cache has been cleared: Search cache has been cleared + Clear Search History and Cache: Clear Search History and Cache + Are you sure you want to clear out your search history and cache?: Are you sure you want to + clear out your search history and cache? + Search history and cache have been cleared: Search history and cache have been cleared Remove Watch History: Remove Watch History Are you sure you want to remove your entire watch history?: Are you sure you want to remove your entire watch history?