import Vue from 'vue' import { mapActions } from 'vuex' import FtInput from '../ft-input/ft-input.vue' import FtSearchFilters from '../ft-search-filters/ft-search-filters.vue' import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue' import debounce from 'lodash.debounce' import { IpcChannels } from '../../../constants' import { openInternalPath, showToast } from '../../helpers/utils' import { clearLocalSearchSuggestionsSession, getLocalSearchSuggestions } from '../../helpers/api/local' export default Vue.extend({ name: 'TopNav', components: { FtInput, FtSearchFilters, FtProfileSelector }, data: () => { return { component: this, showSearchContainer: true, showFilters: false, searchFilterValueChanged: false, historyIndex: 1, isForwardOrBack: false, searchSuggestionsDataList: [], lastSuggestionQuery: '' } }, computed: { hideSearchBar: function () { return this.$store.getters.getHideSearchBar }, hideHeaderLogo: function () { return this.$store.getters.getHideHeaderLogo }, enableSearchSuggestions: function () { return this.$store.getters.getEnableSearchSuggestions }, searchInput: function () { return this.$refs.searchInput.$refs.input }, searchSettings: function () { return this.$store.getters.getSearchSettings }, barColor: function () { return this.$store.getters.getBarColor }, currentInvidiousInstance: function () { return this.$store.getters.getCurrentInvidiousInstance }, backendFallback: function () { return this.$store.getters.getBackendFallback }, backendPreference: function () { return this.$store.getters.getBackendPreference }, expandSideBar: function () { return this.$store.getters.getExpandSideBar }, forwardText: function () { return this.$t('Forward') }, backwardText: function () { return this.$t('Back') }, newWindowText: function () { return this.$t('Open New Window') } }, mounted: function () { let previousWidth = window.innerWidth if (window.innerWidth <= 680) { this.showSearchContainer = false } // Store is not up-to-date when the component mounts, so we use timeout. setTimeout(() => { if (this.expandSideBar) { this.toggleSideNav() } }, 0) window.addEventListener('resize', () => { // Don't change the status of showSearchContainer if only the height of the window changes // Opening the virtual keyboard can trigger this resize event, but it won't change the width if (previousWidth !== window.innerWidth) { this.showSearchContainer = window.innerWidth > 680 previousWidth = window.innerWidth } }) this.debounceSearchResults = debounce(this.getSearchSuggestions, 200) }, methods: { goToSearch: async function (query, { event }) { const doCreateNewWindow = event && event.shiftKey if (window.innerWidth <= 680) { this.$refs.searchContainer.blur() this.showSearchContainer = false } else { this.searchInput.blur() } clearLocalSearchSuggestionsSession() this.getYoutubeUrlInfo(query).then((result) => { switch (result.urlType) { case 'video': { const { videoId, timestamp, playlistId } = result const query = {} if (timestamp) { query.timestamp = timestamp } if (playlistId && playlistId.length > 0) { query.playlistId = playlistId } openInternalPath({ path: `/watch/${videoId}`, query, doCreateNewWindow }) break } case 'playlist': { const { playlistId, query } = result openInternalPath({ path: `/playlist/${playlistId}`, query, doCreateNewWindow }) break } case 'search': { const { searchQuery, query } = result openInternalPath({ path: `/search/${encodeURIComponent(searchQuery)}`, query, doCreateNewWindow, searchQueryText: searchQuery }) break } case 'hashtag': { // TODO: Implement a hashtag related view let message = 'Hashtags have not yet been implemented, try again later' if (this.$t(message) && this.$t(message) !== '') { message = this.$t(message) } showToast(message) break } case 'channel': { const { channelId, idType, subPath } = result openInternalPath({ path: `/channel/${channelId}/${subPath}`, query: { idType }, doCreateNewWindow }) break } case 'invalid_url': default: { openInternalPath({ path: `/search/${encodeURIComponent(query)}`, query: { sortBy: this.searchSettings.sortBy, time: this.searchSettings.time, type: this.searchSettings.type, duration: this.searchSettings.duration }, doCreateNewWindow, searchQueryText: query }) } } }) // Close the filter panel this.showFilters = false }, focusSearch: function () { if (!this.hideSearchBar) { this.searchInput.focus() } }, getSearchSuggestionsDebounce: function (query) { if (this.enableSearchSuggestions) { const trimmedQuery = query.trim() if (trimmedQuery !== this.lastSuggestionQuery) { this.lastSuggestionQuery = trimmedQuery this.debounceSearchResults(trimmedQuery) } } }, getSearchSuggestions: function (query) { switch (this.backendPreference) { case 'local': this.getSearchSuggestionsLocal(query) break case 'invidious': this.getSearchSuggestionsInvidious(query) break } }, getSearchSuggestionsLocal: function (query) { if (query === '') { this.searchSuggestionsDataList = [] return } getLocalSearchSuggestions(query).then((results) => { this.searchSuggestionsDataList = results }) }, getSearchSuggestionsInvidious: function (query) { if (query === '') { this.searchSuggestionsDataList = [] return } const searchPayload = { resource: 'search/suggestions', id: '', params: { q: query } } this.invidiousAPICall(searchPayload).then((results) => { this.searchSuggestionsDataList = results.suggestions }).catch((err) => { console.error(err) if (this.backendFallback) { console.error( 'Error gettings search suggestions. Falling back to Local API' ) this.getSearchSuggestionsLocal(query) } }) }, toggleSearchContainer: function () { this.showSearchContainer = !this.showSearchContainer this.showFilters = false }, handleSearchFilterValueChanged: function (filterValueChanged) { this.searchFilterValueChanged = filterValueChanged }, navigateHistory: function () { if (!this.isForwardOrBack) { this.historyIndex = window.history.length this.$refs.historyArrowBack.classList.remove('fa-arrow-left') this.$refs.historyArrowForward.classList.add('fa-arrow-right') } else { this.isForwardOrBack = false } }, historyBack: function () { this.isForwardOrBack = true window.history.back() if (this.historyIndex > 1) { this.historyIndex-- this.$refs.historyArrowForward.classList.remove('fa-arrow-right') if (this.historyIndex === 1) { this.$refs.historyArrowBack.classList.add('fa-arrow-left') } } }, historyForward: function () { this.isForwardOrBack = true window.history.forward() if (this.historyIndex < window.history.length) { this.historyIndex++ this.$refs.historyArrowBack.classList.remove('fa-arrow-left') if (this.historyIndex === window.history.length) { this.$refs.historyArrowForward.classList.add('fa-arrow-right') } } }, toggleSideNav: function () { this.$store.commit('toggleSideNav') }, createNewWindow: function () { if (process.env.IS_ELECTRON) { const { ipcRenderer } = require('electron') ipcRenderer.send(IpcChannels.CREATE_NEW_WINDOW) } else { // Web placeholder } }, navigate: function (route) { this.$router.push('/' + route) }, hideFilters: function () { this.showFilters = false }, updateSearchInputText: function (text) { this.$refs.searchInput.updateInputData(text) }, ...mapActions([ 'getYoutubeUrlInfo', 'invidiousAPICall' ]) } })