From 82aeaac7346d1499fa959a51857c153edbe91485 Mon Sep 17 00:00:00 2001 From: PikachuEXE Date: Thu, 29 Apr 2021 01:21:16 +0800 Subject: [PATCH] * Update top nav search input to direct to channel panel when channel URL provided (#1221) * $ Extract function which extract details from a Youtube URL * * Update top nav handling to use extract function to handle input text if it's Youtube URL * - Remove no longer used function --- src/renderer/App.js | 164 +++++++-------------- src/renderer/components/top-nav/top-nav.js | 91 +++++++++--- src/renderer/store/modules/utils.js | 145 ++++++++++++++++-- 3 files changed, 253 insertions(+), 147 deletions(-) diff --git a/src/renderer/App.js b/src/renderer/App.js index 40352e66..d7ae6993 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -297,129 +297,75 @@ export default Vue.extend({ }, handleYoutubeLink: function (href) { - const v = this + this.$store.dispatch('getYoutubeUrlInfo', href).then((result) => { + switch (result.urlType) { + case 'video': { + const { videoId, timestamp } = result - // Assume it's a video - this.$store.dispatch('getVideoParamsFromUrl', href).then(({ videoId, timestamp }) => { - if (videoId) { - v.$router.push({ - path: `/watch/${videoId}`, - query: timestamp ? { timestamp } : {} - }) - } else { - // Could be a playlist, channel, search query or hashtag - // If it's none of these, do nothing - // - // There's a limitation where some unknown URL types will be - // determined to be channels - // This is due to the ambiguity of some of the existing - // channel URL formats and there's not much that can be - // done to remedy it - - const url = new URL(href) - let urlType = 'unknown' - - const channelPattern = - /^\/(?:c\/|channel\/|user\/)?([^/]+)(?:\/join)?\/?$/ - - const typePatterns = new Map([ - ['playlist', /^\/playlist\/?$/], - ['search', /^\/results\/?$/], - ['hashtag', /^\/hashtag\/([^/?&#]+)$/], - ['channel', channelPattern] - ]) - - for (const [type, pattern] of typePatterns) { - const matchFound = pattern.test(url.pathname) - if (matchFound) { - urlType = type - break - } + this.$router.push({ + path: `/watch/${videoId}`, + query: timestamp ? { timestamp } : {} + }) + break } - switch (urlType) { - case 'playlist': { - if (!url.searchParams.has('list')) { - throw new Error('Playlist: "list" field not found') - } + case 'playlist': { + const { playlistId, query } = result - const playlistId = url.searchParams.get('list') - url.searchParams.delete('list') + this.$router.push({ + path: `/playlist/${playlistId}`, + query + }) + break + } - const query = {} - for (const [param, value] of url.searchParams) { - query[param] = value - } + case 'search': { + const { searchQuery, query } = result - v.$router.push({ - path: `/playlist/${playlistId}`, - query - }) - break + this.$router.push({ + path: `/search/${encodeURIComponent(searchQuery)}`, + query + }) + break + } + + case 'hashtag': { + // TODO: Implement a hashtag related view + let message = 'Hashtags have not yet been implemented, try again later' + if (this.$te(message) && this.$t(message) !== '') { + message = this.$t(message) } - case 'search': { - if (!url.searchParams.has('search_query')) { - throw new Error('Search: "search_query" field not found') - } + this.showToast({ + message: message + }) + break + } - const searchQuery = url.searchParams.get('search_query') - url.searchParams.delete('search_query') + case 'channel': { + const { channelId } = result - const query = { - sortBy: this.searchSettings.sortBy, - time: this.searchSettings.time, - type: this.searchSettings.type, - duration: this.searchSettings.duration - } + this.$router.push({ + path: `/channel/${channelId}` + }) + break + } - for (const [param, value] of url.searchParams) { - query[param] = value - } + case 'invalid_url': { + // Do nothing + break + } - v.$router.push({ - path: `/search/${encodeURIComponent(searchQuery)}`, - query - }) - break + default: { + // Unknown URL type + let message = 'Unknown YouTube url type, cannot be opened in app' + if (this.$te(message) && this.$t(message) !== '') { + message = this.$t(message) } - case 'hashtag': { - // TODO: Implement a hashtag related view - let message = 'Hashtags have not yet been implemented, try again later' - if (this.$te(message) && this.$t(message) !== '') { - message = this.$t(message) - } - - this.showToast({ - message: message - }) - break - } - - case 'channel': { - const channelId = url.pathname.match(channelPattern)[1] - if (!channelId) { - throw new Error('Channel: could not extract id') - } - - v.$router.push({ - path: `/channel/${channelId}` - }) - break - } - - default: { - // Unknown URL type - let message = 'Unknown YouTube url type, cannot be opened in app' - if (this.$te(message) && this.$t(message) !== '') { - message = this.$t(message) - } - - this.showToast({ - message: message - }) - } + this.showToast({ + message: message + }) } } }) diff --git a/src/renderer/components/top-nav/top-nav.js b/src/renderer/components/top-nav/top-nav.js index 0eb39716..1688454f 100644 --- a/src/renderer/components/top-nav/top-nav.js +++ b/src/renderer/components/top-nav/top-nav.js @@ -3,7 +3,6 @@ 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 $ from 'jquery' -import router from '../../router/index.js' import debounce from 'lodash.debounce' import ytSuggest from 'youtube-suggest' const { ipcRenderer } = require('electron') @@ -90,32 +89,76 @@ export default Vue.extend({ searchInput.blur() } - const { videoId, timestamp } = await this.$store.dispatch('getVideoParamsFromUrl', query) - const playlistId = await this.$store.dispatch('getPlaylistIdFromUrl', query) + this.$store.dispatch('getYoutubeUrlInfo', query).then((result) => { + switch (result.urlType) { + case 'video': { + const { videoId, timestamp } = result - console.log(playlistId) - - if (videoId) { - this.$router.push({ - path: `/watch/${videoId}`, - query: timestamp ? { timestamp } : {} - }) - } else if (playlistId) { - this.$router.push({ - path: `/playlist/${playlistId}` - }) - } else { - router.push({ - path: `/search/${encodeURIComponent(query)}`, - query: { - sortBy: this.searchSettings.sortBy, - time: this.searchSettings.time, - type: this.searchSettings.type, - duration: this.searchSettings.duration + this.$router.push({ + path: `/watch/${videoId}`, + query: timestamp ? { timestamp } : {} + }) + break } - }) - } + case 'playlist': { + const { playlistId, query } = result + + this.$router.push({ + path: `/playlist/${playlistId}`, + query + }) + break + } + + case 'search': { + const { searchQuery, query } = result + + this.$router.push({ + path: `/search/${encodeURIComponent(searchQuery)}`, + query + }) + break + } + + case 'hashtag': { + // TODO: Implement a hashtag related view + let message = 'Hashtags have not yet been implemented, try again later' + if (this.$te(message) && this.$t(message) !== '') { + message = this.$t(message) + } + + this.showToast({ + message: message + }) + break + } + + case 'channel': { + const { channelId } = result + + this.$router.push({ + path: `/channel/${channelId}` + }) + break + } + + case 'invalid_url': + default: { + this.$router.push({ + path: `/search/${encodeURIComponent(query)}`, + query: { + sortBy: this.searchSettings.sortBy, + time: this.searchSettings.time, + type: this.searchSettings.type, + duration: this.searchSettings.duration + } + }) + } + } + }) + + // Close the filter panel this.showFilters = false }, diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js index 4bd882e6..5223a37c 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -239,25 +239,142 @@ const actions = { return extractors.reduce((a, c) => a || c(), null) || paramsObject }, - getPlaylistIdFromUrl (_, url) { - /** @type {URL} */ - let urlObject - try { - urlObject = new URL(url) - } catch (e) { - return false + getYoutubeUrlInfo (_, urlStr) { + // Returns + // - urlType [String] `video`, `playlist` + // + // If `urlType` is "video" + // - videoId [String] + // - timestamp [String] + // + // If `urlType` is "playlist" + // - playlistId [String] + // - query [Object] + // + // If `urlType` is "search" + // - searchQuery [String] + // - query [Object] + // + // If `urlType` is "hashtag" + // Nothing else + // + // If `urlType` is "channel" + // - channelId [String] + // + // If `urlType` is "unknown" + // Nothing else + // + // If `urlType` is "invalid_url" + // Nothing else + const { videoId, timestamp } = actions.getVideoParamsFromUrl(null, urlStr) + if (videoId) { + return { + urlType: 'video', + videoId, + timestamp + } } - const extractors = [ - // anything with /playlist?list= - function() { - if (urlObject.pathname === '/playlist' && urlObject.searchParams.has('list')) { - return urlObject.searchParams.get('list') + let url + try { + url = new URL(urlStr) + } catch { + return { + urlType: 'invalid_url' + } + } + let urlType = 'unknown' + + const channelPattern = + /^\/(?:c\/|channel\/|user\/)?([^/]+)(?:\/join)?\/?$/ + + const typePatterns = new Map([ + ['playlist', /^\/playlist\/?$/], + ['search', /^\/results\/?$/], + ['hashtag', /^\/hashtag\/([^/?&#]+)$/], + ['channel', channelPattern] + ]) + + for (const [type, pattern] of typePatterns) { + const matchFound = pattern.test(url.pathname) + if (matchFound) { + urlType = type + break + } + } + + switch (urlType) { + case 'playlist': { + if (!url.searchParams.has('list')) { + throw new Error('Playlist: "list" field not found') + } + + const playlistId = url.searchParams.get('list') + url.searchParams.delete('list') + + const query = {} + for (const [param, value] of url.searchParams) { + query[param] = value + } + + return { + urlType: 'playlist', + playlistId, + query } } - ] - return extractors.reduce((a, c) => a || c(), null) || false + case 'search': { + if (!url.searchParams.has('search_query')) { + throw new Error('Search: "search_query" field not found') + } + + const searchQuery = url.searchParams.get('search_query') + url.searchParams.delete('search_query') + + const query = { + sortBy: this.searchSettings.sortBy, + time: this.searchSettings.time, + type: this.searchSettings.type, + duration: this.searchSettings.duration + } + + for (const [param, value] of url.searchParams) { + query[param] = value + } + + return { + urlType: 'search', + searchQuery, + query + } + } + + case 'hashtag': { + return { + urlType: 'hashtag' + } + } + + case 'channel': { + const channelId = url.pathname.match(channelPattern)[1] + if (!channelId) { + throw new Error('Channel: could not extract id') + } + + return { + urlType: 'channel', + channelId + } + } + + default: { + // Unknown URL type + return { + urlType: 'unknown' + } + } + } }, padNumberWithLeadingZeros(_, payload) {