From cb18afdf7890d7c9a87c5418bbb119325682768e Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Wed, 18 Dec 2024 17:43:09 -0600 Subject: [PATCH 01/24] Add search history DB integration --- src/constants.js | 2 + src/datastores/handlers/base.js | 28 ++++ src/datastores/handlers/electron.js | 45 ++++++ src/datastores/handlers/index.js | 1 + src/datastores/handlers/web.js | 27 ++++ src/datastores/index.js | 1 + src/main/index.js | 78 ++++++++-- src/renderer/store/modules/index.js | 2 + src/renderer/store/modules/search-history.js | 151 +++++++++++++++++++ src/renderer/store/modules/settings.js | 25 ++- 10 files changed, 345 insertions(+), 15 deletions(-) create mode 100644 src/renderer/store/modules/search-history.js diff --git a/src/constants.js b/src/constants.js index 0ae194d19a6cd..bac4c5b45de24 100644 --- a/src/constants.js +++ b/src/constants.js @@ -29,10 +29,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', diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 18411722951a1..f699e4e585b7d 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -226,6 +226,32 @@ class Playlists { } } +class SearchHistory { + static create(searchHistoryEntry) { + return db.searchHistory.insertAsync(searchHistoryEntry) + } + + static find() { + return db.searchHistory.findAsync({}) + } + + static upsert(searchHistoryEntry) { + return db.searchHistory.updateAsync({ _id: searchHistoryEntry._id }, searchHistoryEntry, { upsert: true }) + } + + static delete(_id) { + return db.searchHistory.removeAsync({ _id: _id }) + } + + static deleteMultiple(ids) { + return db.searchHistory.removeAsync({ _id: { $in: ids } }) + } + + static deleteAll() { + return db.searchHistory.removeAsync({}, { multi: true }) + } +} + class SubscriptionCache { static find() { return db.subscriptionCache.findAsync({}) @@ -311,6 +337,7 @@ function compactAllDatastores() { db.history.compactDatafileAsync(), db.profiles.compactDatafileAsync(), db.playlists.compactDatafileAsync(), + db.searchHistory.compactDatafileAsync(), db.subscriptionCache.compactDatafileAsync(), ]) } @@ -320,6 +347,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 889c91d4f060c..fbe4ed19a91c3 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -218,6 +218,50 @@ class Playlists { } } +class SearchHistory { + static create(searchHistoryEntry) { + return ipcRenderer.invoke( + IpcChannels.DB_SEARCH_HISTORY, + { action: DBActions.GENERAL.CREATE, data: searchHistoryEntry } + ) + } + + 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 deleteMultiple(ids) { + return ipcRenderer.invoke( + IpcChannels.DB_SEARCH_HISTORY, + { action: DBActions.GENERAL.DELETE_MULTIPLE, data: ids } + ) + } + + static deleteAll() { + return ipcRenderer.invoke( + IpcChannels.DB_SEARCH_HISTORY, + { action: DBActions.GENERAL.DELETE_ALL } + ) + } +} + class SubscriptionCache { static find() { return ipcRenderer.invoke( @@ -296,5 +340,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 d68e24f042615..85f6c2907b2a5 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -122,6 +122,32 @@ class Playlists { } } +class SearchHistory { + static create(searchHistoryEntry) { + return baseHandlers.searchHistory.create(searchHistoryEntry) + } + + static find() { + return baseHandlers.searchHistory.find() + } + + static upsert(searchHistoryEntry) { + return baseHandlers.searchHistory.upsert(searchHistoryEntry) + } + + static delete(_id) { + return baseHandlers.searchHistory.delete(_id) + } + + static deleteMultiple(ids) { + return baseHandlers.searchHistory.deleteMultiple(ids) + } + + static deleteAll() { + return baseHandlers.searchHistory.deleteAll() + } +} + class SubscriptionCache { static find() { return baseHandlers.subscriptionCache.find() @@ -180,5 +206,6 @@ export { History as history, Profiles as profiles, Playlists as playlists, + SearchHistory as searchHistory, SubscriptionCache as subscriptionCache, } 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 4bd741a4accb9..133f12accee3c 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -254,16 +254,13 @@ function runApp() { app.on('second-instance', (_, commandLine, __) => { // Someone tried to run a second instance, we should focus our window - if (typeof commandLine !== 'undefined') { - const url = getLinkUrl(commandLine) - if (mainWindow && mainWindow.webContents) { - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.focus() + if (mainWindow && typeof commandLine !== 'undefined') { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() - if (url) mainWindow.webContents.send(IpcChannels.OPEN_URL, url) - } else { - if (url) startupUrl = url - createWindow() + const url = getLinkUrl(commandLine) + if (url) { + mainWindow.webContents.send(IpcChannels.OPEN_URL, url) } } }) @@ -832,11 +829,10 @@ function runApp() { }) } - ipcMain.on(IpcChannels.APP_READY, () => { + ipcMain.once(IpcChannels.APP_READY, () => { if (startupUrl) { mainWindow.webContents.send(IpcChannels.OPEN_URL, startupUrl, { isLaunchLink: true }) } - startupUrl = null }) function relaunch() { @@ -1344,6 +1340,64 @@ function runApp() { } }) + // ************** // + // Search History + ipcMain.handle(IpcChannels.DB_SEARCH_HISTORY, async (event, { action, data }) => { + try { + switch (action) { + case DBActions.GENERAL.CREATE: { + const searchHistoryEntry = await baseHandlers.searchHistory.create(data) + syncOtherWindows( + IpcChannels.SYNC_SEARCH_HISTORY, + event, + { event: SyncEvents.GENERAL.CREATE, data } + ) + return searchHistoryEntry + } + 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_MULTIPLE: + await baseHandlers.searchHistory.deleteMultiple(data) + syncOtherWindows( + IpcChannels.SYNC_SEARCH_HISTORY, + event, + { event: SyncEvents.GENERAL.DELETE_MULTIPLE, data } + ) + return null + + case DBActions.GENERAL.DELETE_ALL: + await baseHandlers.searchHistory.deleteAll() + 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() + } + }) + // *********** // // *********** // @@ -1446,7 +1500,6 @@ function runApp() { app.on('window-all-closed', () => { // Clean up resources (datastores' compaction + Electron cache and storage data clearing) cleanUpResources().finally(() => { - mainWindow = null if (process.platform !== 'darwin') { app.quit() } @@ -1516,7 +1569,6 @@ function runApp() { mainWindow.webContents.send(IpcChannels.OPEN_URL, baseUrl(url)) } else { startupUrl = baseUrl(url) - if (app.isReady()) createWindow() } }) diff --git a/src/renderer/store/modules/index.js b/src/renderer/store/modules/index.js index 144b0355c696f..8270551303c8f 100644 --- a/src/renderer/store/modules/index.js +++ b/src/renderer/store/modules/index.js @@ -8,6 +8,7 @@ import invidious from './invidious' import playlists from './playlists' import profiles from './profiles' import settings from './settings' +import searchHistory from './search-history' import subscriptionCache from './subscription-cache' import utils from './utils' import player from './player' @@ -18,6 +19,7 @@ export default { playlists, profiles, settings, + searchHistory, subscriptionCache, utils, player, diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js new file mode 100644 index 0000000000000..676b764bf20d9 --- /dev/null +++ b/src/renderer/store/modules/search-history.js @@ -0,0 +1,151 @@ +import { DBSearchHistoryHandlers } from '../../../datastores/handlers/index' + +const state = { + searchHistoryEntries: [] +} + +const getters = { + getSearchHistoryEntries: (state) => { + return state.searchHistoryEntries + }, + + getSearchHistoryEntryWithRoute: (state) => (route) => { + const searchHistoryEntry = state.searchHistoryEntries.find(p => p.route === route) + return searchHistoryEntry + }, + + getSearchHistoryEntriesMatchingQuery: (state) => (query, routeToExclude) => { + if (query === '') { + return [] + } + const queryToLower = query.toLowerCase() + return state.searchHistoryEntries.filter((searchHistoryEntry) => + searchHistoryEntry.name.toLowerCase().includes(queryToLower) && searchHistoryEntry.route !== routeToExclude + ) + }, + + getSearchHistoryIdsForMatchingUserPlaylistIds: (state) => (playlistIds) => { + const searchHistoryIds = [] + const allSearchHistoryEntries = state.searchHistoryEntries + const searchHistoryEntryLimitedRoutesMap = new Map() + allSearchHistoryEntries.forEach((searchHistoryEntry) => { + searchHistoryEntryLimitedRoutesMap.set(searchHistoryEntry.route, searchHistoryEntry._id) + }) + + playlistIds.forEach((playlistId) => { + const route = `/playlist/${playlistId}?playlistType=user&searchQueryText=` + if (!searchHistoryEntryLimitedRoutesMap.has(route)) { + return + } + + searchHistoryIds.push(searchHistoryEntryLimitedRoutesMap.get(route)) + }) + + return searchHistoryIds + } +} +const actions = { + async grabSearchHistoryEntries({ commit }) { + try { + const results = await DBSearchHistoryHandlers.find() + commit('setSearchHistoryEntries', results) + } catch (errMessage) { + console.error(errMessage) + } + }, + + async createSearchHistoryEntry({ commit }, searchHistoryEntry) { + try { + const newSearchHistoryEntry = await DBSearchHistoryHandlers.create(searchHistoryEntry) + commit('addSearchHistoryEntryToList', newSearchHistoryEntry) + } 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 removeSearchHistoryEntries({ commit }, ids) { + try { + await DBSearchHistoryHandlers.deleteMultiple(ids) + commit('removeSearchHistoryEntriesFromList', ids) + } catch (errMessage) { + console.error(errMessage) + } + }, + + async removeUserPlaylistSearchHistoryEntries({ dispatch, getters }, userPlaylistIds) { + const searchHistoryIds = getters.getSearchHistoryIdsForMatchingUserPlaylistIds(userPlaylistIds) + if (searchHistoryIds.length === 0) { + return + } + + dispatch('removeSearchHistoryEntries', searchHistoryIds) + }, + + async removeAllSearchHistoryEntries({ commit }) { + try { + await DBSearchHistoryHandlers.deleteAll() + commit('setSearchHistoryEntries', []) + } catch (errMessage) { + console.error(errMessage) + } + }, +} + +const mutations = { + addSearchHistoryEntryToList(state, searchHistoryEntry) { + state.searchHistoryEntries.push(searchHistoryEntry) + }, + + setSearchHistoryEntries(state, searchHistoryEntries) { + state.searchHistoryEntries = searchHistoryEntries + }, + + upsertSearchHistoryEntryToList(state, updatedSearchHistoryEntry) { + const i = state.searchHistoryEntries.findIndex((p) => { + return p.route === updatedSearchHistoryEntry.route + }) + + if (i === -1) { + state.searchHistoryEntries.push(updatedSearchHistoryEntry) + } else { + state.searchHistoryEntries.splice(i, 1, updatedSearchHistoryEntry) + } + }, + + removeSearchHistoryEntryFromList(state, _id) { + const i = state.searchHistoryEntries.findIndex((searchHistoryEntry) => { + return searchHistoryEntry._id === _id + }) + + state.searchHistoryEntries.splice(i, 1) + }, + + removeSearchHistoryEntriesFromList(state, ids) { + state.searchHistoryEntries = state.searchHistoryEntries.filter((searchHistoryEntry) => !ids.includes(searchHistoryEntry._id)) + } +} + +export default { + state, + getters, + actions, + mutations +} diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js index eb241922fd512..9ee544ecd1f83 100644 --- a/src/renderer/store/modules/settings.js +++ b/src/renderer/store/modules/settings.js @@ -172,7 +172,6 @@ const state = { baseTheme: 'system', mainColor: 'Red', secColor: 'Blue', - defaultAutoplayInterruptionIntervalHours: 3, defaultCaptionSettings: '{}', defaultInterval: 5, defaultPlayback: 1, @@ -234,7 +233,6 @@ const state = { listType: 'grid', maxVideoPlaybackRate: 3, onlyShowLatestFromChannel: false, - onlyShowLatestFromChannelNumber: 1, openDeepLinksInNewWindow: false, playNextVideo: false, proxyHostname: '127.0.0.1', @@ -511,6 +509,29 @@ const customActions = { } }) + ipcRenderer.on(IpcChannels.SYNC_SEARCH_HISTORY, (_, { event, data }) => { + switch (event) { + case SyncEvents.GENERAL.CREATE: + commit('addSearchHistoryEntryToList', data) + break + + case SyncEvents.GENERAL.UPSERT: + commit('upsertSearchHistoryEntryToList', data) + break + + case SyncEvents.GENERAL.DELETE: + commit('removeSearchHistoryEntryFromList', data) + break + + case SyncEvents.GENERAL.DELETE_MULTIPLE: + commit('removeSearchHistoryEntriesFromList', data) + break + + default: + console.error('search history: invalid sync event received') + } + }) + ipcRenderer.on(IpcChannels.SYNC_PROFILES, (_, { event, data }) => { switch (event) { case SyncEvents.GENERAL.CREATE: From 820932e299d648b52a2a2d65352a8e07fcb231ac Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 19 Dec 2024 14:32:40 -0600 Subject: [PATCH 02/24] Implement search history display logic and UI --- src/datastores/handlers/base.js | 2 +- src/renderer/App.js | 2 + src/renderer/components/ft-input/ft-input.css | 16 ++++- src/renderer/components/ft-input/ft-input.js | 31 ++++++--- src/renderer/components/ft-input/ft-input.vue | 18 +++-- .../general-settings/general-settings.vue | 1 + src/renderer/components/top-nav/top-nav.js | 34 ++++++++-- src/renderer/components/top-nav/top-nav.vue | 3 +- src/renderer/main.js | 4 +- src/renderer/store/modules/search-history.js | 66 ++++++------------- src/renderer/views/Search/Search.js | 16 ++++- 11 files changed, 121 insertions(+), 72 deletions(-) diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index f699e4e585b7d..14a5a56ba8982 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -232,7 +232,7 @@ class SearchHistory { } static find() { - return db.searchHistory.findAsync({}) + return db.searchHistory.findAsync({}).sort({ lastUpdatedAt: -1 }) } static upsert(searchHistoryEntry) { diff --git a/src/renderer/App.js b/src/renderer/App.js index 5846ca3a2184d..51dbd2263b0af 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -189,6 +189,7 @@ export default defineComponent({ this.grabHistory() this.grabAllPlaylists() this.grabAllSubscriptions() + this.grabSearchHistoryEntries() if (process.env.IS_ELECTRON) { ipcRenderer = require('electron').ipcRenderer @@ -570,6 +571,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..4db1aa301d293 100644 --- a/src/renderer/components/ft-input/ft-input.css +++ b/src/renderer/components/ft-input/ft-input.css @@ -202,8 +202,22 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp .list li { display: block; padding-block: 0; - padding-inline: 15px; line-height: 2rem; + padding-inline: 15px; + text-overflow: ellipsis; + overflow-x: hidden; + white-space: nowrap; +} + +.bookmarkStarIcon { + color: var(--favorite-icon-color); +} + +.searchResultIcon { + opacity: 0.6; + padding-inline-end: 10px; + inline-size: 16px; + block-size: 16px; } .hover { diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 7a3aa07150fe7..8ace59c8b071f 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -63,6 +63,10 @@ export default defineComponent({ type: Array, default: () => { return [] } }, + showDataWhenEmpty: { + type: Boolean, + default: false + }, tooltip: { type: String, default: '' @@ -116,8 +120,7 @@ export default defineComponent({ searchStateKeyboardSelectedOptionValue() { if (this.searchState.keyboardSelectedOptionIndex === -1) { return null } - - return this.visibleDataList[this.searchState.keyboardSelectedOptionIndex] + return this.getTextForArrayAtIndex(this.visibleDataList, this.searchState.keyboardSelectedOptionIndex) }, }, watch: { @@ -143,6 +146,9 @@ export default defineComponent({ this.updateVisibleDataList() }, methods: { + getTextForArrayAtIndex: function (array, index) { + return array[index].name ?? array[index] + }, handleClick: function (e) { // No action if no input text if (!this.inputDataPresent) { @@ -166,7 +172,7 @@ export default defineComponent({ this.$emit('input', val) }, - handleClearTextClick: function () { + handleClearTextClick: function ({ programmaticallyTriggered = false }) { // No action if no input text if (!this.inputDataPresent) { return } @@ -177,7 +183,9 @@ export default defineComponent({ this.$refs.input.value = '' // Focus on input element after text is clear for better UX - this.$refs.input.focus() + if (!programmaticallyTriggered) { + this.$refs.input.focus() + } this.$emit('clear') }, @@ -234,7 +242,11 @@ export default defineComponent({ handleOptionClick: function (index) { this.searchState.showOptions = false - this.inputData = this.visibleDataList[index] + if (this.visibleDataList[index].route) { + this.inputData = `ft:${this.visibleDataList[index].route}` + } else { + this.inputData = this.visibleDataList[index] + } this.$emit('input', this.inputData) this.handleClick() }, @@ -248,9 +260,11 @@ export default defineComponent({ if (this.searchState.selectedOption !== -1) { this.searchState.showOptions = false event.preventDefault() - this.inputData = this.visibleDataList[this.searchState.selectedOption] + this.inputData = this.getTextForArrayAtIndex(this.visibleDataList, this.searchState.selectedOption) + this.handleOptionClick(this.searchState.selectedOption) + } else { + this.handleClick(event) } - this.handleClick(event) // Early return return } @@ -291,12 +305,11 @@ 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 diff --git a/src/renderer/components/ft-input/ft-input.vue b/src/renderer/components/ft-input/ft-input.vue index ea5f5c80d56a0..2482b697d0de5 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -68,21 +68,31 @@
  • - {{ list }} + + + {{ entry.name ?? entry }}
diff --git a/src/renderer/components/general-settings/general-settings.vue b/src/renderer/components/general-settings/general-settings.vue index a2e1d76003326..9ada8aeb42060 100644 --- a/src/renderer/components/general-settings/general-settings.vue +++ b/src/renderer/components/general-settings/general-settings.vue @@ -120,6 +120,7 @@ :show-label="true" :value="currentInvidiousInstance" :data-list="invidiousInstancesList" + :show-data-when-empty="true" :tooltip="$t('Tooltips.General Settings.Invidious Instance')" @input="handleInvidiousInstanceInput" /> diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index cf580cba750c6..476a37259ff6f 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -120,7 +120,15 @@ export default defineComponent({ this.$t('Open New Window'), KeyboardShortcuts.APP.GENERAL.NEW_WINDOW ) - } + }, + + // show latest search history when the search bar is empty + activeDataList: function () { + if (!this.enableSearchSuggestions) { + return + } + return this.lastSuggestionQuery === '' ? this.$store.getters.getLatestUniqueSearchHistoryEntries : this.searchSuggestionsDataList + }, }, watch: { $route: function () { @@ -168,6 +176,17 @@ export default defineComponent({ clearLocalSearchSuggestionsSession() + if (queryText.startsWith('ft:')) { + this.$refs.searchInput.handleClearTextClick({ programmaticallyTriggered: true }) + const adjustedQuery = queryText.substring(3) + openInternalPath({ + path: adjustedQuery, + adjustedQuery, + doCreateNewWindow + }) + return + } + this.getYoutubeUrlInfo(queryText).then((result) => { switch (result.urlType) { case 'video': { @@ -289,12 +308,15 @@ export default defineComponent({ }, getSearchSuggestionsDebounce: function (query) { + const trimmedQuery = query.trim() + if (trimmedQuery === this.lastSuggestionQuery || trimmedQuery.startsWith('ft:')) { + return + } + + this.lastSuggestionQuery = trimmedQuery + if (this.enableSearchSuggestions) { - const trimmedQuery = query.trim() - if (trimmedQuery !== this.lastSuggestionQuery) { - this.lastSuggestionQuery = trimmedQuery - this.debounceSearchResults(trimmedQuery) - } + this.debounceSearchResults(trimmedQuery) } }, diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue index d156e02968541..f49f5a63fc5cc 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -92,9 +92,10 @@ :placeholder="$t('Search / Go to URL')" class="searchInput" :is-search="true" - :data-list="searchSuggestionsDataList" + :data-list="activeDataList" :spellcheck="false" :show-clear-text-button="true" + :show-data-when-empty="true" @input="getSearchSuggestionsDebounce" @click="goToSearch" /> diff --git a/src/renderer/main.js b/src/renderer/main.js index 7d7d1708c87a4..46ffd7063236e 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -33,6 +33,7 @@ import { faCircleUser, faClapperboard, faClock, + faClockRotateLeft, faClone, faComment, faCommentDots, @@ -110,7 +111,7 @@ import { faUserLock, faUsers, faUsersSlash, - faWifi + faWifi, } from '@fortawesome/free-solid-svg-icons' import { faBookmark as farBookmark, @@ -151,6 +152,7 @@ library.add( faCircleUser, faClapperboard, faClock, + faClockRotateLeft, faClone, faComment, faCommentDots, diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js index 676b764bf20d9..a04d5f8a92a55 100644 --- a/src/renderer/store/modules/search-history.js +++ b/src/renderer/store/modules/search-history.js @@ -1,5 +1,8 @@ import { DBSearchHistoryHandlers } from '../../../datastores/handlers/index' +// matches # of results we show for search suggestions +const SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 14 + const state = { searchHistoryEntries: [] } @@ -9,40 +12,22 @@ const getters = { return state.searchHistoryEntries }, + getLatestUniqueSearchHistoryEntries: (state) => { + const nameSet = new Set() + return state.searchHistoryEntries.filter((entry) => { + if (nameSet.has(entry.name)) { + return false + } + + nameSet.add(entry.name) + return true + }).slice(0, SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT) + }, + getSearchHistoryEntryWithRoute: (state) => (route) => { const searchHistoryEntry = state.searchHistoryEntries.find(p => p.route === route) return searchHistoryEntry }, - - getSearchHistoryEntriesMatchingQuery: (state) => (query, routeToExclude) => { - if (query === '') { - return [] - } - const queryToLower = query.toLowerCase() - return state.searchHistoryEntries.filter((searchHistoryEntry) => - searchHistoryEntry.name.toLowerCase().includes(queryToLower) && searchHistoryEntry.route !== routeToExclude - ) - }, - - getSearchHistoryIdsForMatchingUserPlaylistIds: (state) => (playlistIds) => { - const searchHistoryIds = [] - const allSearchHistoryEntries = state.searchHistoryEntries - const searchHistoryEntryLimitedRoutesMap = new Map() - allSearchHistoryEntries.forEach((searchHistoryEntry) => { - searchHistoryEntryLimitedRoutesMap.set(searchHistoryEntry.route, searchHistoryEntry._id) - }) - - playlistIds.forEach((playlistId) => { - const route = `/playlist/${playlistId}?playlistType=user&searchQueryText=` - if (!searchHistoryEntryLimitedRoutesMap.has(route)) { - return - } - - searchHistoryIds.push(searchHistoryEntryLimitedRoutesMap.get(route)) - }) - - return searchHistoryIds - } } const actions = { async grabSearchHistoryEntries({ commit }) { @@ -90,15 +75,6 @@ const actions = { } }, - async removeUserPlaylistSearchHistoryEntries({ dispatch, getters }, userPlaylistIds) { - const searchHistoryIds = getters.getSearchHistoryIdsForMatchingUserPlaylistIds(userPlaylistIds) - if (searchHistoryIds.length === 0) { - return - } - - dispatch('removeSearchHistoryEntries', searchHistoryIds) - }, - async removeAllSearchHistoryEntries({ commit }) { try { await DBSearchHistoryHandlers.deleteAll() @@ -111,7 +87,7 @@ const actions = { const mutations = { addSearchHistoryEntryToList(state, searchHistoryEntry) { - state.searchHistoryEntries.push(searchHistoryEntry) + state.searchHistoryEntries.unshift(searchHistoryEntry) }, setSearchHistoryEntries(state, searchHistoryEntries) { @@ -119,15 +95,11 @@ const mutations = { }, upsertSearchHistoryEntryToList(state, updatedSearchHistoryEntry) { - const i = state.searchHistoryEntries.findIndex((p) => { - return p.route === updatedSearchHistoryEntry.route + state.searchHistoryEntries = state.searchHistoryEntries.filter((p) => { + return p.route !== updatedSearchHistoryEntry.route }) - if (i === -1) { - state.searchHistoryEntries.push(updatedSearchHistoryEntry) - } else { - state.searchHistoryEntries.splice(i, 1, updatedSearchHistoryEntry) - } + state.searchHistoryEntries.unshift(updatedSearchHistoryEntry) }, removeSearchHistoryEntryFromList(state, _id) { diff --git a/src/renderer/views/Search/Search.js b/src/renderer/views/Search/Search.js index eb274ed5ad22e..7304da8cecf70 100644 --- a/src/renderer/views/Search/Search.js +++ b/src/renderer/views/Search/Search.js @@ -53,8 +53,6 @@ export default defineComponent({ }, 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 +106,18 @@ export default defineComponent({ this.checkSearchCache(payload) }, methods: { + updateSearchHistoryEntry: function () { + const currentRoute = this.$router.currentRoute.fullPath + const persistentSearchHistoryPayload = { + name: this.query, + route: currentRoute, + _id: currentRoute, + lastUpdatedAt: new Date() + } + + 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 +146,8 @@ export default defineComponent({ break } } + + this.updateSearchHistoryEntry() }, performSearchLocal: async function (payload) { From 679496ffdad53aa8652fd64ab8f057f00db66c18 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 19 Dec 2024 14:43:38 -0600 Subject: [PATCH 03/24] Modify search cache removal setting to remove search history as well --- .../components/general-settings/general-settings.vue | 1 - .../components/privacy-settings/privacy-settings.js | 4 +++- .../components/privacy-settings/privacy-settings.vue | 4 ++-- src/renderer/store/modules/settings.js | 2 ++ static/locales/en-US.yaml | 8 ++++---- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/renderer/components/general-settings/general-settings.vue b/src/renderer/components/general-settings/general-settings.vue index 9ada8aeb42060..a2e1d76003326 100644 --- a/src/renderer/components/general-settings/general-settings.vue +++ b/src/renderer/components/general-settings/general-settings.vue @@ -120,7 +120,6 @@ :show-label="true" :value="currentInvidiousInstance" :data-list="invidiousInstancesList" - :show-data-when-empty="true" :tooltip="$t('Tooltips.General Settings.Invidious Instance')" @input="handleInvidiousInstanceInput" /> diff --git a/src/renderer/components/privacy-settings/privacy-settings.js b/src/renderer/components/privacy-settings/privacy-settings.js index f9855341ef80d..9c7c12e3146cf 100644 --- a/src/renderer/components/privacy-settings/privacy-settings.js +++ b/src/renderer/components/privacy-settings/privacy-settings.js @@ -60,7 +60,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 cache and history have been cleared')) }, handleRememberHistory: function (value) { @@ -121,6 +122,7 @@ export default defineComponent({ '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..61de5429ab190 100644 --- a/src/renderer/components/privacy-settings/privacy-settings.vue +++ b/src/renderer/components/privacy-settings/privacy-settings.vue @@ -33,7 +33,7 @@
Date: Thu, 19 Dec 2024 15:06:38 -0600 Subject: [PATCH 04/24] Exclude current search route from history suggestions and populate input with matching query --- src/main/index.js | 40 ++++++++++---------- src/renderer/components/ft-input/ft-input.js | 10 ++--- src/renderer/components/top-nav/top-nav.js | 3 +- src/renderer/store/modules/search-history.js | 33 +++++++++++++++- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 133f12accee3c..fb0f4f01e441a 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -254,13 +254,16 @@ function runApp() { app.on('second-instance', (_, commandLine, __) => { // Someone tried to run a second instance, we should focus our window - if (mainWindow && typeof commandLine !== 'undefined') { - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.focus() - + if (typeof commandLine !== 'undefined') { const url = getLinkUrl(commandLine) - if (url) { - mainWindow.webContents.send(IpcChannels.OPEN_URL, url) + if (mainWindow && mainWindow.webContents) { + if (mainWindow.isMinimized()) mainWindow.restore() + mainWindow.focus() + + if (url) mainWindow.webContents.send(IpcChannels.OPEN_URL, url) + } else { + if (url) startupUrl = url + createWindow() } } }) @@ -829,10 +832,11 @@ function runApp() { }) } - ipcMain.once(IpcChannels.APP_READY, () => { + ipcMain.on(IpcChannels.APP_READY, () => { if (startupUrl) { mainWindow.webContents.send(IpcChannels.OPEN_URL, startupUrl, { isLaunchLink: true }) } + startupUrl = null }) function relaunch() { @@ -1294,11 +1298,7 @@ function runApp() { return null case DBActions.PLAYLISTS.DELETE_VIDEO_ID: - await baseHandlers.playlists.deleteVideoIdByPlaylistId({ - _id: data._id, - videoId: data.videoId, - playlistItemId: data.playlistItemId, - }) + await baseHandlers.playlists.deleteVideoIdByPlaylistId(data._id, data.videoId, data.playlistItemId) syncOtherWindows( IpcChannels.SYNC_PLAYLISTS, event, @@ -1340,6 +1340,8 @@ function runApp() { } }) + // *********** // + // ************** // // Search History ipcMain.handle(IpcChannels.DB_SEARCH_HISTORY, async (event, { action, data }) => { @@ -1398,8 +1400,6 @@ function runApp() { } }) - // *********** // - // *********** // // Profiles ipcMain.handle(IpcChannels.DB_SUBSCRIPTION_CACHE, async (event, { action, data }) => { @@ -1409,7 +1409,7 @@ function runApp() { return await baseHandlers.subscriptionCache.find() case DBActions.SUBSCRIPTION_CACHE.UPDATE_VIDEOS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateVideosByChannelId(data) + await baseHandlers.subscriptionCache.updateVideosByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1418,7 +1418,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_LIVE_STREAMS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateLiveStreamsByChannelId(data) + await baseHandlers.subscriptionCache.updateLiveStreamsByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1427,7 +1427,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_SHORTS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateShortsByChannelId(data) + await baseHandlers.subscriptionCache.updateShortsByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1436,7 +1436,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_SHORTS_WITH_CHANNEL_PAGE_SHORTS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateShortsWithChannelPageShortsByChannelId(data) + await baseHandlers.subscriptionCache.updateShortsWithChannelPageShortsByChannelId(data.channelId, data.entries) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1445,7 +1445,7 @@ function runApp() { return null case DBActions.SUBSCRIPTION_CACHE.UPDATE_COMMUNITY_POSTS_BY_CHANNEL: - await baseHandlers.subscriptionCache.updateCommunityPostsByChannelId(data) + await baseHandlers.subscriptionCache.updateCommunityPostsByChannelId(data.channelId, data.entries, data.timestamp) syncOtherWindows( IpcChannels.SYNC_SUBSCRIPTION_CACHE, event, @@ -1500,6 +1500,7 @@ function runApp() { app.on('window-all-closed', () => { // Clean up resources (datastores' compaction + Electron cache and storage data clearing) cleanUpResources().finally(() => { + mainWindow = null if (process.platform !== 'darwin') { app.quit() } @@ -1569,6 +1570,7 @@ function runApp() { mainWindow.webContents.send(IpcChannels.OPEN_URL, baseUrl(url)) } else { startupUrl = baseUrl(url) + if (app.isReady()) createWindow() } }) diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 8ace59c8b071f..05251e0418af7 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -172,7 +172,7 @@ export default defineComponent({ this.$emit('input', val) }, - handleClearTextClick: function ({ programmaticallyTriggered = false }) { + handleClearTextClick: function () { // No action if no input text if (!this.inputDataPresent) { return } @@ -183,9 +183,7 @@ export default defineComponent({ this.$refs.input.value = '' // Focus on input element after text is clear for better UX - if (!programmaticallyTriggered) { - this.$refs.input.focus() - } + this.$refs.input.focus() this.$emit('clear') }, @@ -244,10 +242,12 @@ export default defineComponent({ this.searchState.showOptions = false if (this.visibleDataList[index].route) { this.inputData = `ft:${this.visibleDataList[index].route}` + this.$emit('input', this.inputData) + this.inputData = this.$refs.input.value = this.visibleDataList[index].name } else { this.inputData = this.visibleDataList[index] + this.$emit('input', this.inputData) } - this.$emit('input', this.inputData) this.handleClick() }, diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 476a37259ff6f..a68afa3eb1f0a 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -127,7 +127,7 @@ export default defineComponent({ if (!this.enableSearchSuggestions) { return } - return this.lastSuggestionQuery === '' ? this.$store.getters.getLatestUniqueSearchHistoryEntries : this.searchSuggestionsDataList + return this.lastSuggestionQuery === '' ? this.$store.getters.getLatestUniqueSearchHistoryEntries(this.$router.currentRoute.fullPath) : this.searchSuggestionsDataList }, }, watch: { @@ -177,7 +177,6 @@ export default defineComponent({ clearLocalSearchSuggestionsSession() if (queryText.startsWith('ft:')) { - this.$refs.searchInput.handleClearTextClick({ programmaticallyTriggered: true }) const adjustedQuery = queryText.substring(3) openInternalPath({ path: adjustedQuery, diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js index a04d5f8a92a55..870c738095661 100644 --- a/src/renderer/store/modules/search-history.js +++ b/src/renderer/store/modules/search-history.js @@ -12,10 +12,10 @@ const getters = { return state.searchHistoryEntries }, - getLatestUniqueSearchHistoryEntries: (state) => { + getLatestUniqueSearchHistoryEntries: (state) => (routeToExclude) => { const nameSet = new Set() return state.searchHistoryEntries.filter((entry) => { - if (nameSet.has(entry.name)) { + if (nameSet.has(entry.name) || routeToExclude === entry.route) { return false } @@ -28,6 +28,26 @@ const getters = { const searchHistoryEntry = state.searchHistoryEntries.find(p => p.route === route) return searchHistoryEntry }, + + getSearchHistoryIdsForMatchingUserPlaylistIds: (state) => (playlistIds) => { + const searchHistoryIds = [] + const allSearchHistoryEntries = state.searchHistoryEntries + const searchHistoryEntryLimitedRoutesMap = new Map() + allSearchHistoryEntries.forEach((searchHistoryEntry) => { + searchHistoryEntryLimitedRoutesMap.set(searchHistoryEntry.route, searchHistoryEntry._id) + }) + + playlistIds.forEach((playlistId) => { + const route = `/playlist/${playlistId}?playlistType=user&searchQueryText=` + if (!searchHistoryEntryLimitedRoutesMap.has(route)) { + return + } + + searchHistoryIds.push(searchHistoryEntryLimitedRoutesMap.get(route)) + }) + + return searchHistoryIds + } } const actions = { async grabSearchHistoryEntries({ commit }) { @@ -75,6 +95,15 @@ const actions = { } }, + async removeUserPlaylistSearchHistoryEntries({ dispatch, getters }, userPlaylistIds) { + const searchHistoryIds = getters.getSearchHistoryIdsForMatchingUserPlaylistIds(userPlaylistIds) + if (searchHistoryIds.length === 0) { + return + } + + dispatch('removeSearchHistoryEntries', searchHistoryIds) + }, + async removeAllSearchHistoryEntries({ commit }) { try { await DBSearchHistoryHandlers.deleteAll() From 953ab80dd365b6f2d9ae04668c8f9a6aa277f841 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 19 Dec 2024 18:24:04 -0600 Subject: [PATCH 05/24] Modify new labels for clarity --- .../components/privacy-settings/privacy-settings.js | 2 +- .../components/privacy-settings/privacy-settings.vue | 4 ++-- static/locales/en-US.yaml | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/privacy-settings/privacy-settings.js b/src/renderer/components/privacy-settings/privacy-settings.js index 9c7c12e3146cf..58c334e0623e6 100644 --- a/src/renderer/components/privacy-settings/privacy-settings.js +++ b/src/renderer/components/privacy-settings/privacy-settings.js @@ -61,7 +61,7 @@ export default defineComponent({ this.clearSessionSearchHistory() this.removeAllSearchHistoryEntries() - showToast(this.$t('Settings.Privacy Settings.Search cache and history have been cleared')) + showToast(this.$t('Settings.Privacy Settings.Search history and cache have been cleared')) }, handleRememberHistory: function (value) { diff --git a/src/renderer/components/privacy-settings/privacy-settings.vue b/src/renderer/components/privacy-settings/privacy-settings.vue index 61de5429ab190..5d4c9bc509e1a 100644 --- a/src/renderer/components/privacy-settings/privacy-settings.vue +++ b/src/renderer/components/privacy-settings/privacy-settings.vue @@ -33,7 +33,7 @@
Date: Thu, 19 Dec 2024 19:07:35 -0600 Subject: [PATCH 06/24] Fix issues detected during review Fix focus being lost for a second during searching of search history entry causing clear text button to phase away for a moment. Fix clear text button event not being noticed by the logic in top-nav. Fix spacebar error issue by adding check in updateVisibleDataList function. --- src/renderer/components/ft-input/ft-input.js | 18 +++++++++++------- src/renderer/components/top-nav/top-nav.js | 6 ++++-- src/renderer/components/top-nav/top-nav.vue | 1 + 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 05251e0418af7..b249a35b128dc 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -240,15 +240,15 @@ export default defineComponent({ handleOptionClick: function (index) { this.searchState.showOptions = false - if (this.visibleDataList[index].route) { - this.inputData = `ft:${this.visibleDataList[index].route}` - this.$emit('input', this.inputData) + const isSearchHistoryClick = this.visibleDataList[index].route + this.inputData = isSearchHistoryClick ? `ft:${this.visibleDataList[index].route}` : this.visibleDataList[index] + this.$emit('input', this.inputData) + this.handleClick() + + // update displayed label to match name of the search history entry + if (isSearchHistoryClick) { this.inputData = this.$refs.input.value = this.visibleDataList[index].name - } else { - this.inputData = this.visibleDataList[index] - this.$emit('input', this.inputData) } - this.handleClick() }, /** @@ -321,6 +321,10 @@ export default defineComponent({ const lowerCaseInputData = this.inputData.toLowerCase() this.visibleDataList = this.dataList.filter(x => { + if (x.name) { + return x.name.toLowerCase().indexOf(lowerCaseInputData) !== -1 + } + return x.toLowerCase().indexOf(lowerCaseInputData) !== -1 }) }, diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index a68afa3eb1f0a..22d4f837a9d6b 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -167,16 +167,18 @@ export default defineComponent({ goToSearch: async function (queryText, { event }) { const doCreateNewWindow = event && event.shiftKey + const isFreeTubeInternalQuery = queryText.startsWith('ft:') + if (window.innerWidth <= MOBILE_WIDTH_THRESHOLD) { this.$refs.searchContainer.blur() this.showSearchContainer = false - } else { + } else if (!isFreeTubeInternalQuery) { this.$refs.searchInput.blur() } clearLocalSearchSuggestionsSession() - if (queryText.startsWith('ft:')) { + if (isFreeTubeInternalQuery) { const adjustedQuery = queryText.substring(3) openInternalPath({ path: adjustedQuery, diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue index f49f5a63fc5cc..1483357e9a123 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -98,6 +98,7 @@ :show-data-when-empty="true" @input="getSearchSuggestionsDebounce" @click="goToSearch" + @clear="lastSuggestionQuery = ''" /> Date: Thu, 19 Dec 2024 20:59:07 -0600 Subject: [PATCH 07/24] Implement fixes from code review Fixes clear text button to appear and stay visible when the dropdown options are visible in an ft-input. Fixes updateVisibleDataList not using trim(), thus incorrectly causing filtering of search history when space bar was used. --- src/renderer/components/ft-input/ft-input.css | 13 ++++++------- src/renderer/components/ft-input/ft-input.js | 6 +++++- src/renderer/components/ft-input/ft-input.vue | 7 ++++--- src/renderer/components/top-nav/top-nav.js | 17 +++++++++++------ src/renderer/store/modules/search-history.js | 4 ++-- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/renderer/components/ft-input/ft-input.css b/src/renderer/components/ft-input/ft-input.css index 4db1aa301d293..799dab09e0153 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; } @@ -209,10 +212,6 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp white-space: nowrap; } -.bookmarkStarIcon { - color: var(--favorite-icon-color); -} - .searchResultIcon { opacity: 0.6; padding-inline-end: 10px; diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index b249a35b128dc..e68bb944954ae 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -96,6 +96,10 @@ export default defineComponent({ } }, computed: { + showOptions: function () { + return (this.inputData !== '' || this.showDataWhenEmpty) && this.visibleDataList.length > 0 && this.searchState.showOptions + }, + barColor: function () { return this.$store.getters.getBarColor }, @@ -313,7 +317,7 @@ export default defineComponent({ // Reset selected option before it's updated this.searchState.selectedOption = -1 this.searchState.keyboardSelectedOptionIndex = -1 - if (this.inputData === '') { + 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 2482b697d0de5..d58de72a61313 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -7,7 +7,8 @@ forceTextColor: forceTextColor, showActionButton: showActionButton, showClearTextButton: showClearTextButton, - clearTextButtonVisible: inputDataPresent, + clearTextButtonVisible: inputDataPresent || showOptions, + showOptions: showOptions, disabled: disabled }" > @@ -29,7 +30,7 @@ :icon="['fas', 'times-circle']" class="clearInputTextButton" :class="{ - visible: inputDataPresent + visible: inputDataPresent || showOptions }" tabindex="0" role="button" @@ -68,7 +69,7 @@
    (routeToExclude) => { + getLatestUniqueSearchHistoryEntries: (state) => { const nameSet = new Set() return state.searchHistoryEntries.filter((entry) => { - if (nameSet.has(entry.name) || routeToExclude === entry.route) { + if (nameSet.has(entry.name)) { return false } From a6375c76a5cdcf3dbb983fb5022e64844bd04417 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 19 Dec 2024 21:42:34 -0600 Subject: [PATCH 08/24] Update logic to allowing storing and searching by name rather than by route This is much more efficient for search history particularly. No need for app-specific routes and easier operations & logic due to name being the primary key for search history entries. This still allows for the route to be used as the _id for different kinds of search history (i.e., bookmarks) that could be added in the future. --- src/renderer/components/ft-input/ft-input.js | 18 ++++-------- src/renderer/components/ft-input/ft-input.vue | 12 +++----- src/renderer/components/top-nav/top-nav.js | 27 +++++------------- src/renderer/components/top-nav/top-nav.vue | 2 +- src/renderer/store/modules/search-history.js | 28 +++++++------------ src/renderer/views/Search/Search.js | 4 +-- 6 files changed, 29 insertions(+), 62 deletions(-) diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index e68bb944954ae..08b09b6cecb09 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -63,6 +63,10 @@ export default defineComponent({ type: Array, default: () => { return [] } }, + searchResultIcon: { + type: Array, + default: null + }, showDataWhenEmpty: { type: Boolean, default: false @@ -151,7 +155,7 @@ export default defineComponent({ }, methods: { getTextForArrayAtIndex: function (array, index) { - return array[index].name ?? array[index] + return array[index] }, handleClick: function (e) { // No action if no input text @@ -244,15 +248,9 @@ export default defineComponent({ handleOptionClick: function (index) { this.searchState.showOptions = false - const isSearchHistoryClick = this.visibleDataList[index].route - this.inputData = isSearchHistoryClick ? `ft:${this.visibleDataList[index].route}` : this.visibleDataList[index] + this.inputData = this.visibleDataList[index] this.$emit('input', this.inputData) this.handleClick() - - // update displayed label to match name of the search history entry - if (isSearchHistoryClick) { - this.inputData = this.$refs.input.value = this.visibleDataList[index].name - } }, /** @@ -325,10 +323,6 @@ export default defineComponent({ const lowerCaseInputData = this.inputData.toLowerCase() this.visibleDataList = this.dataList.filter(x => { - if (x.name) { - return x.name.toLowerCase().indexOf(lowerCaseInputData) !== -1 - } - return x.toLowerCase().indexOf(lowerCaseInputData) !== -1 }) }, diff --git a/src/renderer/components/ft-input/ft-input.vue b/src/renderer/components/ft-input/ft-input.vue index d58de72a61313..8a8429bda9861 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -8,6 +8,7 @@ showActionButton: showActionButton, showClearTextButton: showClearTextButton, clearTextButtonVisible: inputDataPresent || showOptions, + inputDataPresent: inputDataPresent, showOptions: showOptions, disabled: disabled }" @@ -84,16 +85,11 @@ @mouseleave="searchState.selectedOption = -1" > - - {{ entry.name ?? entry }} + {{ entry }}
diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 3907d4237bd90..cb9698ed144e6 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -127,8 +127,12 @@ export default defineComponent({ if (!this.enableSearchSuggestions) { return } - return this.lastSuggestionQuery === '' ? this.$store.getters.getLatestUniqueSearchHistoryEntries : this.searchSuggestionsDataList + return this.lastSuggestionQuery === '' ? this.$store.getters.getLatestUniqueSearchHistoryNames : this.searchSuggestionsDataList }, + + searchResultIcon: function () { + return this.lastSuggestionQuery === '' ? ['fas', 'clock-rotate-left'] : ['fas', 'magnifying-glass'] + } }, watch: { $route: function () { @@ -167,32 +171,15 @@ export default defineComponent({ goToSearch: async function (queryText, { event }) { const doCreateNewWindow = event && event.shiftKey - const isFreeTubeInternalQuery = queryText.startsWith('ft:') - if (window.innerWidth <= MOBILE_WIDTH_THRESHOLD) { this.$refs.searchContainer.blur() this.showSearchContainer = false - } else if (!isFreeTubeInternalQuery) { + } else { this.$refs.searchInput.blur() } clearLocalSearchSuggestionsSession() - if (isFreeTubeInternalQuery) { - const adjustedQuery = queryText.substring(3) - if (this.$router.currentRoute.fullPath !== adjustedQuery) { - openInternalPath({ - path: adjustedQuery, - adjustedQuery, - doCreateNewWindow - }) - } - - // update in-use search query to the selected search history entry name - this.lastSuggestionQuery = this.$store.getters.getSearchHistoryEntryWithRoute(adjustedQuery)?.name ?? '' - return - } - this.getYoutubeUrlInfo(queryText).then((result) => { switch (result.urlType) { case 'video': { @@ -315,7 +302,7 @@ export default defineComponent({ getSearchSuggestionsDebounce: function (query) { const trimmedQuery = query.trim() - if (trimmedQuery === this.lastSuggestionQuery || trimmedQuery.startsWith('ft:')) { + if (trimmedQuery === this.lastSuggestionQuery) { return } diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue index 1483357e9a123..f07e81f06a57b 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -93,12 +93,12 @@ class="searchInput" :is-search="true" :data-list="activeDataList" + :search-result-icon="searchResultIcon" :spellcheck="false" :show-clear-text-button="true" :show-data-when-empty="true" @input="getSearchSuggestionsDebounce" @click="goToSearch" - @clear="lastSuggestionQuery = ''" /> { - const nameSet = new Set() - return state.searchHistoryEntries.filter((entry) => { - if (nameSet.has(entry.name)) { - return false - } - - nameSet.add(entry.name) - return true - }).slice(0, SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT) + getLatestUniqueSearchHistoryNames: (state) => { + return state.searchHistoryEntries.slice(0, SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT).map((entry) => entry.name) }, - getSearchHistoryEntryWithRoute: (state) => (route) => { - const searchHistoryEntry = state.searchHistoryEntries.find(p => p.route === route) + getSearchHistoryEntryWithId: (state) => (id) => { + const searchHistoryEntry = state.searchHistoryEntries.find(p => p._id === id) return searchHistoryEntry }, getSearchHistoryIdsForMatchingUserPlaylistIds: (state) => (playlistIds) => { const searchHistoryIds = [] const allSearchHistoryEntries = state.searchHistoryEntries - const searchHistoryEntryLimitedRoutesMap = new Map() + const searchHistoryEntryLimitedIdsMap = new Map() allSearchHistoryEntries.forEach((searchHistoryEntry) => { - searchHistoryEntryLimitedRoutesMap.set(searchHistoryEntry.route, searchHistoryEntry._id) + searchHistoryEntryLimitedIdsMap.set(searchHistoryEntry._id, searchHistoryEntry._id) }) playlistIds.forEach((playlistId) => { - const route = `/playlist/${playlistId}?playlistType=user&searchQueryText=` - if (!searchHistoryEntryLimitedRoutesMap.has(route)) { + const id = `/playlist/${playlistId}?playlistType=user&searchQueryText=` + if (!searchHistoryEntryLimitedIdsMap.has(id)) { return } - searchHistoryIds.push(searchHistoryEntryLimitedRoutesMap.get(route)) + searchHistoryIds.push(searchHistoryEntryLimitedIdsMap.get(id)) }) return searchHistoryIds @@ -125,7 +117,7 @@ const mutations = { upsertSearchHistoryEntryToList(state, updatedSearchHistoryEntry) { state.searchHistoryEntries = state.searchHistoryEntries.filter((p) => { - return p.route !== updatedSearchHistoryEntry.route + return p.name !== updatedSearchHistoryEntry._id }) state.searchHistoryEntries.unshift(updatedSearchHistoryEntry) diff --git a/src/renderer/views/Search/Search.js b/src/renderer/views/Search/Search.js index 7304da8cecf70..94a34015195f1 100644 --- a/src/renderer/views/Search/Search.js +++ b/src/renderer/views/Search/Search.js @@ -107,11 +107,9 @@ export default defineComponent({ }, methods: { updateSearchHistoryEntry: function () { - const currentRoute = this.$router.currentRoute.fullPath const persistentSearchHistoryPayload = { + _id: this.query, name: this.query, - route: currentRoute, - _id: currentRoute, lastUpdatedAt: new Date() } From a618e6cd940116e6346ee241abfbd19a10c26228 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Sun, 22 Dec 2024 17:56:16 -0600 Subject: [PATCH 09/24] Add back clear statement --- src/renderer/components/top-nav/top-nav.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue index f07e81f06a57b..b5c6861f06f25 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -99,6 +99,7 @@ :show-data-when-empty="true" @input="getSearchSuggestionsDebounce" @click="goToSearch" + @clear="() => lastSuggestionQuery = ''" /> Date: Fri, 27 Dec 2024 10:55:37 -0600 Subject: [PATCH 10/24] Fix clear text button not working or appearing as active when arrowing through search history results --- src/renderer/components/ft-input/ft-input.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 08b09b6cecb09..1a7cd3281d5d2 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -113,7 +113,7 @@ export default defineComponent({ }, inputDataPresent: function () { - return this.inputData.length > 0 + return this.inputDataDisplayed.length > 0 }, inputDataDisplayed() { if (!this.isSearch) { return this.inputData } From 2caa250ea3ff99be4401b15603b592d867c56e08 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Fri, 27 Dec 2024 16:22:43 -0600 Subject: [PATCH 11/24] Add 'Remove' option --- src/renderer/components/ft-input/ft-input.css | 11 ++++ src/renderer/components/ft-input/ft-input.js | 62 ++++++++++++++++--- src/renderer/components/ft-input/ft-input.vue | 15 ++++- src/renderer/components/top-nav/top-nav.js | 21 +++++-- src/renderer/components/top-nav/top-nav.vue | 2 + src/renderer/store/modules/search-history.js | 6 +- src/renderer/store/modules/utils.js | 4 ++ static/locales/en-US.yaml | 1 + 8 files changed, 102 insertions(+), 20 deletions(-) diff --git a/src/renderer/components/ft-input/ft-input.css b/src/renderer/components/ft-input/ft-input.css index 799dab09e0153..9587079ad6665 100644 --- a/src/renderer/components/ft-input/ft-input.css +++ b/src/renderer/components/ft-input/ft-input.css @@ -219,6 +219,17 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp block-size: 16px; } +.removeButton { + text-decoration: none; + float: var(--float-right-ltr-rtl-value); + font-size: 13px; +} + +.removeButton:hover, +.removeButton.removeButtonSelected { + text-decoration: underline; +} + .hover { background-color: var(--scrollbar-color-hover); color: var(--scrollbar-text-color-hover); diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 1a7cd3281d5d2..0dea2b751db6d 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -67,6 +67,10 @@ export default defineComponent({ type: Array, default: null }, + canRemoveResults: { + type: Boolean, + default: false + }, showDataWhenEmpty: { type: Boolean, default: false @@ -76,7 +80,7 @@ export default defineComponent({ default: '' } }, - emits: ['clear', 'click', 'input'], + emits: ['clear', 'click', 'input', 'remove'], data: function () { let actionIcon = ['fas', 'search'] if (this.forceActionButtonIconName !== null) { @@ -96,6 +100,8 @@ export default defineComponent({ // As the text input box should be empty clearTextButtonExisting: false, clearTextButtonVisible: false, + removeButtonSelectedIndex: -1, + removalMade: false, actionButtonIconName: actionIcon } }, @@ -166,6 +172,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 }) }, @@ -247,19 +254,34 @@ 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.canRemoveResults) { return } + + // keep focus in input even if removed "Remove" button was clicked + this.$refs.input.focus() + this.removalMade = true + this.$emit('remove', this.visibleDataList[index]) + }, + /** * @param {KeyboardEvent} event */ handleKeyDown: function (event) { 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.getTextForArrayAtIndex(this.visibleDataList, this.searchState.selectedOption) @@ -274,7 +296,7 @@ export default defineComponent({ if (this.visibleDataList.length === 0) { return } this.searchState.showOptions = true - const isArrow = event.key === 'ArrowDown' || event.key === 'ArrowUp' + const isArrow = event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' if (!isArrow) { const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue // Keyboard selected & is char @@ -288,17 +310,33 @@ export default defineComponent({ } event.preventDefault() - if (event.key === 'ArrowDown') { - this.searchState.selectedOption++ - } else if (event.key === 'ArrowUp') { - this.searchState.selectedOption-- + + if (event.key === 'ArrowRight') { + this.removeButtonSelectedIndex = this.searchState.selectedOption + } + + if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { + const newIndex = this.searchState.selectedOption + (event.key === 'ArrowDown' ? 1 : -1) + this.updateSelectedOptionIndex(newIndex) + + // reset removal + this.removeButtonSelectedIndex = -1 + } + + if (this.searchState.selectedOption !== -1 && event.key === 'ArrowRight') { + this.removeButtonSelectedIndex = this.searchState.selectedOption } + }, + + updateSelectedOptionIndex: function (index) { + this.searchState.selectedOption = index // 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 }, @@ -313,8 +351,14 @@ export default defineComponent({ updateVisibleDataList: function () { // Reset selected option before it's updated - this.searchState.selectedOption = -1 - this.searchState.keyboardSelectedOptionIndex = -1 + 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 8a8429bda9861..a0046ada87c23 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -82,14 +82,25 @@ :class="{ hover: searchState.selectedOption === index }" @click="handleOptionClick(index)" @mouseenter="searchState.selectedOption = index" - @mouseleave="searchState.selectedOption = -1" + @mouseleave="searchState.selectedOption = -1; removeButtonSelectedIndex = -1" > - {{ entry }} + {{ entry }} + + {{ $t('Search Bar.Remove') }} + diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 57f8dd774aca5..a46c9eddf7c6f 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -1,5 +1,5 @@ import { defineComponent } from 'vue' -import { mapActions } from 'vuex' +import { mapActions, mapMutations } from 'vuex' import FtInput from '../ft-input/ft-input.vue' import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue' import FtIconButton from '../ft-icon-button/ft-icon-button.vue' @@ -121,17 +121,21 @@ export default defineComponent({ ) }, + usingSearchHistoryResults: function () { + return this.lastSuggestionQuery === '' + }, + // show latest search history when the search bar is empty activeDataList: function () { if (!this.enableSearchSuggestions) { return } - return this.lastSuggestionQuery === '' ? this.$store.getters.getLatestUniqueSearchHistoryNames : this.searchSuggestionsDataList + return this.usingSearchHistoryResults ? this.$store.getters.getLatestUniqueSearchHistoryNames : this.searchSuggestionsDataList }, searchResultIcon: function () { - return this.lastSuggestionQuery === '' ? ['fas', 'clock-rotate-left'] : ['fas', 'magnifying-glass'] - } + return this.usingSearchHistoryResults ? ['fas', 'clock-rotate-left'] : ['fas', 'magnifying-glass'] + }, }, watch: { $route: function () { @@ -424,10 +428,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 b5c6861f06f25..e44a23b1beda5 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -97,9 +97,11 @@ :spellcheck="false" :show-clear-text-button="true" :show-data-when-empty="true" + :can-remove-results="usingSearchHistoryResults" @input="getSearchSuggestionsDebounce" @click="goToSearch" @clear="() => lastSuggestionQuery = ''" + @remove="removeSearchHistoryEntryInDbAndCache" /> { - return searchHistoryEntry._id === _id - }) - - state.searchHistoryEntries.splice(i, 1) + state.searchHistoryEntries = state.searchHistoryEntries.filter((searchHistoryEntry) => searchHistoryEntry._id !== _id) }, removeSearchHistoryEntriesFromList(state, ids) { diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js index b76cb04811fc1..c5b9075487899 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -830,6 +830,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/static/locales/en-US.yaml b/static/locales/en-US.yaml index 4e0a172cd9803..38b0aed05d7e3 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: From 24de0452e0a8b7394337eb8c0d3b15997255fcfd Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Sat, 28 Dec 2024 00:06:10 -0600 Subject: [PATCH 12/24] Implement search history entries showing up alongside YT search suggestions as queries are being typed --- src/constants.js | 4 ++ src/renderer/components/ft-input/ft-input.js | 23 +++++++--- src/renderer/components/ft-input/ft-input.vue | 6 +-- src/renderer/components/top-nav/top-nav.js | 45 ++++++++++++++++--- src/renderer/components/top-nav/top-nav.vue | 3 +- src/renderer/store/modules/search-history.js | 24 ++++++++-- 6 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/constants.js b/src/constants.js index e313136b168e2..33da8dd0587cc 100644 --- a/src/constants.js +++ b/src/constants.js @@ -193,6 +193,9 @@ const PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD = 500 // YouTube search character limit is 100 characters const SEARCH_CHAR_LIMIT = 100 +// matches # of results we show for search suggestions +const SEARCH_RESULTS_DISPLAY_LIMIT = 14 + // 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' @@ -205,5 +208,6 @@ export { MOBILE_WIDTH_THRESHOLD, PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD, SEARCH_CHAR_LIMIT, + SEARCH_RESULTS_DISPLAY_LIMIT, ABOUT_BITCOIN_ADDRESS, } diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 0dea2b751db6d..37395194dc1d2 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -63,13 +63,13 @@ export default defineComponent({ type: Array, default: () => { return [] } }, - searchResultIcon: { + dataListProperties: { type: Array, - default: null + default: () => { return [] } }, - canRemoveResults: { - type: Boolean, - default: false + searchResultIconNames: { + type: Array, + default: null }, showDataWhenEmpty: { type: Boolean, @@ -265,7 +265,7 @@ export default defineComponent({ }, handleRemoveClick: function (index) { - if (!this.canRemoveResults) { return } + if (!this.dataListProperties[index]?.isRemoveable) { return } // keep focus in input even if removed "Remove" button was clicked this.$refs.input.focus() @@ -328,6 +328,16 @@ export default defineComponent({ } }, + getIconFor: function (index) { + if (this.dataListTypes[index] === 'search-history') { + return 'clock-rotate-left' + } else if (this.dataListTypes[index] === 'search-suggestion') { + return 'magnifying-glass' + } + + return null + }, + updateSelectedOptionIndex: function (index) { this.searchState.selectedOption = index // Allow deselecting suggestion @@ -351,6 +361,7 @@ export default defineComponent({ updateVisibleDataList: function () { // Reset selected option before it's updated + // 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 diff --git a/src/renderer/components/ft-input/ft-input.vue b/src/renderer/components/ft-input/ft-input.vue index a0046ada87c23..6960c95ad87a4 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -85,13 +85,13 @@ @mouseleave="searchState.selectedOption = -1; removeButtonSelectedIndex = -1" > {{ entry }} { + const isSearchHistoryEntry = searchHistoryNameLength > i + return isSearchHistoryEntry + ? { isRemoveable: true, iconName: 'clock-rotate-left' } + : { isRemoveable: false, iconName: 'magnifying-glass' } + }) }, }, watch: { diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue index e44a23b1beda5..c9309df3c7a1e 100644 --- a/src/renderer/components/top-nav/top-nav.vue +++ b/src/renderer/components/top-nav/top-nav.vue @@ -93,11 +93,10 @@ class="searchInput" :is-search="true" :data-list="activeDataList" - :search-result-icon="searchResultIcon" + :data-list-properties="activeDataListProperties" :spellcheck="false" :show-clear-text-button="true" :show-data-when-empty="true" - :can-remove-results="usingSearchHistoryResults" @input="getSearchSuggestionsDebounce" @click="goToSearch" @clear="() => lastSuggestionQuery = ''" diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js index 149b3ef76d866..c93528b91b223 100644 --- a/src/renderer/store/modules/search-history.js +++ b/src/renderer/store/modules/search-history.js @@ -1,7 +1,8 @@ +import { SEARCH_RESULTS_DISPLAY_LIMIT } from '../../../constants' import { DBSearchHistoryHandlers } from '../../../datastores/handlers/index' -// matches # of results we show for search suggestions -const SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 14 +// maximum number of search history results to display when mixed with regular YT search suggestions +const MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 4 const state = { searchHistoryEntries: [] @@ -12,8 +13,23 @@ const getters = { return state.searchHistoryEntries }, - getLatestUniqueSearchHistoryNames: (state) => { - return state.searchHistoryEntries.slice(0, SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT).map((entry) => entry.name) + getLatestSearchHistoryNames: (state) => { + return state.searchHistoryEntries.slice(0, SEARCH_RESULTS_DISPLAY_LIMIT).map((entry) => entry.name) + }, + + getLatestMatchingSearchHistoryNames: (state) => (name) => { + const matches = [] + for (const entry of state.searchHistoryEntries) { + if (entry.name.startsWith(name)) { + matches.push(entry.name) + if (matches.length === MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT) { + break + } + } + } + + // prioritize more concise matches + return matches.toSorted((a, b) => a.length - b.length) }, getSearchHistoryEntryWithId: (state) => (id) => { From 08614b056dd69ca0ad9b54948c887c8a4cc0d409 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Sat, 28 Dec 2024 00:33:39 -0600 Subject: [PATCH 13/24] Improve code commenting --- src/renderer/components/ft-input/ft-input.js | 46 +++++++------------- src/renderer/components/top-nav/top-nav.js | 8 ++-- src/renderer/store/modules/search-history.js | 31 +------------ 3 files changed, 21 insertions(+), 64 deletions(-) diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 37395194dc1d2..2fef45a71c890 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -121,6 +121,7 @@ export default defineComponent({ inputDataPresent: function () { return this.inputDataDisplayed.length > 0 }, + inputDataDisplayed() { if (!this.isSearch) { return this.inputData } @@ -134,7 +135,7 @@ export default defineComponent({ searchStateKeyboardSelectedOptionValue() { if (this.searchState.keyboardSelectedOptionIndex === -1) { return null } - return this.getTextForArrayAtIndex(this.visibleDataList, this.searchState.keyboardSelectedOptionIndex) + return this.visibleDataList[this.searchState.keyboardSelectedOptionIndex] }, }, watch: { @@ -160,9 +161,6 @@ export default defineComponent({ this.updateVisibleDataList() }, methods: { - getTextForArrayAtIndex: function (array, index) { - return array[index] - }, handleClick: function (e) { // No action if no input text if (!this.inputDataPresent) { @@ -267,7 +265,7 @@ export default defineComponent({ handleRemoveClick: function (index) { if (!this.dataListProperties[index]?.isRemoveable) { return } - // keep focus in input even if removed "Remove" button was clicked + // 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]) @@ -277,27 +275,29 @@ export default defineComponent({ * @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.removeButtonSelectedIndex !== -1) { this.handleRemoveClick(this.removeButtonSelectedIndex) } else if (this.searchState.selectedOption !== -1) { this.searchState.showOptions = false event.preventDefault() - this.inputData = this.getTextForArrayAtIndex(this.visibleDataList, this.searchState.selectedOption) + this.inputData = this.visibleDataList[this.searchState.selectedOption] this.handleOptionClick(this.searchState.selectedOption) } else { this.handleClick(event) } - // Early return + return } if (this.visibleDataList.length === 0) { return } this.searchState.showOptions = true - const isArrow = event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' - if (!isArrow) { + + const isArrowKeyPress = event.key.startsWith('Arrow') + + if (!isArrowKeyPress) { const selectedOptionValue = this.searchStateKeyboardSelectedOptionValue // Keyboard selected & is char if (!isNullOrEmpty(selectedOptionValue) && isKeyboardEventKeyPrintableChar(event.key)) { @@ -310,34 +310,20 @@ export default defineComponent({ } event.preventDefault() - - if (event.key === 'ArrowRight') { - this.removeButtonSelectedIndex = this.searchState.selectedOption - } - if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { const newIndex = this.searchState.selectedOption + (event.key === 'ArrowDown' ? 1 : -1) this.updateSelectedOptionIndex(newIndex) - // reset removal + // unset selection of "Remove" button this.removeButtonSelectedIndex = -1 + } else { + // "select" the Remove button through right arrow navigation, and unselect it with the left arrow + const newIndex = event.key === 'ArrowRight' ? this.searchState.selectedOption : -1 + this.removeButtonSelectedIndex = newIndex } - - if (this.searchState.selectedOption !== -1 && event.key === 'ArrowRight') { - this.removeButtonSelectedIndex = this.searchState.selectedOption - } - }, - - getIconFor: function (index) { - if (this.dataListTypes[index] === 'search-history') { - return 'clock-rotate-left' - } else if (this.dataListTypes[index] === 'search-suggestion') { - return 'magnifying-glass' - } - - return null }, + // Updates the selected dropdown option index and handles the under/over-flow behavior updateSelectedOptionIndex: function (index) { this.searchState.selectedOption = index // Allow deselecting suggestion diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 5eedfdb18f176..6b848d551a27b 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -133,19 +133,19 @@ export default defineComponent({ return this.$store.getters.getLatestSearchHistoryNames }, - // show latest search history when the search bar is empty activeDataList: function () { if (!this.enableSearchSuggestions) { return [] } + // show latest search history when the search bar is empty if (this.usingOnlySearchHistoryResults) { return this.$store.getters.getLatestSearchHistoryNames } const searchResults = [...this.latestMatchingSearchHistoryNames] for (const searchSuggestion of this.searchSuggestionsDataList) { - // prevent duplicate results + // prevent duplicate results between search history entries and YT search suggestions if (this.latestMatchingSearchHistoryNames.includes(searchSuggestion)) { continue } @@ -161,9 +161,9 @@ export default defineComponent({ }, activeDataListProperties: function () { - const searchHistoryNameLength = this.usingOnlySearchHistoryResults ? this.latestSearchHistoryNames.length : this.latestMatchingSearchHistoryNames.length + const searchHistoryEntriesCount = this.usingOnlySearchHistoryResults ? this.latestSearchHistoryNames.length : this.latestMatchingSearchHistoryNames.length return this.activeDataList.map((_, i) => { - const isSearchHistoryEntry = searchHistoryNameLength > i + const isSearchHistoryEntry = i < searchHistoryEntriesCount return isSearchHistoryEntry ? { isRemoveable: true, iconName: 'clock-rotate-left' } : { isRemoveable: false, iconName: 'magnifying-glass' } diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js index c93528b91b223..a0494f4eb0592 100644 --- a/src/renderer/store/modules/search-history.js +++ b/src/renderer/store/modules/search-history.js @@ -36,26 +36,6 @@ const getters = { const searchHistoryEntry = state.searchHistoryEntries.find(p => p._id === id) return searchHistoryEntry }, - - getSearchHistoryIdsForMatchingUserPlaylistIds: (state) => (playlistIds) => { - const searchHistoryIds = [] - const allSearchHistoryEntries = state.searchHistoryEntries - const searchHistoryEntryLimitedIdsMap = new Map() - allSearchHistoryEntries.forEach((searchHistoryEntry) => { - searchHistoryEntryLimitedIdsMap.set(searchHistoryEntry._id, searchHistoryEntry._id) - }) - - playlistIds.forEach((playlistId) => { - const id = `/playlist/${playlistId}?playlistType=user&searchQueryText=` - if (!searchHistoryEntryLimitedIdsMap.has(id)) { - return - } - - searchHistoryIds.push(searchHistoryEntryLimitedIdsMap.get(id)) - }) - - return searchHistoryIds - } } const actions = { async grabSearchHistoryEntries({ commit }) { @@ -103,15 +83,6 @@ const actions = { } }, - async removeUserPlaylistSearchHistoryEntries({ dispatch, getters }, userPlaylistIds) { - const searchHistoryIds = getters.getSearchHistoryIdsForMatchingUserPlaylistIds(userPlaylistIds) - if (searchHistoryIds.length === 0) { - return - } - - dispatch('removeSearchHistoryEntries', searchHistoryIds) - }, - async removeAllSearchHistoryEntries({ commit }) { try { await DBSearchHistoryHandlers.deleteAll() @@ -133,7 +104,7 @@ const mutations = { upsertSearchHistoryEntryToList(state, updatedSearchHistoryEntry) { state.searchHistoryEntries = state.searchHistoryEntries.filter((p) => { - return p.name !== updatedSearchHistoryEntry._id + return p._id !== updatedSearchHistoryEntry._id }) state.searchHistoryEntries.unshift(updatedSearchHistoryEntry) From f27e1d505e0f8c75905365e46e1ca4e89b06edde Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Sat, 28 Dec 2024 14:01:38 -0600 Subject: [PATCH 14/24] Implement code review changes Fix preventDefault being called for arrow left/right in search input. Revert ellipsis changes for overly long search results. Adjust matching search history logic to be non-heuristic. --- src/constants.js | 6 +++- src/renderer/components/ft-input/ft-input.css | 3 -- src/renderer/components/ft-input/ft-input.js | 32 ++++++++----------- src/renderer/components/top-nav/top-nav.js | 3 +- src/renderer/store/modules/search-history.js | 16 ++-------- src/renderer/views/Search/Search.js | 8 ++++- 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/constants.js b/src/constants.js index 33da8dd0587cc..53d4612bdc65d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -193,9 +193,12 @@ const PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD = 500 // YouTube search character limit is 100 characters const SEARCH_CHAR_LIMIT = 100 -// matches # of results we show for search suggestions +// 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' @@ -209,5 +212,6 @@ export { 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/renderer/components/ft-input/ft-input.css b/src/renderer/components/ft-input/ft-input.css index 9587079ad6665..b9904e56f6e64 100644 --- a/src/renderer/components/ft-input/ft-input.css +++ b/src/renderer/components/ft-input/ft-input.css @@ -207,9 +207,6 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp padding-block: 0; line-height: 2rem; padding-inline: 15px; - text-overflow: ellipsis; - overflow-x: hidden; - white-space: nowrap; } .searchResultIcon { diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 2fef45a71c890..68f305ffb151e 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -295,37 +295,33 @@ export default defineComponent({ this.searchState.showOptions = true - const isArrowKeyPress = event.key.startsWith('Arrow') - - if (!isArrowKeyPress) { + // "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 - } - - event.preventDefault() - if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { - const newIndex = this.searchState.selectedOption + (event.key === 'ArrowDown' ? 1 : -1) - this.updateSelectedOptionIndex(newIndex) - - // unset selection of "Remove" button - this.removeButtonSelectedIndex = -1 - } else { - // "select" the Remove button through right arrow navigation, and unselect it with the left arrow - const newIndex = event.key === 'ArrowRight' ? this.searchState.selectedOption : -1 - this.removeButtonSelectedIndex = newIndex } }, // 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 + // Allow deselecting suggestion if (this.searchState.selectedOption < -1) { this.searchState.selectedOption = this.visibleDataList.length - 1 diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 6b848d551a27b..9f2255356b928 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -5,7 +5,7 @@ import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue' import FtIconButton from '../ft-icon-button/ft-icon-button.vue' import debounce from 'lodash.debounce' -import { IpcChannels, KeyboardShortcuts, MOBILE_WIDTH_THRESHOLD, SEARCH_RESULTS_DISPLAY_LIMIT } from '../../../constants' +import { IpcChannels, KeyboardShortcuts, MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT, MOBILE_WIDTH_THRESHOLD, SEARCH_RESULTS_DISPLAY_LIMIT } from '../../../constants' import { localizeAndAddKeyboardShortcutToActionTitle, openInternalPath } from '../../helpers/utils' import { translateWindowTitle } from '../../helpers/strings' import { clearLocalSearchSuggestionsSession, getLocalSearchSuggestions } from '../../helpers/api/local' @@ -127,6 +127,7 @@ export default defineComponent({ latestMatchingSearchHistoryNames: function () { return this.$store.getters.getLatestMatchingSearchHistoryNames(this.lastSuggestionQuery) + .slice(0, MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT) }, latestSearchHistoryNames: function () { diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js index a0494f4eb0592..888e63451cd01 100644 --- a/src/renderer/store/modules/search-history.js +++ b/src/renderer/store/modules/search-history.js @@ -1,9 +1,6 @@ import { SEARCH_RESULTS_DISPLAY_LIMIT } from '../../../constants' import { DBSearchHistoryHandlers } from '../../../datastores/handlers/index' -// maximum number of search history results to display when mixed with regular YT search suggestions -const MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 4 - const state = { searchHistoryEntries: [] } @@ -18,18 +15,11 @@ const getters = { }, getLatestMatchingSearchHistoryNames: (state) => (name) => { - const matches = [] - for (const entry of state.searchHistoryEntries) { - if (entry.name.startsWith(name)) { - matches.push(entry.name) - if (matches.length === MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT) { - break - } - } - } + const matches = state.searchHistoryEntries.filter((entry) => entry.name.startsWith(name)) // prioritize more concise matches - return matches.toSorted((a, b) => a.length - b.length) + return matches.map((entry) => entry.name) + .toSorted((a, b) => a.length - b.length) }, getSearchHistoryEntryWithId: (state) => (id) => { diff --git a/src/renderer/views/Search/Search.js b/src/renderer/views/Search/Search.js index 820c6eed7249f..1bdf9f34539a6 100644 --- a/src/renderer/views/Search/Search.js +++ b/src/renderer/views/Search/Search.js @@ -50,6 +50,10 @@ export default defineComponent({ showFamilyFriendlyOnly: function() { return this.$store.getters.getShowFamilyFriendlyOnly }, + + enableSearchSuggestions: function () { + return this.$store.getters.getEnableSearchSuggestions + }, }, watch: { $route () { @@ -145,7 +149,9 @@ export default defineComponent({ } } - this.updateSearchHistoryEntry() + if (this.enableSearchSuggestions) { + this.updateSearchHistoryEntry() + } }, performSearchLocal: async function (payload) { From ea73ad13e97fbe5f17e4132c0662b31c624ac368 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Sat, 28 Dec 2024 20:04:13 -0600 Subject: [PATCH 15/24] Fix text overflow issue with long search history entries --- src/renderer/components/ft-input/ft-input.css | 3 ++- src/renderer/components/ft-input/ft-input.vue | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/ft-input/ft-input.css b/src/renderer/components/ft-input/ft-input.css index b9904e56f6e64..d0223f9a5cbb6 100644 --- a/src/renderer/components/ft-input/ft-input.css +++ b/src/renderer/components/ft-input/ft-input.css @@ -203,7 +203,8 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp } .list li { - display: block; + display: flex; + justify-content: space-between; padding-block: 0; line-height: 2rem; padding-inline: 15px; diff --git a/src/renderer/components/ft-input/ft-input.vue b/src/renderer/components/ft-input/ft-input.vue index 6960c95ad87a4..0177c818e1309 100644 --- a/src/renderer/components/ft-input/ft-input.vue +++ b/src/renderer/components/ft-input/ft-input.vue @@ -84,12 +84,14 @@ @mouseenter="searchState.selectedOption = index" @mouseleave="searchState.selectedOption = -1; removeButtonSelectedIndex = -1" > - - {{ entry }} +
+ + {{ entry }} +
Date: Wed, 1 Jan 2025 17:51:43 -0600 Subject: [PATCH 16/24] Remove 'name' field and unused DB methods --- src/datastores/handlers/base.js | 8 ----- src/datastores/handlers/electron.js | 14 -------- src/main/index.js | 18 ----------- src/renderer/store/modules/search-history.js | 34 +++----------------- src/renderer/store/modules/settings.js | 8 ----- src/renderer/views/Search/Search.js | 1 - 6 files changed, 4 insertions(+), 79 deletions(-) diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index d52b819568ae4..d25963376f641 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -227,10 +227,6 @@ class Playlists { } class SearchHistory { - static create(searchHistoryEntry) { - return db.searchHistory.insertAsync(searchHistoryEntry) - } - static find() { return db.searchHistory.findAsync({}).sort({ lastUpdatedAt: -1 }) } @@ -243,10 +239,6 @@ class SearchHistory { return db.searchHistory.removeAsync({ _id: _id }) } - static deleteMultiple(ids) { - return db.searchHistory.removeAsync({ _id: { $in: ids } }) - } - static deleteAll() { return db.searchHistory.removeAsync({}, { multi: true }) } diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index 531033985e511..7a178174f0db0 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -219,13 +219,6 @@ class Playlists { } class SearchHistory { - static create(searchHistoryEntry) { - return ipcRenderer.invoke( - IpcChannels.DB_SEARCH_HISTORY, - { action: DBActions.GENERAL.CREATE, data: searchHistoryEntry } - ) - } - static find() { return ipcRenderer.invoke( IpcChannels.DB_SEARCH_HISTORY, @@ -247,13 +240,6 @@ class SearchHistory { ) } - static deleteMultiple(ids) { - return ipcRenderer.invoke( - IpcChannels.DB_SEARCH_HISTORY, - { action: DBActions.GENERAL.DELETE_MULTIPLE, data: ids } - ) - } - static deleteAll() { return ipcRenderer.invoke( IpcChannels.DB_SEARCH_HISTORY, diff --git a/src/main/index.js b/src/main/index.js index 85ac44a8a2f6c..8bf558269a971 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1356,15 +1356,6 @@ function runApp() { ipcMain.handle(IpcChannels.DB_SEARCH_HISTORY, async (event, { action, data }) => { try { switch (action) { - case DBActions.GENERAL.CREATE: { - const searchHistoryEntry = await baseHandlers.searchHistory.create(data) - syncOtherWindows( - IpcChannels.SYNC_SEARCH_HISTORY, - event, - { event: SyncEvents.GENERAL.CREATE, data } - ) - return searchHistoryEntry - } case DBActions.GENERAL.FIND: return await baseHandlers.searchHistory.find() @@ -1386,15 +1377,6 @@ function runApp() { ) return null - case DBActions.GENERAL.DELETE_MULTIPLE: - await baseHandlers.searchHistory.deleteMultiple(data) - syncOtherWindows( - IpcChannels.SYNC_SEARCH_HISTORY, - event, - { event: SyncEvents.GENERAL.DELETE_MULTIPLE, data } - ) - return null - case DBActions.GENERAL.DELETE_ALL: await baseHandlers.searchHistory.deleteAll() return null diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js index 888e63451cd01..e0945ffc4fb7a 100644 --- a/src/renderer/store/modules/search-history.js +++ b/src/renderer/store/modules/search-history.js @@ -11,14 +11,14 @@ const getters = { }, getLatestSearchHistoryNames: (state) => { - return state.searchHistoryEntries.slice(0, SEARCH_RESULTS_DISPLAY_LIMIT).map((entry) => entry.name) + return state.searchHistoryEntries.slice(0, SEARCH_RESULTS_DISPLAY_LIMIT).map((entry) => entry._id) }, - getLatestMatchingSearchHistoryNames: (state) => (name) => { - const matches = state.searchHistoryEntries.filter((entry) => entry.name.startsWith(name)) + getLatestMatchingSearchHistoryNames: (state) => (id) => { + const matches = state.searchHistoryEntries.filter((entry) => entry._id.startsWith(id)) // prioritize more concise matches - return matches.map((entry) => entry.name) + return matches.map((entry) => entry._id) .toSorted((a, b) => a.length - b.length) }, @@ -37,15 +37,6 @@ const actions = { } }, - async createSearchHistoryEntry({ commit }, searchHistoryEntry) { - try { - const newSearchHistoryEntry = await DBSearchHistoryHandlers.create(searchHistoryEntry) - commit('addSearchHistoryEntryToList', newSearchHistoryEntry) - } catch (errMessage) { - console.error(errMessage) - } - }, - async updateSearchHistoryEntry({ commit }, searchHistoryEntry) { try { await DBSearchHistoryHandlers.upsert(searchHistoryEntry) @@ -64,15 +55,6 @@ const actions = { } }, - async removeSearchHistoryEntries({ commit }, ids) { - try { - await DBSearchHistoryHandlers.deleteMultiple(ids) - commit('removeSearchHistoryEntriesFromList', ids) - } catch (errMessage) { - console.error(errMessage) - } - }, - async removeAllSearchHistoryEntries({ commit }) { try { await DBSearchHistoryHandlers.deleteAll() @@ -84,10 +66,6 @@ const actions = { } const mutations = { - addSearchHistoryEntryToList(state, searchHistoryEntry) { - state.searchHistoryEntries.unshift(searchHistoryEntry) - }, - setSearchHistoryEntries(state, searchHistoryEntries) { state.searchHistoryEntries = searchHistoryEntries }, @@ -102,10 +80,6 @@ const mutations = { removeSearchHistoryEntryFromList(state, _id) { state.searchHistoryEntries = state.searchHistoryEntries.filter((searchHistoryEntry) => searchHistoryEntry._id !== _id) - }, - - removeSearchHistoryEntriesFromList(state, ids) { - state.searchHistoryEntries = state.searchHistoryEntries.filter((searchHistoryEntry) => !ids.includes(searchHistoryEntry._id)) } } diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js index eda17593209f5..997263c41a6b9 100644 --- a/src/renderer/store/modules/settings.js +++ b/src/renderer/store/modules/settings.js @@ -515,10 +515,6 @@ const customActions = { ipcRenderer.on(IpcChannels.SYNC_SEARCH_HISTORY, (_, { event, data }) => { switch (event) { - case SyncEvents.GENERAL.CREATE: - commit('addSearchHistoryEntryToList', data) - break - case SyncEvents.GENERAL.UPSERT: commit('upsertSearchHistoryEntryToList', data) break @@ -527,10 +523,6 @@ const customActions = { commit('removeSearchHistoryEntryFromList', data) break - case SyncEvents.GENERAL.DELETE_MULTIPLE: - commit('removeSearchHistoryEntriesFromList', data) - break - default: console.error('search history: invalid sync event received') } diff --git a/src/renderer/views/Search/Search.js b/src/renderer/views/Search/Search.js index 1bdf9f34539a6..e4e26e21c4adc 100644 --- a/src/renderer/views/Search/Search.js +++ b/src/renderer/views/Search/Search.js @@ -113,7 +113,6 @@ export default defineComponent({ updateSearchHistoryEntry: function () { const persistentSearchHistoryPayload = { _id: this.query, - name: this.query, lastUpdatedAt: new Date() } From 613fd44d980c1d06ca19d0577c676beea9f9ad9b Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Wed, 1 Jan 2025 18:04:32 -0600 Subject: [PATCH 17/24] Implement 'Remember Search History' setting and rename 'Remember History' to 'Remember Watch History' --- src/main/index.js | 5 +++++ .../components/privacy-settings/privacy-settings.js | 4 ++++ .../components/privacy-settings/privacy-settings.vue | 8 ++++++++ src/renderer/store/modules/settings.js | 5 +++++ src/renderer/views/Search/Search.js | 6 +++--- static/locales/en-GB.yaml | 3 ++- static/locales/en-US.yaml | 3 ++- 7 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index 8bf558269a971..1a7544a6ea7b3 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1379,6 +1379,11 @@ function runApp() { case DBActions.GENERAL.DELETE_ALL: await baseHandlers.searchHistory.deleteAll() + syncOtherWindows( + IpcChannels.SYNC_SEARCH_HISTORY, + event, + { event: SyncEvents.GENERAL.DELETE_ALL } + ) return null default: diff --git a/src/renderer/components/privacy-settings/privacy-settings.js b/src/renderer/components/privacy-settings/privacy-settings.js index 58c334e0623e6..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 }, @@ -119,6 +122,7 @@ export default defineComponent({ ...mapActions([ 'updateRememberHistory', 'removeAllHistory', + 'updateRememberSearchHistory', 'updateSaveWatchedProgress', 'updateSaveVideoHistoryWithLastViewedPlaylist', 'clearSessionSearchHistory', diff --git a/src/renderer/components/privacy-settings/privacy-settings.vue b/src/renderer/components/privacy-settings/privacy-settings.vue index 5d4c9bc509e1a..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" />
+
+ +
Date: Thu, 2 Jan 2025 21:25:52 +0000 Subject: [PATCH 18/24] Apply suggestions from code review Co-authored-by: absidue <48293849+absidue@users.noreply.github.com> --- src/renderer/store/modules/search-history.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/renderer/store/modules/search-history.js b/src/renderer/store/modules/search-history.js index e0945ffc4fb7a..df03788a2f610 100644 --- a/src/renderer/store/modules/search-history.js +++ b/src/renderer/store/modules/search-history.js @@ -19,12 +19,11 @@ const getters = { // prioritize more concise matches return matches.map((entry) => entry._id) - .toSorted((a, b) => a.length - b.length) + .sort((a, b) => a.length - b.length) }, getSearchHistoryEntryWithId: (state) => (id) => { - const searchHistoryEntry = state.searchHistoryEntries.find(p => p._id === id) - return searchHistoryEntry + return state.searchHistoryEntries.find(p => p._id === id) }, } const actions = { From c27b89b502d10dd061c15fea4825719f28f312d2 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 2 Jan 2025 15:36:17 -0600 Subject: [PATCH 19/24] Update to not show search history suggestions if 'Remember Search History' is disabled --- src/renderer/components/top-nav/top-nav.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 9f2255356b928..7f2cbe96bcf0b 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -122,15 +122,27 @@ export default defineComponent({ }, usingOnlySearchHistoryResults: function () { - return this.lastSuggestionQuery === '' + return this.rememberSearchHistory && this.lastSuggestionQuery === '' + }, + + rememberSearchHistory: function () { + return this.$store.getters.getRememberSearchHistory }, latestMatchingSearchHistoryNames: function () { + if (!this.rememberSearchHistory) { + return [] + } + return this.$store.getters.getLatestMatchingSearchHistoryNames(this.lastSuggestionQuery) .slice(0, MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT) }, latestSearchHistoryNames: function () { + if (!this.rememberSearchHistory) { + return [] + } + return this.$store.getters.getLatestSearchHistoryNames }, From 11098f009d0d0e34d90ffa338f232026c00fb947 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 2 Jan 2025 16:26:25 -0600 Subject: [PATCH 20/24] Update logic to make 'Enable Search Suggestions' being false still allow showing search history results --- src/renderer/components/top-nav/top-nav.js | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 7f2cbe96bcf0b..bb1be065dd39e 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -147,26 +147,25 @@ export default defineComponent({ }, activeDataList: function () { - if (!this.enableSearchSuggestions) { - return [] - } - // show latest search history when the search bar is empty if (this.usingOnlySearchHistoryResults) { return this.$store.getters.getLatestSearchHistoryNames } const searchResults = [...this.latestMatchingSearchHistoryNames] - for (const searchSuggestion of this.searchSuggestionsDataList) { - // prevent duplicate results between search history entries and YT search suggestions - if (this.latestMatchingSearchHistoryNames.includes(searchSuggestion)) { - continue - } - searchResults.push(searchSuggestion) + if (this.enableSearchSuggestions) { + for (const searchSuggestion of this.searchSuggestionsDataList) { + // prevent duplicate results between search history entries and YT search suggestions + if (this.latestMatchingSearchHistoryNames.includes(searchSuggestion)) { + continue + } + + searchResults.push(searchSuggestion) - if (searchResults.length === SEARCH_RESULTS_DISPLAY_LIMIT) { - break + if (searchResults.length === SEARCH_RESULTS_DISPLAY_LIMIT) { + break + } } } From cd8a1d1b3a7e33476b1e6445079f4a9e48cd946e Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 2 Jan 2025 17:08:06 -0600 Subject: [PATCH 21/24] Revert hiding old search history entries if rememberSearchHistory is disabled --- src/renderer/components/top-nav/top-nav.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index bb1be065dd39e..6ed717333c914 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -122,27 +122,15 @@ export default defineComponent({ }, usingOnlySearchHistoryResults: function () { - return this.rememberSearchHistory && this.lastSuggestionQuery === '' - }, - - rememberSearchHistory: function () { - return this.$store.getters.getRememberSearchHistory + return this.lastSuggestionQuery === '' }, latestMatchingSearchHistoryNames: function () { - if (!this.rememberSearchHistory) { - return [] - } - return this.$store.getters.getLatestMatchingSearchHistoryNames(this.lastSuggestionQuery) .slice(0, MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT) }, latestSearchHistoryNames: function () { - if (!this.rememberSearchHistory) { - return [] - } - return this.$store.getters.getLatestSearchHistoryNames }, From fd5b612bc5ee08bbf5dbebca2f3acbdc404608b3 Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Thu, 2 Jan 2025 18:20:06 -0600 Subject: [PATCH 22/24] Fix searchOptions not closing in edge case Bug scenario: 1. click search history entry, 2. click clear text button, 3. see state of searchOptions not being closeable until re-interacting with the input --- src/renderer/components/ft-input/ft-input.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 68f305ffb151e..1c0d5a132d463 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -192,6 +192,7 @@ export default defineComponent({ this.inputData = '' this.handleActionIconChange() this.updateVisibleDataList() + this.searchState.isPointerInList = false this.$refs.input.value = '' From ebac58b283fe3508e8001a481ee858852b544bad Mon Sep 17 00:00:00 2001 From: Jason <84899178+kommunarr@users.noreply.github.com> Date: Sun, 5 Jan 2025 17:33:48 +0000 Subject: [PATCH 23/24] Update src/renderer/views/Search/Search.js Co-authored-by: absidue <48293849+absidue@users.noreply.github.com> --- src/renderer/views/Search/Search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/views/Search/Search.js b/src/renderer/views/Search/Search.js index 9580a18c253c9..d7bd2f74a896d 100644 --- a/src/renderer/views/Search/Search.js +++ b/src/renderer/views/Search/Search.js @@ -113,7 +113,7 @@ export default defineComponent({ updateSearchHistoryEntry: function () { const persistentSearchHistoryPayload = { _id: this.query, - lastUpdatedAt: new Date() + lastUpdatedAt: Date.now() } this.$store.dispatch('updateSearchHistoryEntry', persistentSearchHistoryPayload) From c8b08fe7c78b5cab2be87d9a5620d1fc09092dee Mon Sep 17 00:00:00 2001 From: Jason Henriquez Date: Sun, 5 Jan 2025 11:53:48 -0600 Subject: [PATCH 24/24] Add bolding to selected/focused 'Remove' button --- src/renderer/components/ft-input/ft-input.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/ft-input/ft-input.css b/src/renderer/components/ft-input/ft-input.css index d0223f9a5cbb6..9ad55ce7f9c1f 100644 --- a/src/renderer/components/ft-input/ft-input.css +++ b/src/renderer/components/ft-input/ft-input.css @@ -224,8 +224,9 @@ body[dir='rtl'] .ft-input-component.search.showClearTextButton:focus-within .inp } .removeButton:hover, -.removeButton.removeButtonSelected { +.removeButtonSelected { text-decoration: underline; + font-weight: bold; } .hover {