diff --git a/src/constants.js b/src/constants.js
index 3224725035b5b..2e2ad56bc324e 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -76,9 +76,14 @@ const SyncEvents = {
// Utils
const MAIN_PROFILE_ID = 'allChannels'
+const MiscConstants = {
+ CHANNEL_IMAGE_BROKEN: '_',
+ CHANNEL_IMAGE_NOT_EXISTENT: '//'
+}
export {
IpcChannels,
DBActions,
SyncEvents,
- MAIN_PROFILE_ID
+ MAIN_PROFILE_ID,
+ MiscConstants
}
diff --git a/src/renderer/assets/img/loading-spinner.svg b/src/renderer/assets/img/loading-spinner.svg
new file mode 100644
index 0000000000000..ee97ae8972d38
--- /dev/null
+++ b/src/renderer/assets/img/loading-spinner.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js
index 9cda4bad1504c..cc9404e7207fe 100644
--- a/src/renderer/components/data-settings/data-settings.js
+++ b/src/renderer/components/data-settings/data-settings.js
@@ -5,11 +5,11 @@ import FtButton from '../ft-button/ft-button.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtPrompt from '../ft-prompt/ft-prompt.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
+
import { MAIN_PROFILE_ID } from '../../../constants'
import { calculateColorLuminance, getRandomColor } from '../../helpers/colors'
import {
- copyToClipboard,
deepCopy,
escapeHTML,
getTodayDateStrLocalTimezone,
@@ -19,8 +19,6 @@ import {
showToast,
writeFileFromDialog,
} from '../../helpers/utils'
-import { invidiousAPICall } from '../../helpers/api/invidious'
-import { getLocalChannel } from '../../helpers/api/local'
export default defineComponent({
name: 'DataSettings',
@@ -107,6 +105,7 @@ export default defineComponent({
showToast(`${message}: ${err}`)
return
}
+
response.filePaths.forEach(filePath => {
if (filePath.endsWith('.csv')) {
this.importCsvYouTubeSubscriptions(textDecode)
@@ -214,13 +213,9 @@ export default defineComponent({
return sub !== ''
})
const subscriptions = []
- const errorList = []
-
- showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
- let count = 0
const splitCSVRegex = /(?:,|\n|^)("(?:(?:"")|[^"])*"|[^\n",]*|(?:\n|$))/g
@@ -235,93 +230,58 @@ export default defineComponent({
}).filter(channel => {
return channel.length > 0
})
- new Promise((resolve) => {
- let finishCount = 0
- ytsubs.forEach(async (yt) => {
- const { subscription, result } = await this.subscribeToChannel({
- channelId: yt[0],
- subscriptions: subscriptions,
- channelName: yt[2],
- count: count++,
- total: ytsubs.length
- })
- if (result === 1) {
- subscriptions.push(subscription)
- } else if (result === -1) {
- errorList.push(yt)
- }
- finishCount++
- if (finishCount === ytsubs.length) {
- resolve(true)
+
+ ytsubs.forEach((yt) => {
+ const channelId = yt[0]
+ if (!this.isChannelSubscribed(channelId, subscriptions)) {
+ const subscription = {
+ id: channelId,
+ name: yt[2],
+ thumbnail: null
}
- })
- }).then(_ => {
- this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
- this.updateProfile(this.primaryProfile)
- if (errorList.length !== 0) {
- errorList.forEach(e => { // log it to console for now, dedicated tab for 'error' channels needed
- console.error(`failed to import ${e[2]}. Url to channel: ${e[1]}.`)
- })
- showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
- } else {
- showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
+ subscriptions.push(subscription)
}
- }).finally(_ => {
- this.updateShowProgressBar(false)
})
+
+ this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
+ this.updateProfile(this.primaryProfile)
+ showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
+ this.updateShowProgressBar(false)
},
importYouTubeSubscriptions: async function (textDecode) {
const subscriptions = []
- const errorList = []
-
- showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
+ let count = 0
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
- let count = 0
- new Promise((resolve) => {
- let finishCount = 0
- textDecode.forEach(async (channel) => {
- const snippet = channel.snippet
- if (typeof snippet === 'undefined') {
- const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
- showToast(message)
- throw new Error('Unable to find channel data')
- }
- const { subscription, result } = await this.subscribeToChannel({
- channelId: snippet.resourceId.channelId,
- subscriptions: subscriptions,
- channelName: snippet.title,
- thumbnail: snippet.thumbnails.default.url,
- count: count++,
- total: textDecode.length
- })
- if (result === 1) {
- subscriptions.push(subscription)
- } else if (result === -1) {
- errorList.push([snippet.resourceId.channelId, `https://www.youtube.com/channel/${snippet.resourceId.channelId}`, snippet.title])
- }
- finishCount++
- if (finishCount === textDecode.length) {
- resolve(true)
- }
- })
- }).then(_ => {
- this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
- this.updateProfile(this.primaryProfile)
- if (errorList.length !== 0) {
- errorList.forEach(e => { // log it to console for now, dedicated tab for 'error' channels needed
- console.error(`failed to import ${e[2]}. Url to channel: ${e[1]}.`)
+ textDecode.forEach((channel) => {
+ const snippet = channel.snippet
+ if (typeof snippet === 'undefined') {
+ const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
+ showToast(message)
+ throw new Error('Unable to find channel data')
+ }
+
+ const channelId = snippet.resourceId.channelId
+ if (!this.isChannelSubscribed(channelId, subscriptions)) {
+ subscriptions.push({
+ id: channelId,
+ name: snippet.title,
+ thumbnail: snippet.thumbnails.default.url
})
- showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
- } else {
- showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
}
- }).finally(_ => {
- this.updateShowProgressBar(false)
+ count++
+
+ const progressPercentage = (count / (textDecode.length - 1)) * 100
+ this.setProgressBarPercentage(progressPercentage)
})
+
+ this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
+ this.updateProfile(this.primaryProfile)
+ showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
+ this.updateShowProgressBar(false)
},
importOpmlYouTubeSubscriptions: async function (data) {
@@ -359,15 +319,14 @@ export default defineComponent({
const subscriptions = []
- showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
-
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
let count = 0
- feedData.forEach(async (channel) => {
+ feedData.forEach((channel) => {
const xmlUrl = channel.getAttribute('xmlUrl')
+ const channelName = channel.getAttribute('title')
let channelId
if (xmlUrl.includes('https://www.youtube.com/feeds/videos.xml?channel_id=')) {
channelId = new URL(xmlUrl).searchParams.get('channel_id')
@@ -377,51 +336,31 @@ export default defineComponent({
} else {
console.error(`Unknown xmlUrl format: ${xmlUrl}`)
}
- const subExists = this.primaryProfile.subscriptions.findIndex((sub) => {
- return sub.id === channelId
- })
- if (subExists === -1) {
- let channelInfo
- if (this.backendPreference === 'invidious') {
- channelInfo = await this.getChannelInfoInvidious(channelId)
- } else {
- channelInfo = await this.getChannelInfoLocal(channelId)
- }
- if (typeof channelInfo.author !== 'undefined') {
- const subscription = {
- id: channelId,
- name: channelInfo.author,
- thumbnail: channelInfo.authorThumbnails[1].url
- }
- subscriptions.push(subscription)
+ if (!this.isChannelSubscribed(channelId, subscriptions)) {
+ const subscription = {
+ id: channelId,
+ name: channelName,
+ thumbnail: null
}
+ subscriptions.push(subscription)
}
count++
const progressPercentage = (count / feedData.length) * 100
this.setProgressBarPercentage(progressPercentage)
-
- if (count === feedData.length) {
- this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
- this.updateProfile(this.primaryProfile)
-
- if (subscriptions.length < count) {
- showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
- } else {
- showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
- }
-
- this.updateShowProgressBar(false)
- }
})
+
+ this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
+ this.updateProfile(this.primaryProfile)
+ showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
+ this.updateShowProgressBar(false)
},
importNewPipeSubscriptions: async function (newPipeData) {
if (typeof newPipeData.subscriptions === 'undefined') {
showToast(this.$t('Settings.Data Settings.Invalid subscriptions file'))
-
return
}
@@ -430,51 +369,32 @@ export default defineComponent({
})
const subscriptions = []
- const errorList = []
-
- showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
let count = 0
- new Promise((resolve) => {
- let finishCount = 0
- newPipeSubscriptions.forEach(async (channel, index) => {
- const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
- const { subscription, result } = await this.subscribeToChannel({
- channelId: channelId,
- subscriptions: subscriptions,
- channelName: channel.name,
- count: count++,
- total: newPipeSubscriptions.length
- })
- if (result === 1) {
- subscriptions.push(subscription)
- }
- if (result === -1) {
- errorList.push([channelId, channel.url, channel.name])
- }
- finishCount++
- if (finishCount === newPipeSubscriptions.length) {
- resolve(true)
- }
- })
- }).then(_ => {
- this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
- this.updateProfile(this.primaryProfile)
- if (errorList.count > 0) {
- errorList.forEach(e => { // log it to console for now, dedicated tab for 'error' channels needed
- console.error(`failed to import ${e[2]}. Url to channel: ${e[1]}.`)
+ newPipeSubscriptions.forEach((channel) => {
+ const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
+
+ if (!this.isChannelSubscribed(channelId, subscriptions)) {
+ subscriptions.push({
+ id: channelId,
+ name: channel.name,
+ thumbnail: null
})
- showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
- } else {
- showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
}
- }).finally(_ => {
- this.updateShowProgressBar(false)
+
+ count++
+
+ const progressPercentage = (count / (newPipeSubscriptions.length - 1)) * 100
+ this.setProgressBarPercentage(progressPercentage)
})
+ this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
+ this.updateProfile(this.primaryProfile)
+ showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
+ this.updateShowProgressBar(false)
},
exportSubscriptions: function (option) {
@@ -620,9 +540,10 @@ export default defineComponent({
let exportText = 'Channel ID,Channel URL,Channel title\n'
this.profileList[0].subscriptions.forEach((channel) => {
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
- let channelName = channel.name
- if (channelName.search(',') !== -1) { // add quotations and escape existing quotations if channel has comma in name
- channelName = `"${channelName.replaceAll('"', '""')}"`
+ // escape quotations in channel name
+ let channelName = channel.name.replaceAll('"', '""')
+ if (channelName.search(',') !== -1) { // put channel name inside quotations if there's a comma in the name
+ channelName = `"${channelName}"`
}
exportText += `${channel.id},${channelUrl},${channelName}\n`
})
@@ -1136,105 +1057,6 @@ export default defineComponent({
showToast(successMessage)
},
- getChannelInfoInvidious: function (channelId) {
- return new Promise((resolve, reject) => {
- const subscriptionsPayload = {
- resource: 'channels',
- id: channelId,
- params: {}
- }
-
- invidiousAPICall(subscriptionsPayload).then((response) => {
- resolve(response)
- }).catch((err) => {
- const errorMessage = this.$t('Invidious API Error (Click to copy)')
- showToast(`${errorMessage}: ${err}`, 10000, () => {
- copyToClipboard(err)
- })
-
- if (process.env.SUPPORTS_LOCAL_API && this.backendFallback && this.backendPreference === 'invidious') {
- showToast(this.$t('Falling back to Local API'))
- resolve(this.getChannelInfoLocal(channelId))
- } else {
- resolve([])
- }
- })
- })
- },
-
- getChannelInfoLocal: async function (channelId) {
- try {
- const channel = await getLocalChannel(channelId)
-
- if (channel.alert) {
- return []
- }
-
- return {
- author: channel.header.author.name,
- authorThumbnails: channel.header.author.thumbnails
- }
- } catch (err) {
- console.error(err)
- const errorMessage = this.$t('Local API Error (Click to copy)')
- showToast(`${errorMessage}: ${err}`, 10000, () => {
- copyToClipboard(err)
- })
-
- if (this.backendFallback && this.backendPreference === 'local') {
- showToast(this.$t('Falling back to Invidious API'))
- return await this.getChannelInfoInvidious(channelId)
- } else {
- return []
- }
- }
- },
-
- /*
- TODO: allow default thumbnail to be used to limit requests to YouTube
- (thumbnail will get updated when user goes to their channel page)
- Returns:
- -1: an error occured
- 0: already subscribed
- 1: successfully subscribed
- */
- async subscribeToChannel({ channelId, subscriptions, channelName = null, thumbnail = null, count = 0, total = 0 }) {
- let result = 1
- if (this.isChannelSubscribed(channelId, subscriptions)) {
- return { subscription: null, successMessage: 0 }
- }
-
- let channelInfo
- let subscription = null
- if (channelName === null || thumbnail === null) {
- try {
- if (this.backendPreference === 'invidious') {
- channelInfo = await this.getChannelInfoInvidious(channelId)
- } else {
- channelInfo = await this.getChannelInfoLocal(channelId)
- }
- } catch (err) {
- console.error(err)
- result = -1
- }
- } else {
- channelInfo = { author: channelName, authorThumbnails: [null, { url: thumbnail }] }
- }
-
- if (typeof channelInfo.author !== 'undefined') {
- subscription = {
- id: channelId,
- name: channelInfo.author,
- thumbnail: channelInfo.authorThumbnails[1].url
- }
- } else {
- result = -1
- }
- const progressPercentage = (count / (total - 1)) * 100
- this.setProgressBarPercentage(progressPercentage)
- return { subscription, result }
- },
-
isChannelSubscribed(channelId, subscriptions) {
if (channelId === null) { return true }
const subExists = this.primaryProfile.subscriptions.findIndex((sub) => {
diff --git a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.css b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.css
index 701c0eb961aff..b60938134e770 100644
--- a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.css
+++ b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.css
@@ -50,3 +50,26 @@
text-overflow: ellipsis;
inline-size: 100%;
}
+
+/*
+ display loading image when we dont have a channel thumbnail yet
+*/
+.bubble[src="//"] {
+ content: url('../../assets/img/loading-spinner.svg');
+ animation: rotation 1s infinite linear;
+}
+
+@media (prefers-reduced-motion) {
+ .bubble[src="//"] {
+ animation: rotation 2s infinite linear;
+ }
+}
+
+@keyframes rotation {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(359deg);
+ }
+}
diff --git a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js
index c9854b34d6eb6..f84d17309f01f 100644
--- a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js
+++ b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.js
@@ -1,4 +1,5 @@
import { defineComponent } from 'vue'
+import { MiscConstants } from '../../../constants'
export default defineComponent({
name: 'FtChannelBubble',
@@ -22,7 +23,7 @@ export default defineComponent({
},
data: function () {
return {
- selected: false
+ selected: false,
}
},
computed: {
@@ -30,6 +31,9 @@ export default defineComponent({
return 'channelBubble' + this.channelId
}
},
+ created () {
+ this.CHANNEL_IMAGE_BROKEN = MiscConstants.CHANNEL_IMAGE_BROKEN
+ },
methods: {
handleClick: function (event) {
if (event instanceof KeyboardEvent) {
diff --git a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.vue b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.vue
index 53abe2f282c36..815a4a8ab8d7d 100644
--- a/src/renderer/components/ft-channel-bubble/ft-channel-bubble.vue
+++ b/src/renderer/components/ft-channel-bubble/ft-channel-bubble.vue
@@ -6,10 +6,16 @@
:to="`/channel/${channelId}`"
>
+